diff --git a/src/buildkite_test_collector/pytest_plugin/__init__.py b/src/buildkite_test_collector/pytest_plugin/__init__.py index 53a10a0..65131a0 100644 --- a/src/buildkite_test_collector/pytest_plugin/__init__.py +++ b/src/buildkite_test_collector/pytest_plugin/__init__.py @@ -41,9 +41,13 @@ def pytest_unconfigure(config): if plugin: api = API(os.environ) - numprocesses = config.getoption("numprocesses", None) + xdist_plugin = config.pluginmanager.getplugin("xdist") + if xdist_plugin is not None: + numprocesses = config.getoption("numprocesses") + else: + numprocesses = None xdist_enabled = ( - config.pluginmanager.getplugin("xdist") is not None + xdist_plugin is not None and numprocesses is not None and numprocesses > 0 ) diff --git a/src/buildkite_test_collector/pytest_plugin/buildkite_plugin.py b/src/buildkite_test_collector/pytest_plugin/buildkite_plugin.py index 52d528c..4e39612 100644 --- a/src/buildkite_test_collector/pytest_plugin/buildkite_plugin.py +++ b/src/buildkite_test_collector/pytest_plugin/buildkite_plugin.py @@ -55,10 +55,12 @@ def pytest_runtest_logreport(self, report): # This hook is called three times during the lifecycle of a test: # after the setup phase, the call phase, and the teardown phase. - # We capture outcomes from the call phase, or setup phase if it failed - # (since setup failures prevent the call phase from running). + # We capture outcomes from the call phase, or setup/teardown phase if it failed + # (since setup failures prevent the call phase from running, and teardown + # failures should mark an otherwise passing test as failed). # See: https://github.com/buildkite/test-collector-python/pull/45 - if report.when == 'call' or (report.when == 'setup' and report.failed): + # See: https://github.com/buildkite/test-collector-python/issues/84 + if report.when == 'call' or (report.when in ('setup', 'teardown') and report.failed): self.update_test_result(report) # This hook only runs in xdist worker thread, not controller thread. diff --git a/tests/buildkite_test_collector/pytest_plugin/test_plugin.py b/tests/buildkite_test_collector/pytest_plugin/test_plugin.py index 07b5456..fd3f249 100644 --- a/tests/buildkite_test_collector/pytest_plugin/test_plugin.py +++ b/tests/buildkite_test_collector/pytest_plugin/test_plugin.py @@ -145,6 +145,39 @@ def test_pytest_runtest_logreport_fail_exception_in_setup(fake_env): assert len(fe["backtrace"]) > 0 +def test_pytest_runtest_logreport_fail_exception_in_teardown(fake_env): + """Teardown failure should override a passed call result""" + payload = Payload.init(fake_env) + plugin = BuildkitePlugin(payload) + + location = ("", None, "") + + # First, simulate a passing call phase + call_report = TestReport(nodeid="", location=location, keywords={}, outcome="passed", longrepr=None, when="call") + plugin.pytest_runtest_logstart(call_report.nodeid, location) + plugin.pytest_runtest_logreport(call_report) + + # Verify it's marked as passed after call + test_data = plugin.in_flight.get(call_report.nodeid) + assert isinstance(test_data.result, TestResultPassed) + + # Now simulate a failing teardown phase + try: + raise Exception("a fake teardown exception") + except Exception as e: + longrepr = ExceptionInfo.from_exception(e) + teardown_report = TestReport(nodeid="", location=location, keywords={}, outcome="failed", longrepr=longrepr, when="teardown") + + plugin.pytest_runtest_logreport(teardown_report) + test_data = plugin.in_flight.get(teardown_report.nodeid) + plugin.pytest_runtest_logfinish(teardown_report.nodeid, location) + + # Verify teardown failure overrides the passed result + assert isinstance(test_data, TestData) + assert isinstance(test_data.result, TestResultFailed) + assert test_data.result.failure_reason == "Exception: a fake teardown exception" + + def test_pytest_runtest_logreport_simple_skip(fake_env): payload = Payload.init(fake_env) plugin = BuildkitePlugin(payload)