Skip to content

Commit 181217b

Browse files
test(packaging): cover native overlay manifest refusals (#394)
* test(packaging): cover manifest invariants * refactor(packaging): remove redundant path check * docs(packaging): record why artifact paths need no uniqueness refusal The removed check was unreachable because both path derivations are injective in the module name and the manifest's paths are forced to equal them. That reasoning lived only in the PR description, where the next person to relax either equality will not find it. --------- Co-authored-by: Kingston <kingstonkuan@u.nus.edu> Co-authored-by: Kingston <kingston@hebbianrobotics.com>
1 parent f28e67f commit 181217b

2 files changed

Lines changed: 109 additions & 6 deletions

File tree

src/hflow/packaging.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -943,8 +943,6 @@ def _validate_manifest(manifest: CythonOverlayManifest) -> CythonOverlayManifest
943943
module_names = [artifact.module_name for artifact in manifest.artifacts]
944944
if len(module_names) != len(set(module_names)):
945945
raise CythonOverlayManifestError("artifact module names must be unique")
946-
artifact_paths: set[str] = set()
947-
source_paths: set[str] = set()
948946
for artifact in manifest.artifacts:
949947
_require_dotted_name(artifact.module_name, "module_name")
950948
expected_source_path = _source_relative_path(manifest.package_name, artifact.module_name)
@@ -964,16 +962,17 @@ def _validate_manifest(manifest: CythonOverlayManifest) -> CythonOverlayManifest
964962
raise CythonOverlayManifestError(
965963
f"artifact_path does not match module_name for {artifact.module_name}"
966964
)
965+
# These two equalities are what make paths unique, given the
966+
# module-name uniqueness checked above: both derivations are injective
967+
# in the module name, so distinct names cannot converge on one path.
968+
# Relaxing either equality (allowing a caller-chosen path, say) brings
969+
# back the need for an explicit path-uniqueness refusal here.
967970
_require_relative_path(artifact.source_path, "source_path")
968971
_require_relative_path(artifact.artifact_path, "artifact_path")
969972
_require_sha256(artifact.source_sha256, "source_sha256")
970973
_require_nonnegative_integer(artifact.source_size_bytes, "source_size_bytes")
971974
_require_sha256(artifact.artifact_sha256, "artifact_sha256")
972975
_require_positive_integer(artifact.artifact_size_bytes, "artifact_size_bytes")
973-
if artifact.source_path in source_paths or artifact.artifact_path in artifact_paths:
974-
raise CythonOverlayManifestError("artifact paths must be unique")
975-
source_paths.add(artifact.source_path)
976-
artifact_paths.add(artifact.artifact_path)
977976
expected_bundle_digest = _calculate_bundle_digest(
978977
package_name=manifest.package_name,
979978
target=manifest.target,

tests/test_packaging.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@
1111
import sys
1212
from dataclasses import replace
1313
from pathlib import Path
14+
from typing import cast
1415

1516
import pytest
1617

18+
import hflow.packaging as packaging
1719
from hflow.cli import main
1820
from hflow.packaging import (
1921
CYTHON_OVERLAY_MANIFEST_FILE_NAME,
2022
INSTALLED_CYTHON_OVERLAY_MANIFEST_FILE_NAME,
2123
CythonOverlayApplyError,
2224
CythonOverlayBuildConfig,
25+
CythonOverlayManifestError,
2326
CythonOverlayVerificationCode,
2427
CythonOverlayVerificationIssue,
2528
apply_cython_overlay,
@@ -147,6 +150,18 @@ def _example_record_path(package_root: Path) -> Path:
147150
return package_root.parent / "sample_native_package-7.2.dist-info" / "RECORD"
148151

149152

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+
150165
def _run_example_package(site_packages_directory: Path) -> subprocess.CompletedProcess[str]:
151166
process_environment = os.environ.copy()
152167
process_environment["PYTHONPATH"] = str(site_packages_directory)
@@ -256,6 +271,95 @@ def test_native_overlay_replaces_only_implementation_sources_and_preserves_distr
256271
assert _example_record_path(package_root).read_bytes() == finalized_record
257272

258273

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+
259363
def test_apply_refuses_a_changed_source_before_installing_any_artifact(
260364
tmp_path: Path,
261365
) -> None:

0 commit comments

Comments
 (0)