Skip to content

Commit 9b82120

Browse files
add support for printing the diff of AST trees when running tests (#3902)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 3dcacdd commit 9b82120

File tree

6 files changed

+125
-16
lines changed

6 files changed

+125
-16
lines changed

docs/contributing/the_basics.md

+38
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,44 @@ the root of the black repo:
3737
(.venv)$ tox -e run_self
3838
```
3939

40+
### Development
41+
42+
Further examples of invoking the tests
43+
44+
```console
45+
# Run all of the above mentioned, in parallel
46+
(.venv)$ tox --parallel=auto
47+
48+
# Run tests on a specific python version
49+
(.venv)$ tox -e py39
50+
51+
# pass arguments to pytest
52+
(.venv)$ tox -e py -- --no-cov
53+
54+
# print full tree diff, see documentation below
55+
(.venv)$ tox -e py -- --print-full-tree
56+
57+
# disable diff printing, see documentation below
58+
(.venv)$ tox -e py -- --print-tree-diff=False
59+
```
60+
61+
`Black` has two pytest command-line options affecting test files in `tests/data/` that
62+
are split into an input part, and an output part, separated by a line with`# output`.
63+
These can be passed to `pytest` through `tox`, or directly into pytest if not using
64+
`tox`.
65+
66+
#### `--print-full-tree`
67+
68+
Upon a failing test, print the full concrete syntax tree (CST) as it is after processing
69+
the input ("actual"), and the tree that's yielded after parsing the output ("expected").
70+
Note that a test can fail with different output with the same CST. This used to be the
71+
default, but now defaults to `False`.
72+
73+
#### `--print-tree-diff`
74+
75+
Upon a failing test, print the diff of the trees as described above. This is the
76+
default. To turn it off pass `--print-tree-diff=False`.
77+
4078
### News / Changelog Requirement
4179

4280
`Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If

src/black/debug.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from dataclasses import dataclass
2-
from typing import Iterator, TypeVar, Union
1+
from dataclasses import dataclass, field
2+
from typing import Any, Iterator, List, TypeVar, Union
33

44
from black.nodes import Visitor
55
from black.output import out
@@ -14,26 +14,33 @@
1414
@dataclass
1515
class DebugVisitor(Visitor[T]):
1616
tree_depth: int = 0
17+
list_output: List[str] = field(default_factory=list)
18+
print_output: bool = True
19+
20+
def out(self, message: str, *args: Any, **kwargs: Any) -> None:
21+
self.list_output.append(message)
22+
if self.print_output:
23+
out(message, *args, **kwargs)
1724

1825
def visit_default(self, node: LN) -> Iterator[T]:
1926
indent = " " * (2 * self.tree_depth)
2027
if isinstance(node, Node):
2128
_type = type_repr(node.type)
22-
out(f"{indent}{_type}", fg="yellow")
29+
self.out(f"{indent}{_type}", fg="yellow")
2330
self.tree_depth += 1
2431
for child in node.children:
2532
yield from self.visit(child)
2633

2734
self.tree_depth -= 1
28-
out(f"{indent}/{_type}", fg="yellow", bold=False)
35+
self.out(f"{indent}/{_type}", fg="yellow", bold=False)
2936
else:
3037
_type = token.tok_name.get(node.type, str(node.type))
31-
out(f"{indent}{_type}", fg="blue", nl=False)
38+
self.out(f"{indent}{_type}", fg="blue", nl=False)
3239
if node.prefix:
3340
# We don't have to handle prefixes for `Node` objects since
3441
# that delegates to the first child anyway.
35-
out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
36-
out(f" {node.value!r}", fg="blue", bold=False)
42+
self.out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
43+
self.out(f" {node.value!r}", fg="blue", bold=False)
3744

3845
@classmethod
3946
def show(cls, code: Union[str, Leaf, Node]) -> None:

tests/conftest.py

+27
Original file line numberDiff line numberDiff line change
@@ -1 +1,28 @@
1+
import pytest
2+
13
pytest_plugins = ["tests.optional"]
4+
5+
PRINT_FULL_TREE: bool = False
6+
PRINT_TREE_DIFF: bool = True
7+
8+
9+
def pytest_addoption(parser: pytest.Parser) -> None:
10+
parser.addoption(
11+
"--print-full-tree",
12+
action="store_true",
13+
default=False,
14+
help="print full syntax trees on failed tests",
15+
)
16+
parser.addoption(
17+
"--print-tree-diff",
18+
action="store_true",
19+
default=True,
20+
help="print diff of syntax trees on failed tests",
21+
)
22+
23+
24+
def pytest_configure(config: pytest.Config) -> None:
25+
global PRINT_FULL_TREE
26+
global PRINT_TREE_DIFF
27+
PRINT_FULL_TREE = config.getoption("--print-full-tree")
28+
PRINT_TREE_DIFF = config.getoption("--print-tree-diff")

tests/test_black.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import re
1010
import sys
1111
import types
12-
import unittest
1312
from concurrent.futures import ThreadPoolExecutor
1413
from contextlib import contextmanager, redirect_stderr
1514
from dataclasses import replace
@@ -1047,9 +1046,10 @@ def test_endmarker(self) -> None:
10471046
self.assertEqual(len(n.children), 1)
10481047
self.assertEqual(n.children[0].type, black.token.ENDMARKER)
10491048

1049+
@patch("tests.conftest.PRINT_FULL_TREE", True)
1050+
@patch("tests.conftest.PRINT_TREE_DIFF", False)
10501051
@pytest.mark.incompatible_with_mypyc
1051-
@unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
1052-
def test_assertFormatEqual(self) -> None:
1052+
def test_assertFormatEqual_print_full_tree(self) -> None:
10531053
out_lines = []
10541054
err_lines = []
10551055

@@ -1068,6 +1068,29 @@ def err(msg: str, **kwargs: Any) -> None:
10681068
self.assertIn("Actual tree:", out_str)
10691069
self.assertEqual("".join(err_lines), "")
10701070

1071+
@patch("tests.conftest.PRINT_FULL_TREE", False)
1072+
@patch("tests.conftest.PRINT_TREE_DIFF", True)
1073+
@pytest.mark.incompatible_with_mypyc
1074+
def test_assertFormatEqual_print_tree_diff(self) -> None:
1075+
out_lines = []
1076+
err_lines = []
1077+
1078+
def out(msg: str, **kwargs: Any) -> None:
1079+
out_lines.append(msg)
1080+
1081+
def err(msg: str, **kwargs: Any) -> None:
1082+
err_lines.append(msg)
1083+
1084+
with patch("black.output._out", out), patch("black.output._err", err):
1085+
with self.assertRaises(AssertionError):
1086+
self.assertFormatEqual("j = [1, 2, 3]\n", "j = [1, 2, 3,]\n")
1087+
1088+
out_str = "".join(out_lines)
1089+
self.assertIn("Tree Diff:", out_str)
1090+
self.assertIn("+ COMMA", out_str)
1091+
self.assertIn("+ ','", out_str)
1092+
self.assertEqual("".join(err_lines), "")
1093+
10711094
@event_loop()
10721095
@patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
10731096
def test_works_in_mono_process_only_environment(self) -> None:

tests/util.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from black.mode import TargetVersion
1313
from black.output import diff, err, out
1414

15+
from . import conftest
16+
1517
PYTHON_SUFFIX = ".py"
1618
ALLOWED_SUFFIXES = (PYTHON_SUFFIX, ".pyi", ".out", ".diff", ".ipynb")
1719

@@ -34,22 +36,34 @@
3436

3537

3638
def _assert_format_equal(expected: str, actual: str) -> None:
37-
if actual != expected and not os.environ.get("SKIP_AST_PRINT"):
39+
if actual != expected and (conftest.PRINT_FULL_TREE or conftest.PRINT_TREE_DIFF):
3840
bdv: DebugVisitor[Any]
39-
out("Expected tree:", fg="green")
41+
actual_out: str = ""
42+
expected_out: str = ""
43+
if conftest.PRINT_FULL_TREE:
44+
out("Expected tree:", fg="green")
4045
try:
4146
exp_node = black.lib2to3_parse(expected)
42-
bdv = DebugVisitor()
47+
bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE)
4348
list(bdv.visit(exp_node))
49+
expected_out = "\n".join(bdv.list_output)
4450
except Exception as ve:
4551
err(str(ve))
46-
out("Actual tree:", fg="red")
52+
if conftest.PRINT_FULL_TREE:
53+
out("Actual tree:", fg="red")
4754
try:
4855
exp_node = black.lib2to3_parse(actual)
49-
bdv = DebugVisitor()
56+
bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE)
5057
list(bdv.visit(exp_node))
58+
actual_out = "\n".join(bdv.list_output)
5159
except Exception as ve:
5260
err(str(ve))
61+
if conftest.PRINT_TREE_DIFF:
62+
out("Tree Diff:")
63+
out(
64+
diff(expected_out, actual_out, "expected tree", "actual tree")
65+
or "Trees do not differ"
66+
)
5367

5468
if actual != expected:
5569
out(diff(expected, actual, "expected", "actual"))

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tox]
22
isolated_build = true
3-
envlist = {,ci-}py{37,38,39,310,311,py3},fuzz,run_self
3+
envlist = {,ci-}py{38,39,310,311,py3},fuzz,run_self
44

55
[testenv]
66
setenv =

0 commit comments

Comments
 (0)