Skip to content

Commit 6d35a8f

Browse files
committed
Merge pull request #328 from TomCruiseTorpedo/fix/collection-skip-fails-strict-run
# Conflicts: # CHANGELOG.md
2 parents 3d99110 + c3a6dcf commit 6d35a8f

4 files changed

Lines changed: 114 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ All notable changes to Open Ontologies are documented here.
1212
- Python: keep anonymous blank-node identities stable when diffing identical RDF text in `OntologyEngine.diff` and `kgcl_diff`; identical inputs remain parsed and validated.
1313
- Python: serialize RDF triple-term values in SELECT, CONSTRUCT and DESCRIBE query results instead of raising `AttributeError`; ordinary scalar values remain unchanged.
1414
- Python: accept reported blank-node graph names when selecting asserted graphs for Horn reasoning; preserve ordinary graph-name forms and inferred-graph refusal.
15+
- **A module that skipped at collection did not fail the strict Python leg.**
16+
The entry that added `python/tests/conftest.py` says collection-time skips
17+
"are caught too, through `pytest_collectreport`". They were seen and not
18+
failed: the hook rewrote `report.outcome`, and that never reached the exit
19+
status. MEASURED on pytest 9.0.3 and 9.1.1, under `OO_REQUIRE_FIXTURES=1`,
20+
with one passing test and one module that skips at collection:
21+
`1 passed, 1 skipped`, exit 0. Four files in the strict leg skip that way —
22+
`test_shacl.py`, `test_shacl_vacuous.py`, `test_align.py` and
23+
`test_vocab_check.py` — so the leg stayed green if their extras went missing.
24+
The hook now records the skip, and `pytest_sessionfinish` fails the run.
25+
`python/tests/test_conftest_collection_skip.py` holds it, with a control run
26+
without the variable.
1527

1628
- **`owl-dl` classifies Pizza exactly as HermiT does, and says when it is not
1729
complete.** On the canonical `pizza.owl` it returned no subsumptions and 143

‎docs/ci-gates.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,10 @@ property is that CI cannot skip in silence, not that a laptop cannot skip at all
207207
The Python half is still by hand:
208208

