Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

- name: Run tox
# Run tox using the version of Python in `PATH`
run: tox -e py
run: tox run-parallel -e py,subtests

coverage:
needs: build
Expand All @@ -47,7 +47,7 @@ jobs:
run: pip install tox

- name: Collect coverage data
run: tox -e coverage
run: tox run-parallel -e coverage,subtests-coverage

- name: Combine coverage & fail if it's <100%
run: |
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tap = "pytest_tap.plugin"
[dependency-groups]
dev = [
"pytest-tap",
"pytest-subtests",
"tox>=4.24.1",
]

Expand All @@ -55,6 +56,7 @@ packages = ["pytest_tap"]

[tool.pytest.ini_options]
pythonpath = [".", "src"]
markers = ["subtests"]

[tool.uv.sources]
pytest-tap = { workspace = true }
Expand Down
15 changes: 13 additions & 2 deletions src/pytest_tap/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ def __init__(self, config: pytest.Config) -> None:
def pytest_runtestloop(self, session):
"""Output the plan line first."""
option = session.config.option
if option.tap_stream or option.tap_combined:
if (option.tap_stream or option.tap_combined) and not (
session.config.pluginmanager.has_plugin("subtests")
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this logic. Can you help me understand why we don't want to write the plan when the subtests plugin is available?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest does not know about subtests at collection time. So if I have 1 test with 3 subtests, the test plan will show "1..1". But then the TAP file will contain 4 lines.

self._tracker.set_plan(session.testscollected)

@pytest.hookimpl(optionalhook=True)
def pytest_xdist_node_collection_finished(self, node, ids):
"""Output the plan line first when using xdist."""
if self._tracker.streaming or self._tracker.combined:
if (self._tracker.streaming or self._tracker.combined) and not (
node.config.pluginmanager.has_plugin("subtests")
):
self._tracker.set_plan(len(ids))

@pytest.hookimpl()
Expand All @@ -57,6 +61,10 @@ def pytest_runtest_logreport(self, report: pytest.TestReport):
return

description = str(report.location[0]) + "::" + str(report.location[2])
if hasattr(report, "sub_test_description"):
# Handle pytest-subtests plugin
description += report.sub_test_description()

testcase = report.location[0]

# Handle xfails first because they report in unusual ways.
Expand Down Expand Up @@ -116,6 +124,9 @@ def pytest_runtest_logreport(self, report: pytest.TestReport):
@pytest.hookimpl()
def pytest_unconfigure(self, config: pytest.Config):
"""Dump the results."""
if self._tracker.combined and config.pluginmanager.has_plugin("subtests"):
# Force Tracker to write plan at beginning of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we didn't write the test plan earlier, tap-py will use the number of lines to determine the test plan (combined_line_number). From my example above, that will correctly be "1..4". However, tap-py writes the test plan at the end of the file. Not sure why it was implemented that way, though it's still valid TAP formatting.

To try to be as consistent as possible when subtests are vs are not used, I wanted the resulting TAP file to look similar. So by calling "set_plan" before generating the report, it ensures that "1..4" appears at the top of the file instead.

self._tracker.set_plan(self._tracker.combined_line_number)
self._tracker.generate_tap_reports()


Expand Down
61 changes: 61 additions & 0 deletions tests/test_subtests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import pytest
from tap.tracker import ENABLE_VERSION_13


@pytest.mark.subtests
def test_log_subtests_stream(testdir):
"""Subtests are added individually to stream."""
testdir.makepyfile(
"""
import pytest

def test_subtests(subtests):
for i in range(2):
with subtests.test(msg="sub_msg", i=i):
assert i % 2 == 0
"""
)
result = testdir.runpytest_subprocess("--tap")

result.stdout.fnmatch_lines(
[
"ok 1 test_log_subtests_stream.py::test_subtests[sub_msg] (i=0)",
"not ok 2 test_log_subtests_stream.py::test_subtests[sub_msg] (i=1)",
"ok 3 test_log_subtests_stream.py::test_subtests",
"1..3",
]
)


@pytest.mark.subtests
def test_log_subtests_combined(testdir):
"""Subtests are added individually to combined."""
testdir.makepyfile(
"""
import pytest

def test_subtests(subtests):
for i in range(2):
with subtests.test(msg="sub_msg", i=i):
assert i % 2 == 0
"""
)
testdir.runpytest_subprocess("--tap-outdir", "subtest-results", "--tap-combined")
outdir = testdir.tmpdir.join("subtest-results")
testresults = outdir.join("testresults.tap")
assert testresults.check()
actual_results = [
line.strip() for line in testresults.readlines() if not line.startswith("#")
]

expected_results = [
"1..3",
"ok 1 test_log_subtests_combined.py::test_subtests[sub_msg] (i=0)",
"not ok 2 test_log_subtests_combined.py::test_subtests[sub_msg] (i=1)",
"ok 3 test_log_subtests_combined.py::test_subtests",
]

# If the dependencies for version 13 happen to be installed, tweak the output.
if ENABLE_VERSION_13:
expected_results.insert(0, "TAP version 13")
assert actual_results == expected_results
1 change: 1 addition & 0 deletions tests/test_xdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_sets_plan_streaming(combined, stream, expected):
config.option.tap_stream = stream
plugin = TAPPlugin(config)
node = mock.Mock()
node.config.pluginmanager.has_plugin = lambda x: x != "subtests"
test_ids = ["a", "b", "c"]

plugin.pytest_xdist_node_collection_finished(node, test_ids)
Expand Down
19 changes: 17 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
[tox]
envlist =
subtests
coverage

[testenv]
deps =
pytest
commands = pytest --tap-combined {posargs}
commands = pytest -m 'not subtests' --tap-combined {posargs}

[testenv:subtests]
deps =
pytest
pytest-subtests
commands = pytest -m 'subtests' --tap-combined {posargs}

[testenv:coverage]
deps =
pytest
pytest-cov
commands =
pytest --cov=pytest_tap --cov-report xml --cov-report term
pytest -m 'not subtests' --cov=pytest_tap --cov-append --cov-report xml --cov-report term

[testenv:subtests-coverage]
deps =
pytest
pytest-subtests
pytest-cov
commands =
pytest -m 'subtests' --cov=pytest_tap --cov-append --cov-report xml --cov-report term
24 changes: 24 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.