Skip to content

Commit

Permalink
add support for printing the diff of AST trees when running tests (#3902
Browse files Browse the repository at this point in the history
)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
jakkdl and JelleZijlstra committed Sep 28, 2023
1 parent 3dcacdd commit 9b82120
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 16 deletions.
38 changes: 38 additions & 0 deletions docs/contributing/the_basics.md
Expand Up @@ -37,6 +37,44 @@ the root of the black repo:
(.venv)$ tox -e run_self
```

### Development

Further examples of invoking the tests

```console
# Run all of the above mentioned, in parallel
(.venv)$ tox --parallel=auto

# Run tests on a specific python version
(.venv)$ tox -e py39

# pass arguments to pytest
(.venv)$ tox -e py -- --no-cov

# print full tree diff, see documentation below
(.venv)$ tox -e py -- --print-full-tree

# disable diff printing, see documentation below
(.venv)$ tox -e py -- --print-tree-diff=False
```

`Black` has two pytest command-line options affecting test files in `tests/data/` that
are split into an input part, and an output part, separated by a line with`# output`.
These can be passed to `pytest` through `tox`, or directly into pytest if not using
`tox`.

#### `--print-full-tree`

Upon a failing test, print the full concrete syntax tree (CST) as it is after processing
the input ("actual"), and the tree that's yielded after parsing the output ("expected").
Note that a test can fail with different output with the same CST. This used to be the
default, but now defaults to `False`.

#### `--print-tree-diff`

Upon a failing test, print the diff of the trees as described above. This is the
default. To turn it off pass `--print-tree-diff=False`.

### News / Changelog Requirement

`Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If
Expand Down
21 changes: 14 additions & 7 deletions src/black/debug.py
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Iterator, TypeVar, Union
from dataclasses import dataclass, field
from typing import Any, Iterator, List, TypeVar, Union

from black.nodes import Visitor
from black.output import out
Expand All @@ -14,26 +14,33 @@
@dataclass
class DebugVisitor(Visitor[T]):
tree_depth: int = 0
list_output: List[str] = field(default_factory=list)
print_output: bool = True

def out(self, message: str, *args: Any, **kwargs: Any) -> None:
self.list_output.append(message)
if self.print_output:
out(message, *args, **kwargs)

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

self.tree_depth -= 1
out(f"{indent}/{_type}", fg="yellow", bold=False)
self.out(f"{indent}/{_type}", fg="yellow", bold=False)
else:
_type = token.tok_name.get(node.type, str(node.type))
out(f"{indent}{_type}", fg="blue", nl=False)
self.out(f"{indent}{_type}", fg="blue", nl=False)
if node.prefix:
# We don't have to handle prefixes for `Node` objects since
# that delegates to the first child anyway.
out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
out(f" {node.value!r}", fg="blue", bold=False)
self.out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
self.out(f" {node.value!r}", fg="blue", bold=False)

@classmethod
def show(cls, code: Union[str, Leaf, Node]) -> None:
Expand Down
27 changes: 27 additions & 0 deletions tests/conftest.py
@@ -1 +1,28 @@
import pytest

pytest_plugins = ["tests.optional"]

PRINT_FULL_TREE: bool = False
PRINT_TREE_DIFF: bool = True


def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--print-full-tree",
action="store_true",
default=False,
help="print full syntax trees on failed tests",
)
parser.addoption(
"--print-tree-diff",
action="store_true",
default=True,
help="print diff of syntax trees on failed tests",
)


def pytest_configure(config: pytest.Config) -> None:
global PRINT_FULL_TREE
global PRINT_TREE_DIFF
PRINT_FULL_TREE = config.getoption("--print-full-tree")
PRINT_TREE_DIFF = config.getoption("--print-tree-diff")
29 changes: 26 additions & 3 deletions tests/test_black.py
Expand Up @@ -9,7 +9,6 @@
import re
import sys
import types
import unittest
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager, redirect_stderr
from dataclasses import replace
Expand Down Expand Up @@ -1047,9 +1046,10 @@ def test_endmarker(self) -> None:
self.assertEqual(len(n.children), 1)
self.assertEqual(n.children[0].type, black.token.ENDMARKER)

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

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

@patch("tests.conftest.PRINT_FULL_TREE", False)
@patch("tests.conftest.PRINT_TREE_DIFF", True)
@pytest.mark.incompatible_with_mypyc
def test_assertFormatEqual_print_tree_diff(self) -> None:
out_lines = []
err_lines = []

def out(msg: str, **kwargs: Any) -> None:
out_lines.append(msg)

def err(msg: str, **kwargs: Any) -> None:
err_lines.append(msg)

with patch("black.output._out", out), patch("black.output._err", err):
with self.assertRaises(AssertionError):
self.assertFormatEqual("j = [1, 2, 3]\n", "j = [1, 2, 3,]\n")

out_str = "".join(out_lines)
self.assertIn("Tree Diff:", out_str)
self.assertIn("+ COMMA", out_str)
self.assertIn("+ ','", out_str)
self.assertEqual("".join(err_lines), "")

@event_loop()
@patch("concurrent.futures.ProcessPoolExecutor", MagicMock(side_effect=OSError))
def test_works_in_mono_process_only_environment(self) -> None:
Expand Down
24 changes: 19 additions & 5 deletions tests/util.py
Expand Up @@ -12,6 +12,8 @@
from black.mode import TargetVersion
from black.output import diff, err, out

from . import conftest

PYTHON_SUFFIX = ".py"
ALLOWED_SUFFIXES = (PYTHON_SUFFIX, ".pyi", ".out", ".diff", ".ipynb")

Expand All @@ -34,22 +36,34 @@


def _assert_format_equal(expected: str, actual: str) -> None:
if actual != expected and not os.environ.get("SKIP_AST_PRINT"):
if actual != expected and (conftest.PRINT_FULL_TREE or conftest.PRINT_TREE_DIFF):
bdv: DebugVisitor[Any]
out("Expected tree:", fg="green")
actual_out: str = ""
expected_out: str = ""
if conftest.PRINT_FULL_TREE:
out("Expected tree:", fg="green")
try:
exp_node = black.lib2to3_parse(expected)
bdv = DebugVisitor()
bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE)
list(bdv.visit(exp_node))
expected_out = "\n".join(bdv.list_output)
except Exception as ve:
err(str(ve))
out("Actual tree:", fg="red")
if conftest.PRINT_FULL_TREE:
out("Actual tree:", fg="red")
try:
exp_node = black.lib2to3_parse(actual)
bdv = DebugVisitor()
bdv = DebugVisitor(print_output=conftest.PRINT_FULL_TREE)
list(bdv.visit(exp_node))
actual_out = "\n".join(bdv.list_output)
except Exception as ve:
err(str(ve))
if conftest.PRINT_TREE_DIFF:
out("Tree Diff:")
out(
diff(expected_out, actual_out, "expected tree", "actual tree")
or "Trees do not differ"
)

if actual != expected:
out(diff(expected, actual, "expected", "actual"))
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
@@ -1,6 +1,6 @@
[tox]
isolated_build = true
envlist = {,ci-}py{37,38,39,310,311,py3},fuzz,run_self
envlist = {,ci-}py{38,39,310,311,py3},fuzz,run_self

[testenv]
setenv =
Expand Down

0 comments on commit 9b82120

Please sign in to comment.