209209
```bash
210-
# every Python file that can skip: 10 hits, of which conftest.py is the
211-
# mechanism and not a test, so 9, and the table has 9 rows
210+
# every Python file that can skip: 11 hits. conftest.py is the mechanism and
211+
# not a test, and test_conftest_collection_skip.py tests it: it writes a
212+
# skipping module into a temp directory and cannot skip itself. So 9, and the
213+
# table has 9 rows
212214
grep -rln 'pytest.skip\|importorskip\|skipif' python/tests/*.py
213215
# every leg CI makes strict
214216
grep -rn 'OO_REQUIRE_FIXTURES=1' .github/workflows/

‎python/tests/conftest.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,45 @@ def pytest_runtest_makereport(item, call):
8484
return report
8585

8686

87+
_COLLECTION_SKIPS = []
88+
89+
8790
def pytest_collectreport(report):
8891
"""A module-level `importorskip` skips at COLLECTION, not at run time.
8992
9093
`pytest_runtest_makereport` never sees those, so three of this suite's files
9194
— the two pyshacl ones and the hnswlib one — would have stayed exempt from a
9295
rule written only there.
96+
97+
This hook can SEE such a skip and cannot FAIL it. Until this was changed it
98+
set `report.outcome = "failed"` here, through `_fail`, and that never
99+
reached the exit status. MEASURED on pytest 9.0.3 and 9.1.1, with one
100+
passing test and one module that skips at collection, under
101+
OO_REQUIRE_FIXTURES=1: `1 passed, 1 skipped`, exit 0. Marked `tryfirst`, the
102+
summary said `1 error` and the exit was still 0. Run with `-rs` or `-ra`,
103+
the terminal reporter crashed on the rewritten report. So the skip is
104+
recorded here, and `pytest_sessionfinish` fails the run.
105+
`test_conftest_collection_skip.py` holds this.
93106
"""
94107
if report.skipped:
95-
_fail(report, report.nodeid, _reason(report))
108+
reason = _reason(report)
109+
print(f"{MARKER}: {report.nodeid}: {reason}", flush=True)
110+
_COLLECTION_SKIPS.append(f"{report.nodeid}: {reason}")
111+
112+
113+
@pytest.hookimpl(trylast=True)
114+
def pytest_sessionfinish(session, exitstatus):
115+
"""Where a skip at collection becomes a failure. See `pytest_collectreport`."""
116+
if not (STRICT and _COLLECTION_SKIPS):
117+
return
118+
print(
119+
f"\n{MARKER}: OO_REQUIRE_FIXTURES=1, so this is a failure, not a skip. "
120+
f"{len(_COLLECTION_SKIPS)} module(s) skipped at collection and none of "
121+
f"their tests ran:",
122+
flush=True,
123+
)
124+
for line in _COLLECTION_SKIPS:
125+
print(f" {line}", flush=True)
126+
# A status that already says something went wrong is left as it is.
127+
if session.exitstatus == pytest.ExitCode.OK:
128+
session.exitstatus = pytest.ExitCode.TESTS_FAILED
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""`OO_REQUIRE_FIXTURES=1` fails a run in which a module skipped at COLLECTION.
2+
3+
A module-level skip happens before any test item exists, so only
4+
`pytest_collectreport` sees it. `conftest.py` used to rewrite the report there,
5+
and that did not reach the exit status: one passing test plus one such module
6+
gave `1 passed, 1 skipped` and exit 0 under the variable. Four files in the
7+
strict leg of the CI `python` job skip this way (`test_shacl.py`,
8+
`test_shacl_vacuous.py`, `test_align.py`, `test_vocab_check.py`), so that leg
9+
stayed green if their extras went missing.
10+
11+
Each test runs pytest in a subprocess, on a throwaway directory that holds a
12+
COPY of this suite's own `conftest.py`. The hook under test is the shipped one,
13+
not a restatement of it.
14+
"""
15+
16+
import os
17+
import shutil
18+
import subprocess
19+
import sys
20+
from pathlib import Path
21+
22+
CONFTEST = Path(__file__).with_name("conftest.py")
23+
24+
PASSES = "def test_passes():\n assert True\n"
25+
26+
SKIPS_AT_COLLECTION = (
27+
"import pytest\n"
28+
"pytest.importorskip('a_module_that_is_installed_nowhere')\n"
29+
"\n"
30+
"def test_never_runs():\n"
31+
" assert True\n"
32+
)
33+
34+
35+
def _run(tmp_path, strict):
36+
shutil.copy(CONFTEST, tmp_path / "conftest.py")
37+
(tmp_path / "test_passes.py").write_text(PASSES)
38+
(tmp_path / "test_skips_at_collection.py").write_text(SKIPS_AT_COLLECTION)
39+
env = dict(os.environ)
40+
env.pop("OO_REQUIRE_FIXTURES", None)
41+
env.pop("PYTEST_ADDOPTS", None)
42+
if strict:
43+
env["OO_REQUIRE_FIXTURES"] = "1"
44+
return subprocess.run(
45+
[sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider",
46+
"--rootdir", str(tmp_path), str(tmp_path)],
47+
cwd=tmp_path,
48+
env=env,
49+
capture_output=True,
50+
text=True,
51+
)
52+
53+
54+
def test_a_skip_at_collection_fails_the_strict_run(tmp_path):
55+
r = _run(tmp_path, strict=True)
56+
assert r.returncode == 1, r.stdout + r.stderr
57+
assert "test_skips_at_collection.py" in r.stdout, r.stdout
58+
59+
60+
def test_the_same_skip_passes_without_the_variable(tmp_path):
61+
"""The control. A conftest that failed EVERY run would pass the test above."""
62+
r = _run(tmp_path, strict=False)
63+
assert r.returncode == 0, r.stdout + r.stderr
64+
assert "1 passed, 1 skipped" in r.stdout, r.stdout

0 commit comments

Comments
 (0)