Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #43 from darrenburns/diff-improvements
Browse files Browse the repository at this point in the history
Human-readable diff initial implementation
  • Loading branch information
darrenburns authored Oct 19, 2019
2 parents 1f5ebe8 + fad4c75 commit 3a0d436
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 69 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Binary file modified screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 0 additions & 47 deletions tests/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
78 changes: 62 additions & 16 deletions ward/diff.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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)
6 changes: 3 additions & 3 deletions ward/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
)
Expand Down Expand Up @@ -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')} "
Expand Down

0 comments on commit 3a0d436

Please sign in to comment.