diff --git a/README.md b/README.md index aad5def0..07f6f51b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ An experimental test runner for Python 3.6+ that is heavily inspired by `pytest` This project is a work in progress. Some of the features that are currently available in a basic form are listed below. * Modular setup/teardown with fixtures and dependency injection -* Highly readable, colourful diffs intended to be as readable as possible +* Colourful, human readable diffs allowing you to quickly pinpoint issues * A human readable assertion API * Tested on Mac OS, Linux, and Windows * stderr/stdout captured during test and fixture execution @@ -25,7 +25,6 @@ Planned features: * Handling flaky tests with test-specific retries, timeouts * Integration with unittest.mock (specifics to be ironed out) * Plugin system -* Highlighting diffs on a per-character basis, similar to [diff-so-fancy](https://github.com/so-fancy/diff-so-fancy) (right now it's just per line) ## Quick Start diff --git a/screenshot.png b/screenshot.png index 5c4b2c63..8626c5c0 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/setup.py b/setup.py index 68ea4751..c7927fde 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ with open("README.md", "r") as fh: long_description = fh.read() -version = "0.8.0a0" +version = "0.9.0a0" setup( name="ward", diff --git a/tests/test_suite.py b/tests/test_suite.py index b397a72d..54ba3521 100644 --- a/tests/test_suite.py +++ b/tests/test_suite.py @@ -127,50 +127,3 @@ def my_test(fix_a, fix_b): list(suite.generate_test_runs()) expect(events).equals([1, 2, 3]) - - -# region example - - -def get_capitals_from_server(): - return { - "glasgow": "scotland", - "tokyo": "japan", - "london": "england", - "warsaw": "poland", - "berlin": "germany", - "madrid": "spain", - } - - -@fixture -def cities(): - yield { - "edinburgh": "scotland", - "tokyo": "japan", - "london": "england", - "warsaw": "poland", - "berlin": "germany", - "madrid": "spain", - } - - -@skip -def test_capital_cities(cities): - found_cities = get_capitals_from_server() - - def all_keys_less_than_length_10(cities): - return all(len(k) < 10 for k in cities) - - ( - expect(found_cities) - .satisfies(lambda c: all(len(k) < 10 for k in c)) - .satisfies(all_keys_less_than_length_10) - .is_instance_of(dict) - .contains("tokyo") - .has_length(6) - .equals(cities) - ) - - -# endregion example diff --git a/ward/diff.py b/ward/diff.py index 859f56c3..f2732387 100644 --- a/ward/diff.py +++ b/ward/diff.py @@ -1,19 +1,21 @@ import difflib import pprint +from colorama import Style, Fore from termcolor import colored def build_auto_diff(lhs, rhs, width=60) -> str: """Determines the best type of diff to use based on the output""" + if isinstance(lhs, str): + lhs_repr = lhs + else: + lhs_repr = pprint.pformat(lhs, width=width) - lhs_repr = pprint.pformat(lhs, width=width) - rhs_repr = pprint.pformat(rhs, width=width) - # TODO: Right now, just stick to unified diff while deciding what to do - # if "\n" in lhs_repr and "\n" in rhs_repr: - # diff = build_unified_diff(lhs_repr, rhs_repr) - # else: - # diff = build_split_diff(lhs_repr, rhs_repr) + if isinstance(rhs, str): + rhs_repr = rhs + else: + rhs_repr = pprint.pformat(rhs, width=width) return build_unified_diff(lhs_repr, rhs_repr) @@ -55,23 +57,67 @@ def build_split_diff(lhs_repr, rhs_repr) -> str: return f"LHS: {lhs_out}\nRHS: {rhs_out}" +def bright_red(s: str) -> str: + return f"{Fore.LIGHTRED_EX}{s}{Style.RESET_ALL}" + + +def bright_green(s: str) -> str: + return f"{Fore.LIGHTGREEN_EX}{s}{Style.RESET_ALL}" + + def build_unified_diff(lhs_repr, rhs_repr, margin_left=4) -> str: differ = difflib.Differ() lines_lhs = lhs_repr.splitlines() lines_rhs = rhs_repr.splitlines() diff = differ.compare(lines_lhs, lines_rhs) - output = [] - for line in diff: + output_lines = [] + prev_marker = "" + for line_idx, line in enumerate(diff): if line.startswith("- "): - output.append(colored(line[2:], color="green")) + output_lines.append(colored(line[2:], color="green")) elif line.startswith("+ "): - output.append(colored(line[2:], color="red")) + output_lines.append(colored(line[2:], color="red")) elif line.startswith("? "): - # We can use this to find the index of change in - # the line above if required in the future - pass + last_output_idx = len(output_lines) - 1 + # Remove the 5 char escape code from the line + esc_code_length = 5 + line_to_rewrite = output_lines[last_output_idx][esc_code_length:] + output_lines[last_output_idx] = "" # We'll rewrite the prev line with highlights + current_span = "" + index = 2 # Differ lines start with a 2 letter code, so skip past that + char = line[index] + prev_char = char + while index < len(line): + char = line[index] + if prev_marker in "+-": + if char != prev_char: + if prev_char == " " and prev_marker == "+": + output_lines[last_output_idx] += colored(current_span, color="red") + elif prev_char == " " and prev_marker == "-": + output_lines[last_output_idx] += colored(current_span, color="green") + elif prev_char in "+^" and prev_marker == "+": + output_lines[last_output_idx] += bright_red( + colored(current_span, on_color="on_red", attrs=["bold"])) + elif prev_char in "-^" and prev_marker == "-": + output_lines[last_output_idx] += bright_green( + colored(current_span, on_color="on_green", attrs=["bold"])) + current_span = "" + current_span += line_to_rewrite[index - 2] # Subtract 2 to account for code at start of line + prev_char = char + index += 1 + + # Lines starting with ? aren't guaranteed to be the same length as the lines before them + # so some characters may be left over. Add any leftover characters to the output + remaining_index = index - 3 # subtract 2 for code at start, and 1 to remove the newline char + if prev_marker == "+": + output_lines[last_output_idx] += colored(line_to_rewrite[remaining_index:], color="red") + elif prev_marker == "-": + output_lines[last_output_idx] += colored(line_to_rewrite[remaining_index:], color="green") + + else: - output.append(line[2:]) + output_lines.append(line[2:]) + prev_marker = line[0] - return " " * margin_left + f"\n{' ' * margin_left}".join(output) + return " " * margin_left + f"\n{' ' * margin_left}".join(output_lines) diff --git a/ward/terminal.py b/ward/terminal.py index 174c4bbb..e9420e19 100644 --- a/ward/terminal.py +++ b/ward/terminal.py @@ -123,7 +123,7 @@ def output_single_test_result(self, test_result: TestResult): def output_why_test_failed_header(self, test_result: TestResult): print( - colored(" Failure", color="red", attrs=["bold"]), + colored(" Failure", color="red"), "in", colored(test_result.test.qualified_name, attrs=["bold"]), ) @@ -178,9 +178,9 @@ def output_test_result_summary(self, test_results: List[TestResult], time_taken: exit_code = get_exit_code(test_results) if exit_code == ExitCode.FAILED: - result = colored(exit_code.name, color="red", attrs=["bold"]) + result = colored(exit_code.name, color="red") else: - result = colored(exit_code.name, color="green", attrs=["bold"]) + result = colored(exit_code.name, color="green") print( f"{result} in {time_taken:.2f} seconds [ " f"{colored(str(outcome_counts[TestOutcome.FAIL]) + ' failed', color='red')} "