|
11 | 11 | import sys |
12 | 12 | from dataclasses import replace |
13 | 13 | from pathlib import Path |
| 14 | +from typing import cast |
14 | 15 |
|
15 | 16 | import pytest |
16 | 17 |
|
| 18 | +import hflow.packaging as packaging |
17 | 19 | from hflow.cli import main |
18 | 20 | from hflow.packaging import ( |
19 | 21 | CYTHON_OVERLAY_MANIFEST_FILE_NAME, |
20 | 22 | INSTALLED_CYTHON_OVERLAY_MANIFEST_FILE_NAME, |
21 | 23 | CythonOverlayApplyError, |
22 | 24 | CythonOverlayBuildConfig, |
| 25 | + CythonOverlayManifestError, |
23 | 26 | CythonOverlayVerificationCode, |
24 | 27 | CythonOverlayVerificationIssue, |
25 | 28 | apply_cython_overlay, |
@@ -147,6 +150,18 @@ def _example_record_path(package_root: Path) -> Path: |
147 | 150 | return package_root.parent / "sample_native_package-7.2.dist-info" / "RECORD" |
148 | 151 |
|
149 | 152 |
|
| 153 | +def _read_manifest_payload(manifest_path: Path) -> dict[str, object]: |
| 154 | + return cast(dict[str, object], json.loads(manifest_path.read_text(encoding="utf-8"))) |
| 155 | + |
| 156 | + |
| 157 | +def _write_manifest_payload(manifest_path: Path, payload: dict[str, object]) -> None: |
| 158 | + manifest_path.chmod(0o644) |
| 159 | + manifest_path.write_text( |
| 160 | + json.dumps(payload, indent=2, sort_keys=True) + "\n", |
| 161 | + encoding="utf-8", |
| 162 | + ) |
| 163 | + |
| 164 | + |
150 | 165 | def _run_example_package(site_packages_directory: Path) -> subprocess.CompletedProcess[str]: |
151 | 166 | process_environment = os.environ.copy() |
152 | 167 | process_environment["PYTHONPATH"] = str(site_packages_directory) |
@@ -256,6 +271,95 @@ def test_native_overlay_replaces_only_implementation_sources_and_preserves_distr |
256 | 271 | assert _example_record_path(package_root).read_bytes() == finalized_record |
257 | 272 |
|
258 | 273 |
|
| 274 | +@pytest.mark.parametrize( |
| 275 | + ("mutation", "expected_message"), |
| 276 | + [ |
| 277 | + ("schema-version", "unsupported native overlay schema version"), |
| 278 | + ("format", "unsupported native overlay format"), |
| 279 | + ("empty-artifacts", "artifacts must not be empty"), |
| 280 | + ("unsorted-artifacts", "artifacts must be sorted by module_name"), |
| 281 | + ("duplicate-module", "artifact module names must be unique"), |
| 282 | + ], |
| 283 | +) |
| 284 | +def test_apply_refuses_invalid_manifest_before_mutation( |
| 285 | + tmp_path: Path, |
| 286 | + mutation: str, |
| 287 | + expected_message: str, |
| 288 | +) -> None: |
| 289 | + package_root, _ = _write_example_distribution(tmp_path) |
| 290 | + overlay_directory = tmp_path / "native-overlay" |
| 291 | + manifest = build_cython_overlay( |
| 292 | + CythonOverlayBuildConfig(package_root=package_root), |
| 293 | + overlay_directory, |
| 294 | + ) |
| 295 | + manifest_path = overlay_directory / CYTHON_OVERLAY_MANIFEST_FILE_NAME |
| 296 | + payload = _read_manifest_payload(manifest_path) |
| 297 | + artifacts = cast(list[dict[str, object]], payload["artifacts"]) |
| 298 | + if mutation == "schema-version": |
| 299 | + assert payload["schema_version"] == packaging.CYTHON_OVERLAY_SCHEMA_VERSION |
| 300 | + payload["schema_version"] = packaging.CYTHON_OVERLAY_SCHEMA_VERSION + 1 |
| 301 | + elif mutation == "format": |
| 302 | + payload["format"] = "unsupported-native-overlay" |
| 303 | + elif mutation == "empty-artifacts": |
| 304 | + payload["artifacts"] = [] |
| 305 | + elif mutation == "unsorted-artifacts": |
| 306 | + artifacts.reverse() |
| 307 | + elif mutation == "duplicate-module": |
| 308 | + artifacts[1]["module_name"] = artifacts[0]["module_name"] |
| 309 | + else: |
| 310 | + raise AssertionError(f"unknown manifest mutation: {mutation}") |
| 311 | + _write_manifest_payload(manifest_path, payload) |
| 312 | + original_record = _example_record_path(package_root).read_bytes() |
| 313 | + |
| 314 | + with pytest.raises(CythonOverlayManifestError, match=expected_message): |
| 315 | + apply_cython_overlay(overlay_directory, package_root) |
| 316 | + |
| 317 | + assert all((package_root / artifact.source_path).is_file() for artifact in manifest.artifacts) |
| 318 | + assert not any( |
| 319 | + (package_root / artifact.installed_artifact_path).exists() |
| 320 | + for artifact in manifest.artifacts |
| 321 | + ) |
| 322 | + assert not (package_root / INSTALLED_CYTHON_OVERLAY_MANIFEST_FILE_NAME).exists() |
| 323 | + assert _example_record_path(package_root).read_bytes() == original_record |
| 324 | + |
| 325 | + |
| 326 | +def test_schema_version_is_bound_into_the_bundle_digest(tmp_path: Path) -> None: |
| 327 | + package_root, _ = _write_example_distribution(tmp_path) |
| 328 | + overlay_directory = tmp_path / "native-overlay" |
| 329 | + manifest = build_cython_overlay( |
| 330 | + CythonOverlayBuildConfig(package_root=package_root), |
| 331 | + overlay_directory, |
| 332 | + ) |
| 333 | + manifest_path = overlay_directory / CYTHON_OVERLAY_MANIFEST_FILE_NAME |
| 334 | + payload = _read_manifest_payload(manifest_path) |
| 335 | + assert payload["schema_version"] == packaging.CYTHON_OVERLAY_SCHEMA_VERSION |
| 336 | + digest_payload = { |
| 337 | + key: payload[key] for key in ("format", "package_name", "target", "toolchain", "artifacts") |
| 338 | + } |
| 339 | + canonical_bytes = json.dumps( |
| 340 | + digest_payload, |
| 341 | + sort_keys=True, |
| 342 | + separators=(",", ":"), |
| 343 | + ).encode("utf-8") |
| 344 | + payload["bundle_digest"] = "sha256:" + hashlib.sha256(canonical_bytes).hexdigest() |
| 345 | + _write_manifest_payload(manifest_path, payload) |
| 346 | + original_record = _example_record_path(package_root).read_bytes() |
| 347 | + |
| 348 | + with pytest.raises( |
| 349 | + CythonOverlayManifestError, |
| 350 | + match="bundle_digest does not match manifest components", |
| 351 | + ): |
| 352 | + apply_cython_overlay(overlay_directory, package_root) |
| 353 | + |
| 354 | + assert all((package_root / artifact.source_path).is_file() for artifact in manifest.artifacts) |
| 355 | + assert not any( |
| 356 | + (package_root / artifact.installed_artifact_path).exists() |
| 357 | + for artifact in manifest.artifacts |
| 358 | + ) |
| 359 | + assert not (package_root / INSTALLED_CYTHON_OVERLAY_MANIFEST_FILE_NAME).exists() |
| 360 | + assert _example_record_path(package_root).read_bytes() == original_record |
| 361 | + |
| 362 | + |
259 | 363 | def test_apply_refuses_a_changed_source_before_installing_any_artifact( |
260 | 364 | tmp_path: Path, |
261 | 365 | ) -> None: |
|
0 commit comments