From d4c2002e649b3e692d4c921e1fac04451c7bec81 Mon Sep 17 00:00:00 2001 From: Stuart Laughlin Date: Thu, 29 Jan 2026 13:55:48 -0600 Subject: [PATCH 1/4] Add --collect-only-tree (--co-tree) flag for modern tree output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because it's no longer 2005 and we don't have to pretend we're writing XML. The new --co-tree flag displays collected tests as a tree with box-drawing characters (├── └── │) instead of the classic format. Features: - Box-drawing characters for TTY, ASCII fallback for non-TTY - Colors: bold for files/dirs, cyan for classes, green for functions - Custom collector types shown with (TypeName) annotation - Proper deduplication for overlapping collection paths Architecture: - New CollectionTree class separates tree-building from rendering - Classic --collect-only unchanged (preserves execution order view) - Tree view shows structural hierarchy (groups by module/class) Design note: --co-tree deliberately shows structural organization rather than execution order. It does not reflect pytest_collection_modifyitems reordering. Use --co for execution order, --co-tree for structure. --- src/_pytest/cacheprovider.py | 2 +- src/_pytest/logging.py | 2 +- src/_pytest/main.py | 9 +- src/_pytest/terminal.py | 249 +++++++++++++++++++++++++++++++++-- testing/test_terminal.py | 115 ++++++++++++++++ 5 files changed, 364 insertions(+), 13 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 4383f105af6..d5d1d20cd8f 100644 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -468,7 +468,7 @@ def pytest_sessionfinish(self) -> None: if config.getoption("cacheshow") or hasattr(config, "workerinput"): return - if config.getoption("collectonly"): + if config.getoption("collectonly") or config.getoption("collect_only_tree"): return assert config.cache is not None diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 6f34c1b93fd..aea862cac77 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -789,7 +789,7 @@ def pytest_collection(self) -> Generator[None]: @hookimpl(wrapper=True) def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]: - if session.config.option.collectonly: + if session.config.option.collectonly or session.config.option.collect_only_tree: return (yield) if self._log_cli_enabled() and self._config.get_verbosity() < 1: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 02c7fb373fd..450d3423224 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -141,6 +141,13 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", help="Only collect tests, don't execute them", ) + group.addoption( + "--collect-only-tree", + "--co-tree", + action="store_true", + dest="collect_only_tree", + help="Like --collect-only, but display as a tree with box-drawing characters", + ) group.addoption( "--pyargs", action="store_true", @@ -387,7 +394,7 @@ def pytest_runtestloop(session: Session) -> bool: f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection" ) - if session.config.option.collectonly: + if session.config.option.collectonly or session.config.option.collect_only_tree: return True for i, item in enumerate(session.items): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bb6f35633b9..ebb5a309827 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -375,6 +375,210 @@ def get_location(self, config: Config) -> str | None: return None +@dataclasses.dataclass +class CollectionTreeNode: + """A node in the collection tree.""" + + node: Node + children: list[CollectionTreeNode] = dataclasses.field(default_factory=list) + + @property + def name(self) -> str: + return self.node.name + + +class CollectionTree: + """A tree structure built from collected test items. + + This class separates the concern of building a hierarchical representation + of collected items from the concern of rendering that structure. Different + output formats (tree, JSON, etc.) can use the same tree structure. + + Note: This builds a *structural* view of tests grouped by their hierarchy + (directory/module/class), not an *execution order* view. This means it + deliberately does not reflect reordering from hooks like + ``pytest_collection_modifyitems``. The classic ``--collect-only`` output + shows execution order; this tree view answers "how are tests organized?" + rather than "what order will tests run?". + """ + + def __init__(self, items: Sequence[Item]) -> None: + self.roots: list[CollectionTreeNode] = [] + self._node_map: dict[int, CollectionTreeNode] = {} + self._build(items) + + def _build(self, items: Sequence[Item]) -> None: + """Build the tree structure from collected items.""" + # Track seen names per parent to deduplicate overlapping collection paths + seen_names: dict[int | None, set[str]] = {None: set()} + + for item in items: + chain = item.listchain()[1:] # strip Session + parent_tree_node: CollectionTreeNode | None = None + + for pytest_node in chain: + node_id = id(pytest_node) + parent_id = id(parent_tree_node.node) if parent_tree_node else None + + # Check if we've already added this exact node object + if node_id in self._node_map: + parent_tree_node = self._node_map[node_id] + continue + + # Check for name collision (different object, same name under same parent) + if parent_id not in seen_names: + seen_names[parent_id] = set() + if pytest_node.name in seen_names[parent_id]: + # Find existing node with this name + if parent_tree_node is None: + for root in self.roots: + if root.name == pytest_node.name: + parent_tree_node = root + break + else: + for child in parent_tree_node.children: + if child.name == pytest_node.name: + parent_tree_node = child + break + continue + seen_names[parent_id].add(pytest_node.name) + + # Create new tree node + tree_node = CollectionTreeNode(node=pytest_node) + self._node_map[node_id] = tree_node + + if parent_tree_node is None: + self.roots.append(tree_node) + else: + parent_tree_node.children.append(tree_node) + + parent_tree_node = tree_node + + def render_classic(self, tw: TerminalWriter, verbosity: int = 0) -> None: + """Render the tree in classic format.""" + + def render_node(tree_node: CollectionTreeNode, depth: int) -> None: + indent = " " * depth + tw.line(f"{indent}{tree_node.node}") + if verbosity >= 1: + obj = getattr(tree_node.node, "obj", None) + doc = inspect.getdoc(obj) if obj else None + if doc: + for line in doc.splitlines(): + tw.line(f"{indent} {line}") + for child in tree_node.children: + render_node(child, depth + 1) + + for root in self.roots: + render_node(root, 0) + + def render_tree( + self, tw: TerminalWriter, verbosity: int = 0, use_markup: bool = True + ) -> None: + """Render the tree with box-drawing characters.""" + # Import here to avoid circular imports + from _pytest.main import Dir + from _pytest.main import Session + from _pytest.python import Class + from _pytest.python import Function + from _pytest.python import Module + from _pytest.python import Package + + def get_node_markup(pytest_node: Node) -> dict[str, bool]: + """Return markup kwargs for a node based on its type.""" + if not use_markup: + return {} + if isinstance(pytest_node, (Module, nodes.File)): + return {"bold": True} + elif isinstance(pytest_node, Package): + return {"bold": True, "cyan": True} + elif isinstance(pytest_node, nodes.Directory): + return {"bold": True} + elif isinstance(pytest_node, Class): + return {"cyan": True} + elif isinstance(pytest_node, Function): + return {"green": True} + return {} + + def get_node_label(pytest_node: Node) -> str: + """Return a label for a node.""" + standard_types = ( + Module, + Function, + Class, + Package, + Dir, + Session, + nodes.Directory, + nodes.File, + nodes.Item, + ) + node_type = type(pytest_node) + is_custom = not any(node_type is t for t in standard_types) and isinstance( + pytest_node, standard_types + ) + if is_custom: + return f"{pytest_node.name} ({node_type.__name__})" + return pytest_node.name + + def render_node( + tree_node: CollectionTreeNode, + depth: int, + is_last_at_level: list[bool], + is_last: bool, + ) -> None: + # Build prefix + if depth == 0: + prefix = "" + else: + prefix_parts = [] + for i in range(1, depth): + if is_last_at_level[i]: + prefix_parts.append(" ") + else: + prefix_parts.append("│ " if use_markup else "| ") + if is_last: + prefix_parts.append("└── " if use_markup else "`-- ") + else: + prefix_parts.append("├── " if use_markup else "+-- ") + prefix = "".join(prefix_parts) + + label = get_node_label(tree_node.node) + markup = get_node_markup(tree_node.node) + tw.write(prefix) + tw.line(label, **markup) + + # Print docstrings if verbosity >= 1 + if verbosity >= 1: + obj = getattr(tree_node.node, "obj", None) + doc = inspect.getdoc(obj) if obj else None + if doc: + doc_prefix_parts = [] + for i in range(1, depth): + if is_last_at_level[i]: + doc_prefix_parts.append(" ") + else: + doc_prefix_parts.append("│ " if use_markup else "| ") + if depth > 0: + if is_last: + doc_prefix_parts.append(" ") + else: + doc_prefix_parts.append("│ " if use_markup else "| ") + doc_prefix = "".join(doc_prefix_parts) + for line in doc.splitlines(): + tw.line(f"{doc_prefix}{line}") + + # Render children + new_is_last_at_level = [*is_last_at_level, is_last] + for i, child in enumerate(tree_node.children): + child_is_last = i == len(tree_node.children) - 1 + render_node(child, depth + 1, new_is_last_at_level, child_is_last) + + for i, root in enumerate(self.roots): + is_last = i == len(self.roots) - 1 + render_node(root, 0, [], is_last) + + @final class TerminalReporter: def __init__(self, config: Config, file: TextIO | None = None) -> None: @@ -912,11 +1116,16 @@ def pytest_collection_finish(self, session: Session) -> None: ) self._write_report_lines_from_hooks(lines) - if self.config.getoption("collectonly"): + if self.config.getoption("collectonly") or self.config.getoption( + "collect_only_tree" + ): if session.items: if self.config.option.verbose > -1: self._tw.line("") - self._printcollecteditems(session.items) + if self.config.getoption("collect_only_tree"): + self._printcollecteditems_tree(session.items) + else: + self._printcollecteditems(session.items) failed = self.stats.get("failed") if failed: @@ -925,15 +1134,14 @@ def pytest_collection_finish(self, session: Session) -> None: rep.toterminal(self._tw) def _printcollecteditems(self, items: Sequence[Item]) -> None: + """Print collected items in classic format. + + Uses stack-based traversal to follow collection order, which naturally + handles --keep-duplicates by reprinting nodes when paths diverge. + """ test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) if test_cases_verbosity < 0: - if test_cases_verbosity < -1: - counts = Counter(item.nodeid.split("::", 1)[0] for item in items) - for name, count in sorted(counts.items()): - self._tw.line(f"{name}: {count}") - else: - for item in items: - self._tw.line(item.nodeid) + self._print_items_quiet(items, test_cases_verbosity) return stack: list[Node] = [] indent = "" @@ -954,6 +1162,25 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) + def _printcollecteditems_tree(self, items: Sequence[Item]) -> None: + """Print collected items as a tree with box-drawing characters.""" + test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) + if test_cases_verbosity < 0: + self._print_items_quiet(items, test_cases_verbosity) + return + tree = CollectionTree(items) + tree.render_tree(self._tw, test_cases_verbosity, self.hasmarkup) + + def _print_items_quiet(self, items: Sequence[Item], verbosity: int) -> None: + """Print items in quiet mode (nodeid or count format).""" + if verbosity < -1: + counts = Counter(item.nodeid.split("::", 1)[0] for item in items) + for name, count in sorted(counts.items()): + self._tw.line(f"{name}: {count}") + else: + for item in items: + self._tw.line(item.nodeid) + @hookimpl(wrapper=True) def pytest_sessionfinish( self, session: Session, exitstatus: int | ExitCode @@ -1418,7 +1645,9 @@ def build_summary_stats_line(self) -> tuple[list[tuple[str, dict[str, bool]]], s The final color of the line is also determined by this function, and is the second element of the returned tuple. """ - if self.config.getoption("collectonly"): + if self.config.getoption("collectonly") or self.config.getoption( + "collect_only_tree" + ): return self._build_collect_only_summary_stats_line() else: return self._build_normal_summary_stats_line() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 3053f5ef9a1..63151adf843 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -652,6 +652,121 @@ def test_bar(): pass ) +class TestCollectonlyTree: + """Tests for the --collect-only-tree (--co-tree) flag.""" + + def test_tree_basic(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_one(): + pass + def test_two(): + pass + """ + ) + result = pytester.runpytest("--co-tree") + result.stdout.fnmatch_lines( + [ + "*", + "`-- test_tree_basic.py", + " *-- test_one", + " `-- test_two", + ] + ) + + def test_tree_with_classes(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + class TestFoo: + def test_method_one(self): + pass + def test_method_two(self): + pass + + def test_standalone(): + pass + """ + ) + result = pytester.runpytest("--co-tree") + result.stdout.fnmatch_lines( + [ + "*", + "`-- test_tree_with_classes.py", + " *-- TestFoo", + " | *-- test_method_one", + " | `-- test_method_two", + " `-- test_standalone", + ] + ) + + def test_tree_nested_directories(self, pytester: Pytester) -> None: + pytester.makepyfile( + **{ + "a/test_a.py": "def test_a(): pass", + "a/b/test_b.py": "def test_b(): pass", + } + ) + result = pytester.runpytest("--co-tree") + result.stdout.fnmatch_lines( + [ + "*", + "`-- a", + " *-- b", + " | `-- test_b.py", + " | `-- test_b", + " `-- test_a.py", + " `-- test_a", + ] + ) + + def test_tree_multiple_files(self, pytester: Pytester) -> None: + pytester.makepyfile( + test_alpha="def test_alpha(): pass", + test_beta="def test_beta(): pass", + ) + result = pytester.runpytest("--co-tree") + result.stdout.fnmatch_lines( + [ + "*", + "*-- test_alpha.py", + "| `-- test_alpha", + "`-- test_beta.py", + " `-- test_beta", + ] + ) + + def test_tree_quiet_mode(self, pytester: Pytester) -> None: + """With -q, tree output falls back to nodeids.""" + pytester.makepyfile( + """ + def test_func(): + pass + """ + ) + result = pytester.runpytest("--co-tree", "-q") + result.stdout.fnmatch_lines(["test_tree_quiet_mode.py::test_func"]) + + def test_tree_no_tests_collected(self, pytester: Pytester) -> None: + result = pytester.runpytest("--co-tree") + result.stdout.fnmatch_lines("*== no tests collected in * ==*") + + def test_tree_verbose_shows_docstrings(self, pytester: Pytester) -> None: + pytester.makepyfile( + ''' + def test_documented(): + """This is a documented test.""" + pass + ''' + ) + result = pytester.runpytest("--co-tree", "-v") + result.stdout.fnmatch_lines( + [ + "*-- test_documented", + "*This is a documented test.*", + ] + ) + + class TestFixtureReporting: def test_setup_fixture_error(self, pytester: Pytester) -> None: pytester.makepyfile( From 6fbf7cf7549cf20ba76ccf11ce2eb59d14c3e456 Mon Sep 17 00:00:00 2001 From: Stuart Laughlin Date: Thu, 29 Jan 2026 14:43:55 -0600 Subject: [PATCH 2/4] changelog and AUTHORS --- AUTHORS | 1 + changelog/14150.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/14150.feature.rst diff --git a/AUTHORS b/AUTHORS index 09c5872fe07..ed11bce2763 100644 --- a/AUTHORS +++ b/AUTHORS @@ -439,6 +439,7 @@ Stefanie Molin Stefano Taschini Steffen Allner Stephan Obermann +Stuart Laughlin Sven Sven-Hendrik Haase Sviatoslav Sydorenko diff --git a/changelog/14150.feature.rst b/changelog/14150.feature.rst new file mode 100644 index 00000000000..b6eb28f9900 --- /dev/null +++ b/changelog/14150.feature.rst @@ -0,0 +1 @@ +Added ``--collect-only-tree`` (``--co-tree``) flag to display collected tests as a tree with box-drawing characters. From d8311ac900ce64646da57c20d4eed2ff1d9a5d0e Mon Sep 17 00:00:00 2001 From: Stuart Laughlin Date: Thu, 29 Jan 2026 14:55:31 -0600 Subject: [PATCH 3/4] adds documentation for --collect-only-tree --- doc/en/reference/reference.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 62ae3564e18..a33108a9e36 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -2778,6 +2778,13 @@ Collection Only collect tests, don't execute them. Shows which tests would be collected and run. +.. option:: --collect-only-tree, --co-tree + + Like :option:`--collect-only`, but displays collected tests as a tree + with box-drawing characters instead of the classic ```` format. + Uses colors when outputting to a TTY, and falls back to ASCII characters + for non-TTY output. + .. option:: --pyargs Try to interpret all arguments as Python packages. @@ -3360,6 +3367,8 @@ All the command-line flags can also be obtained by running ``pytest --help``:: collection: --collect-only, --co Only collect tests, don't execute them + --collect-only-tree, --co-tree + Like --collect-only, but display as a tree --pyargs Try to interpret all arguments as Python packages --ignore=path Ignore path during collection (multi-allowed) --ignore-glob=path Ignore path pattern during collection (multi- From 052102b2a8432d29ac6268d4e74e3efed621f293 Mon Sep 17 00:00:00 2001 From: Stuart Laughlin Date: Thu, 29 Jan 2026 15:14:21 -0600 Subject: [PATCH 4/4] Address review feedback for --collect-only-tree - Remove unused render_classic method - Fix Unicode detection: check file encoding instead of color support - Update docs to clarify structural vs execution order difference - Add tests for parametrized tests and --co/--co-tree precedence - Update test patterns to use Unicode box-drawing characters --- doc/en/reference/reference.rst | 7 +++- src/_pytest/terminal.py | 34 +++++---------- testing/test_terminal.py | 76 +++++++++++++++++++++++++--------- 3 files changed, 74 insertions(+), 43 deletions(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index a33108a9e36..286cb312665 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -2783,7 +2783,12 @@ Collection Like :option:`--collect-only`, but displays collected tests as a tree with box-drawing characters instead of the classic ```` format. Uses colors when outputting to a TTY, and falls back to ASCII characters - for non-TTY output. + for non-UTF-8 output. + + Note: This shows the *structural* organization of tests (grouped by + directory, module, and class), which may differ from the execution order + shown by :option:`--collect-only` if plugins reorder tests via the + :hook:`pytest_collection_modifyitems` hook. .. option:: --pyargs diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index ebb5a309827..be1803cef29 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -454,24 +454,6 @@ def _build(self, items: Sequence[Item]) -> None: parent_tree_node = tree_node - def render_classic(self, tw: TerminalWriter, verbosity: int = 0) -> None: - """Render the tree in classic format.""" - - def render_node(tree_node: CollectionTreeNode, depth: int) -> None: - indent = " " * depth - tw.line(f"{indent}{tree_node.node}") - if verbosity >= 1: - obj = getattr(tree_node.node, "obj", None) - doc = inspect.getdoc(obj) if obj else None - if doc: - for line in doc.splitlines(): - tw.line(f"{indent} {line}") - for child in tree_node.children: - render_node(child, depth + 1) - - for root in self.roots: - render_node(root, 0) - def render_tree( self, tw: TerminalWriter, verbosity: int = 0, use_markup: bool = True ) -> None: @@ -484,6 +466,12 @@ def render_tree( from _pytest.python import Module from _pytest.python import Package + # Check Unicode support separately from color support + use_unicode = True + if tw._file is not None: + encoding = getattr(tw._file, "encoding", None) or "" + use_unicode = "utf" in encoding.lower() + def get_node_markup(pytest_node: Node) -> dict[str, bool]: """Return markup kwargs for a node based on its type.""" if not use_markup: @@ -536,11 +524,11 @@ def render_node( if is_last_at_level[i]: prefix_parts.append(" ") else: - prefix_parts.append("│ " if use_markup else "| ") + prefix_parts.append("│ " if use_unicode else "| ") if is_last: - prefix_parts.append("└── " if use_markup else "`-- ") + prefix_parts.append("└── " if use_unicode else "`-- ") else: - prefix_parts.append("├── " if use_markup else "+-- ") + prefix_parts.append("├── " if use_unicode else "+-- ") prefix = "".join(prefix_parts) label = get_node_label(tree_node.node) @@ -558,12 +546,12 @@ def render_node( if is_last_at_level[i]: doc_prefix_parts.append(" ") else: - doc_prefix_parts.append("│ " if use_markup else "| ") + doc_prefix_parts.append("│ " if use_unicode else "| ") if depth > 0: if is_last: doc_prefix_parts.append(" ") else: - doc_prefix_parts.append("│ " if use_markup else "| ") + doc_prefix_parts.append("│ " if use_unicode else "| ") doc_prefix = "".join(doc_prefix_parts) for line in doc.splitlines(): tw.line(f"{doc_prefix}{line}") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 63151adf843..1b439f94db3 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -668,9 +668,9 @@ def test_two(): result.stdout.fnmatch_lines( [ "*", - "`-- test_tree_basic.py", - " *-- test_one", - " `-- test_two", + "└── test_tree_basic.py", + " ├── test_one", + " └── test_two", ] ) @@ -691,11 +691,11 @@ def test_standalone(): result.stdout.fnmatch_lines( [ "*", - "`-- test_tree_with_classes.py", - " *-- TestFoo", - " | *-- test_method_one", - " | `-- test_method_two", - " `-- test_standalone", + "└── test_tree_with_classes.py", + " ├── TestFoo", + " │ ├── test_method_one", + " │ └── test_method_two", + " └── test_standalone", ] ) @@ -710,12 +710,12 @@ def test_tree_nested_directories(self, pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "*", - "`-- a", - " *-- b", - " | `-- test_b.py", - " | `-- test_b", - " `-- test_a.py", - " `-- test_a", + "└── a", + " ├── b", + " │ └── test_b.py", + " │ └── test_b", + " └── test_a.py", + " └── test_a", ] ) @@ -728,10 +728,10 @@ def test_tree_multiple_files(self, pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "*", - "*-- test_alpha.py", - "| `-- test_alpha", - "`-- test_beta.py", - " `-- test_beta", + "├── test_alpha.py", + "│ └── test_alpha", + "└── test_beta.py", + " └── test_beta", ] ) @@ -761,11 +761,49 @@ def test_documented(): result = pytester.runpytest("--co-tree", "-v") result.stdout.fnmatch_lines( [ - "*-- test_documented", + "*── test_documented", "*This is a documented test.*", ] ) + def test_tree_parametrized(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("x", [1, 2, 3]) + def test_param(x): + pass + """ + ) + result = pytester.runpytest("--co-tree") + result.stdout.fnmatch_lines( + [ + "*── test_tree_parametrized.py", + " ├── test_param[[]1]", + " ├── test_param[[]2]", + " └── test_param[[]3]", + ] + ) + + def test_tree_with_both_collect_flags(self, pytester: Pytester) -> None: + """When both --co and --co-tree are specified, --co-tree takes precedence.""" + pytester.makepyfile( + """ + def test_one(): + pass + """ + ) + result = pytester.runpytest("--co", "--co-tree") + # Should show tree format, not classic format + result.stdout.fnmatch_lines( + [ + "└── test_tree_with_both_collect_flags.py", + ] + ) + # Should NOT show classic format + assert " None: