Skip to content

Commit 4e28517

Browse files
authored
Merge pull request #1 from agammann/fix/pytest-final-outcomes
Respect final pytest xfail outcomes
2 parents 3176427 + a1a3119 commit 4e28517

9 files changed

Lines changed: 87 additions & 23 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.4.0a2 — pytest outcome correction
4+
5+
- Record final pytest outcomes after expected-failure processing; mixed suites containing xfail or xpass remain inconclusive.
6+
- Preserve ordinary assertion failures, runtime errors, and existing unittest behavior.
7+
- Align the package, CLI, and report version.
8+
39
## 0.4.0a1 — public alpha
410

511
- Compare selected unittest or pytest cases on a Git baseline and a committed or working-tree fix.

‎README.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ It runs the same selected Python tests against a Git baseline and your current
99
code, including uncommitted edits and new, non-ignored files. It records individual
1010
test outcomes, source hashes, snapshots, and raw logs.
1111

12-
Version 0.4.0a1 is a public alpha for Python projects with a known pre-fix Git
12+
Version 0.4.0a2 is a public alpha for Python projects with a known pre-fix Git
1313
commit. It has no runtime package dependencies, account, or model API. It has
1414
been tested locally on Windows with Python 3.12; see [VERIFICATION.md](VERIFICATION.md)
1515
for the exact results and current limits.
@@ -19,12 +19,12 @@ for the exact results and current limits.
1919
Requires Python 3.11+ and Git. Install the alpha from its tagged source:
2020

2121
```sh
22-
python -m pip install "git+https://github.com/agammann/bugbix.git@v0.4.0-alpha.1"
22+
python -m pip install "git+https://github.com/agammann/bugbix.git@v0.4.0-alpha.2"
2323
bugbix --version
2424
```
2525

