|
13 | 13 | DIST_DIR = Path(__file__).parents[3] / "dist" |
14 | 14 |
|
15 | 15 | # 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). |
18 | 21 | EXPECTED_IN_TARBALL = [ |
19 | 22 | "reqstool_config.yml", |
20 | 23 | "requirements.yml", |
21 | 24 | "software_verification_cases.yml", |
| 25 | + "manual_verification_results.yml", |
22 | 26 | ] |
23 | 27 |
|
24 | 28 |
|
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). |
29 | 33 |
|
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). |
33 | 35 | """ |
| 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 | + |
34 | 48 | wheels = sorted(DIST_DIR.glob("reqstool_python_hatch_plugin-*.whl")) |
35 | 49 | if not wheels: |
36 | 50 | pytest.skip("No local wheel found — run `hatch build --target wheel` first") |
37 | 51 |
|
| 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.""" |
38 | 76 | 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) |
70 | 78 |
|
71 | | - # annotations.yml is generated on disk (not bundled in the tarball) |
72 | 79 | annotations_file = tmp_project / "build" / "reqstool" / "annotations.yml" |
73 | 80 | assert annotations_file.exists(), f"annotations.yml not generated at {annotations_file}" |
74 | 81 | annotations_content = annotations_file.read_text() |
75 | 82 | assert "REQ_001" in annotations_content, "annotations.yml missing REQ_001" |
76 | 83 | 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" |
0 commit comments