Skip to content

Commit 3ab88d0

Browse files
committed
fix(reqstool): address full-pr-review findings on #93
- Split the single e2e test into test_hatch_build_generates_annotations_yml (@svcs SVC_HATCH_PLUGIN_001) and test_hatch_build_sdist_bundles_reqstool_dataset (@svcs SVC_HATCH_PLUGIN_002), so a regression in one hook method no longer conflates both SVCs' pass/fail status in the traceability dataset - Extended the sdist-bundling test to cover the previously-untested manual_verification_results.yml and test_results branches of _append_to_sdist_tar_gz: added a real manual_verification_results.yml fixture, carved out a .gitignore exception so the fixture's build/test-results/junit.xml can be committed, and asserted reqstool_config.yml records the configured test_results glob (the hook only records the glob pattern as metadata, it doesn't bundle the actual XML into the tarball -- confirmed by re-reading _append_to_sdist_tar_gz) - Fixed a misleading build.yml comment implying reqstool-status only runs on the pypi leg; it runs unconditionally on both - Added a one-line note flagging the 4 SHA-pinned reqstool/.github refs that need to stay in sync when bumped Re-validated: 2 passed/1 skipped (pre-existing skip, unrelated), reqstool validate --strict and reqstool status --check-all-reqs-met both pass (2/2 complete, now mapped to 2 independent tests instead of 1 shared one), openspec validate --specs --strict 2/2, black clean. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
1 parent 143b9f0 commit 3ab88d0

5 files changed

Lines changed: 83 additions & 42 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@ jobs:
4545
run: hatch build
4646
- name: Self-apply own decorators to own src/tests
4747
run: hatch run dev:python scripts/generate_annotations.py
48+
# NOTE: all reqstool/.github refs below are pinned to the same commit -- keep
49+
# these 4 in sync when bumping (install-reqstool, validate-reqstool,
50+
# reqstool-status, and the validate-openspec job's workflow ref further down).
4851
- name: Install reqstool
4952
uses: reqstool/.github/.github/actions/install-reqstool@74cc3ac55a476258898db82c69a78eeaabb2fcbc # main 2026-06-24
5053
with:
5154
reqstool-source: ${{ matrix.reqstool-source }}
5255
- name: Validate reqstool spec completeness
5356
# not yet available in the latest PyPI release; supplementary to (not a
5457
# replacement for) the `reqstool status --fail-if-incomplete` gate below, which
55-
# is the actual required check on the pypi leg
58+
# is the actual required check, run unconditionally on both matrix legs
5659
if: matrix.reqstool-source == 'main'
5760
uses: reqstool/.github/.github/actions/validate-reqstool@74cc3ac55a476258898db82c69a78eeaabb2fcbc # main 2026-06-24
5861
- name: Run reqstool status

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ __pycache__/
101101
# Distribution / packaging
102102
.Python
103103
build/
104+
!tests/fixtures/test_project/build/
104105
develop-eggs/
105106
dist/
106107
downloads/

‎tests/e2e/reqstool_python_hatch_plugin/test_build_e2e.py‎

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,64 +13,93 @@
1313
DIST_DIR = Path(__file__).parents[3] / "dist"
1414

1515
# The hatch plugin appends reqstool_config.yml to the tar.gz and generates
16-
# annotations.yml on disk. requirements.yml and software_verification_cases.yml
17-
# are included via the sdist include config (docs/reqstool/**).
16+
# annotations.yml on disk. requirements.yml, software_verification_cases.yml, and
17+
# manual_verification_results.yml are all included via the sdist include config
18+
# (docs/reqstool/**). The hook does NOT bundle the actual test-result XML files into
19+
# the tarball -- it only records the configured glob pattern as a reqstool_config.yml
20+
# resource (test results are transient CI artifacts, not shipped in the package).
1821
EXPECTED_IN_TARBALL = [
1922
"reqstool_config.yml",
2023
"requirements.yml",
2124
"software_verification_cases.yml",
25+
"manual_verification_results.yml",
2226
]
2327

2428

25-
@pytest.mark.e2e
26-
@SVCs("SVC_HATCH_PLUGIN_001", "SVC_HATCH_PLUGIN_002")
27-
def test_hatch_build_sdist_contains_reqstool_artifacts():
28-
"""hatch build (sdist) triggers the reqstool hook and bundles all artifacts.
29+
def _build_fixture_sdist(tmpdir: str) -> tuple[Path, Path]:
30+
"""Builds the fixture project's sdist via an isolated venv with the local plugin
31+
wheel pre-installed (bypassing hatch's own build-env management, which can't
32+
resolve `@ file://` hook dependencies reliably across pip/uv versions).
2933
30-
Runs hatchling directly inside an isolated venv that has the local plugin wheel
31-
pre-installed, bypassing hatch's own build-env management (which can't resolve
32-
@ file:// hook dependencies reliably across pip/uv versions).
34+
Returns (tarball_path, project_dir).
3335
"""
36+
tmp_project = Path(tmpdir) / "test_project"
37+
# Keep build/test-results/junit.xml (the committed test_results fixture the hook
38+
# bundles into the sdist) but drop dist/__pycache__. ignore_patterns matches by
39+
# basename anywhere in the tree, so "build/reqstool" can't be excluded this way
40+
# without also matching docs/reqstool -- removed separately below instead.
41+
shutil.copytree(FIXTURE_DIR, tmp_project, ignore=shutil.ignore_patterns("dist", "__pycache__"))
42+
shutil.rmtree(tmp_project / "build" / "reqstool", ignore_errors=True)
43+
44+
venv_dir = Path(tmpdir) / "build-venv"
45+
venv.create(str(venv_dir), with_pip=True)
46+
python = str(venv_dir / "bin" / "python")
47+
3448
wheels = sorted(DIST_DIR.glob("reqstool_python_hatch_plugin-*.whl"))
3549
if not wheels:
3650
pytest.skip("No local wheel found — run `hatch build --target wheel` first")
3751

52+
subprocess.run(
53+
[python, "-m", "pip", "install", "--quiet", "hatchling", str(wheels[-1])],
54+
check=True,
55+
)
56+
57+
result = subprocess.run(
58+
[python, "-m", "hatchling", "build", "--target", "sdist"],
59+
cwd=tmp_project,
60+
capture_output=True,
61+
text=True,
62+
)
63+
assert result.returncode == 0, f"hatchling build failed:\n{result.stderr}"
64+
65+
tarballs = sorted((tmp_project / "dist").glob("mypackage-*.tar.gz"))
66+
assert tarballs, "No tarball found in dist/"
67+
68+
return tarballs[-1], tmp_project
69+
70+
71+
@pytest.mark.e2e
72+
@SVCs("SVC_HATCH_PLUGIN_001")
73+
def test_hatch_build_generates_annotations_yml():
74+
"""hatch build's `initialize` hook generates annotations.yml from decorated source,
75+
independently of whether the sdist-bundling (`finalize`) hook also succeeds."""
3876
with tempfile.TemporaryDirectory() as tmpdir:
39-
tmp_project = Path(tmpdir) / "test_project"
40-
shutil.copytree(FIXTURE_DIR, tmp_project, ignore=shutil.ignore_patterns("dist", "build", "__pycache__"))
41-
42-
# Build an isolated venv with hatchling + the local plugin wheel.
43-
# We call hatchling directly so we fully control what's installed.
44-
venv_dir = Path(tmpdir) / "build-venv"
45-
venv.create(str(venv_dir), with_pip=True)
46-
python = str(venv_dir / "bin" / "python")
47-
48-
subprocess.run(
49-
[python, "-m", "pip", "install", "--quiet", "hatchling", str(wheels[-1])],
50-
check=True,
51-
)
52-
53-
result = subprocess.run(
54-
[python, "-m", "hatchling", "build", "--target", "sdist"],
55-
cwd=tmp_project,
56-
capture_output=True,
57-
text=True,
58-
)
59-
assert result.returncode == 0, f"hatchling build failed:\n{result.stderr}"
60-
61-
tarballs = sorted((tmp_project / "dist").glob("mypackage-*.tar.gz"))
62-
assert tarballs, "No tarball found in dist/"
63-
64-
with tarfile.open(tarballs[-1]) as tf:
65-
names = tf.getnames()
66-
for expected in EXPECTED_IN_TARBALL:
67-
assert any(
68-
expected in n for n in names
69-
), f"{expected!r} missing from {tarballs[-1].name};\ngot: {names}"
77+
_, tmp_project = _build_fixture_sdist(tmpdir)
7078

71-
# annotations.yml is generated on disk (not bundled in the tarball)
7279
annotations_file = tmp_project / "build" / "reqstool" / "annotations.yml"
7380
assert annotations_file.exists(), f"annotations.yml not generated at {annotations_file}"
7481
annotations_content = annotations_file.read_text()
7582
assert "REQ_001" in annotations_content, "annotations.yml missing REQ_001"
7683
assert "SVC_001" in annotations_content, "annotations.yml missing SVC_001"
84+
85+
86+
@pytest.mark.e2e
87+
@SVCs("SVC_HATCH_PLUGIN_002")
88+
def test_hatch_build_sdist_bundles_reqstool_dataset():
89+
"""hatch build's `finalize` hook bundles the full reqstool dataset -- including the
90+
optional manual_verification_results.yml and configured test_results glob -- into
91+
the sdist tarball, independently of whether annotation generation also succeeds."""
92+
with tempfile.TemporaryDirectory() as tmpdir:
93+
tarball, _ = _build_fixture_sdist(tmpdir)
94+
95+
with tarfile.open(tarball) as tf:
96+
names = tf.getnames()
97+
for expected in EXPECTED_IN_TARBALL:
98+
assert any(expected in n for n in names), f"{expected!r} missing from {tarball.name};\ngot: {names}"
99+
100+
config_member = next(n for n in names if n.endswith("reqstool_config.yml"))
101+
config_fileobj = tf.extractfile(config_member)
102+
assert config_fileobj is not None, f"{config_member!r} is not a regular file"
103+
config_content = config_fileobj.read().decode()
104+
assert "test_results" in config_content, "reqstool_config.yml missing test_results"
105+
assert "junit.xml" in config_content, "reqstool_config.yml missing the configured test_results glob"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="1" time="0.009" timestamp="2026-03-03T21:13:14.848434+01:00" hostname="turing"><testcase classname="tests.test_main" name="test_hello" time="0.000" /></testsuite></testsuites>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/reqstool/reqstool-client/main/src/reqstool/resources/schemas/v1/manual_verification_results.schema.json
2+
3+
results:
4+
- id: MVR_001
5+
svc_ids: ["SVC_001"]
6+
comment: Manually verified hello function returns hello.
7+
pass: true

0 commit comments

Comments
 (0)