26-
Alternatively, download the wheel from the [alpha release](https://github.com/agammann/bugbix/releases/tag/v0.4.0-alpha.1)
27-
and install that file with `python -m pip install /path/to/bugbix-0.4.0a1-py3-none-any.whl`.
26+
Alternatively, download the wheel from the [alpha release](https://github.com/agammann/bugbix/releases/tag/v0.4.0-alpha.2)
27+
and install that file with `python -m pip install /path/to/bugbix-0.4.0a2-py3-none-any.whl`.
2828
Bugbix is not published to PyPI.
2929

3030
## Quick start
@@ -91,9 +91,10 @@ Exit `0` means `regression_observed`; exit `1` means another completed verdict;
9191
exit `2` means configuration, snapshot, or orchestration failure. CLI output is JSON.
9292
For `inconclusive`, the CLI and report name the blocking run, test, and exception
9393
type when available. Runner failures include a short cause; the full trace remains
94-
in the evidence folder. An assertion failure in one test does not override an error
95-
or skip in another selected test. Select the relevant test and rerun, or repair
96-
the test error. The report lists Git-visible changed files and links both stdout
94+
in the evidence folder. An assertion failure in one test does not override an error,
95+
skip, expected failure (`xfail`), or unexpected pass (`xpass`) in another selected
96+
test. Remove an obsolete expected-failure marker when checking a fixed test. Select the relevant test and rerun, or
97+
repair the test error. The report lists Git-visible changed files and links both stdout
9798
and stderr. A failure-to-pass transition cannot be reported as a regression when
9899
the only Git-visible changes are test files. Test-file recognition uses common
99100
Python names and `test/` or `tests/` folders; review the changed-file list.

‎VERIFICATION.md‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,29 @@ known pre-fix Git commit from a fix. This record separates local observations
55
from broader product claims. The project does not yet have independent user
66
adoption evidence.
77

8-
## Local product checks
8+
## 0.4.0a2 checks — 2026-10-02
9+
10+
Windows 11, CPython 3.12.14, pytest 9.1.1:
11+
12+
- All 22 product tests passed without skips, including mixed ordinary/expected
13+
failures and strict/non-strict unexpected passes.
14+
- Before the fix, a mixed pytest suite incorrectly returned `regression_observed`
15+
and recorded an xfail-to-xpass case as an ordinary failure-to-pass transition.
16+
The corrected runner reads pytest's final report and returns `inconclusive`.
17+
- A wheel was installed in a separate virtual environment and run outside the
18+
source checkout. Its six Python module files matched the source. The package,
19+
CLI, and report version agreed on `0.4.0a2`.
20+
- The installed CLI passed five disposable example checks: a real assertion
21+
transition, a weak test, an import error, a skipped test, and a timeout.
22+
- The installed CLI checked Bugbix's new mixed-xfail regression test against
23+
baseline `3176427c3744009772c5be2bc1a276b8464c3fa7`: two assertion failures
24+
before the fix, two passes after it, yielding `regression_observed`.
25+
26+
These are local controlled checks. The earlier third-party repository trials
27+
below were not rerun for this correction. Arbitrary pytest plugins and detached
28+
background processes remain outside the verified scope.
29+
30+
## Earlier alpha checks — 2026-09-27
931

1032
Test host: Windows 11 build 26200, CPython 3.12.14, 2026-09-27. The source
1133
suite exercises positive and negative verdicts, pytest and unittest, skipped
@@ -15,7 +37,7 @@ in a separate virtual environment and invoked outside the source tree.
1537

1638
| Check | Observed result |
1739
| --- | --- |
18-
| [Source suite on the alpha source](evidence/alpha-source-tests.txt) | 20 tests passed |
40+
| [Source suite on the first alpha source](evidence/alpha-source-tests.txt) | 20 tests passed |
1941
| Installed wheel smoke check | `bugbix --version` returned `Bugbix 0.4.0a1` |
2042
| Package consistency | Wheel Python source bytes matched the files in this repository |
2143

@@ -55,7 +77,7 @@ python -m pip wheel --no-deps --wheel-dir dist .
5577
```
5678

5779
The GitHub Actions workflow runs the suite on Windows and Ubuntu with Python
58-
3.12. A passing workflow is separate evidence from the local results above;
80+
3.11 and 3.12. A passing workflow is separate evidence from the local results above;
5981
check the latest run before assuming either platform passed remotely.
6082

6183
## Boundaries

‎bugbix/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Bugbix: regression evidence for the change in front of you."""
22

3-
__version__ = "0.1.0"
3+
__version__ = "0.4.0a2"

‎bugbix/cli.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import subprocess
44
import sys
55

6+
from . import __version__
67
from .core import check
78

89

910
def main(argv=None):
1011
parser = argparse.ArgumentParser(description="Does your regression test catch the old bug?")
11-
parser.add_argument("--version", action="version", version="Bugbix 0.4.0a1")
12+
parser.add_argument("--version", action="version", version=f"Bugbix {__version__}")
1213
commands = parser.add_subparsers(dest="command", required=True)
1314
run = commands.add_parser("check", help="Compare selected tests on old and current code")
1415
run.add_argument("--repo", default=".")

‎bugbix/core.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import sys
1414
import time
1515

16+
from . import __version__
17+
1618
MAX_FILE = 10 * 1024 * 1024
1719
MAX_TOTAL = 100 * 1024 * 1024
1820

@@ -310,7 +312,7 @@ def check(repo, base, head, tests, output, python=sys.executable, timeout=30.0,
310312
blockers = [{"run": "all", "issue": "no_non_test_changes"}]
311313
else:
312314
blockers = blockers_for(runs) if verdict == "inconclusive" else []
313-
report = {"schema_version": 2, "product": "Bugbix", "version": "0.4.0a1",
315+
report = {"schema_version": 2, "product": "Bugbix", "version": __version__,
314316
"verdict": verdict, "reason": reason, "regression_tests": transitions,
315317
"blockers": blockers, "changed_files": changed_files,
316318
"repo": str(repo), "base": base_oid, "head": head_oid, "tests": tests,

‎bugbix/pytest_runner.py‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@ def pytest_collectreport(self, report):
2828
if report.failed:
2929
self.collection_errors.append(report.nodeid)
3030

31-
def pytest_runtest_makereport(self, item, call):
32-
# This hook sees the original exception, allowing an assertion failure
33-
# to be distinguished from a setup/runtime error.
34-
import pytest
35-
36-
report = pytest.TestReport.from_item_and_call(item, call)
31+
def record_report(self, report, call):
32+
# Inspect the final report after pytest has applied xfail and other hooks,
33+
# while retaining the original exception for assertion/error distinction.
3734
if report.failed:
3835
if hasattr(report, "wasxfail"):
3936
status = "unexpected_success"
@@ -46,8 +43,8 @@ def pytest_runtest_makereport(self, item, call):
4643
elif report.skipped:
4744
self.record(report.nodeid, "expected_failure" if hasattr(report, "wasxfail") else "skip")
4845
elif call.when == "call":
49-
self.record(report.nodeid, "pass")
50-
return report
46+
self.record(report.nodeid,
47+
"unexpected_success" if hasattr(report, "wasxfail") else "pass")
5148

5249

5350
def main():
@@ -58,7 +55,13 @@ def main():
5855
try:
5956
import pytest
6057

61-
recorder = Recorder()
58+
class Plugin(Recorder):
59+
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
60+
def pytest_runtest_makereport(self, item, call):
61+
outcome = yield
62+
self.record_report(outcome.get_result(), call)
63+
64+
recorder = Plugin()
6265
exit_code = int(pytest.main(["-q", "-p", "no:cacheprovider", *paths], plugins=[recorder]))
6366
payload = {"events": [{"id": nodeid, "status": status,
6467
**({"error_type": recorder.error_types[nodeid]}

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "bugbix"
7-
version = "0.4.0a1"
7+
version = "0.4.0a2"
88
description = "Check whether a regression test catches the bug before your fix."
99
requires-python = ">=3.11"
1010
readme = "README.md"

‎tests/test_product.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,35 @@ def test_pytest_function_catches_dirty_tree_fix(self):
177177
self.root / "focused", repeat=1, runner="pytest")
178178
self.assertEqual(focused["verdict"], "regression_observed")
179179

180+
def test_pytest_xfail_outcomes_block_mixed_regression(self):
181+
try:
182+
import pytest # noqa: F401
183+
except ImportError:
184+
self.skipTest("pytest is not installed in this test environment")
185+
self.change_test("import pytest\nfrom pricing import total\n"
186+
"def test_discount():\n assert total(100, 20, 10) == 88\n"
187+
"@pytest.mark.xfail(reason='pending', strict=False)\n"
188+
"def test_marked():\n assert total(100, 20, 10) == 88\n")
189+
report = self.run_check(runner="pytest")
190+
self.assertEqual(report["verdict"], "inconclusive")
191+
outcomes = {(item["run"], item.get("status")) for item in report["blockers"]}
192+
self.assertIn(("base-1", "expected_failure"), outcomes)
193+
self.assertIn(("head-1", "unexpected_success"), outcomes)
194+
195+
def test_pytest_xpass_does_not_count_as_an_ordinary_pass(self):
196+
try:
197+
import pytest # noqa: F401
198+
except ImportError:
199+
self.skipTest("pytest is not installed in this test environment")
200+
for strict in (False, True):
201+
with self.subTest(strict=strict):
202+
self.change_test("import pytest\n"
203+
f"@pytest.mark.xfail(reason='pending', strict={strict})\n"
204+
"def test_marked():\n assert True\n")
205+
report = check(self.repo, "HEAD", "working", ["test_pricing.py"],
206+
self.root / f"xpass-{strict}", repeat=1, runner="pytest")
207+
self.assertEqual(report["verdict"], "inconclusive")
208+
180209
def test_pytest_runtime_error_is_inconclusive(self):
181210
try:
182211
import pytest # noqa: F401

0 commit comments

Comments
 (0)