| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
|
|
| import pytest |
|
|
| from vons.bundle import ( |
| BundleManifestError, |
| _snapshot_hash, |
| build_bundle_manifest, |
| verify_bundle_manifest, |
| ) |
|
|
|
|
| def _write_external_model(root: Path) -> tuple[Path, Path]: |
| onnx = pytest.importorskip("onnx") |
| from onnx import TensorProto, helper, numpy_helper |
|
|
| root.mkdir(parents=True, exist_ok=True) |
| initializer = numpy_helper.from_array(__import__("numpy").ones(2048, dtype="float32"), name="weight") |
| graph = helper.make_graph( |
| [helper.make_node("Identity", ["input"], ["output"])], |
| "test", |
| [helper.make_tensor_value_info("input", TensorProto.FLOAT, [2])], |
| [helper.make_tensor_value_info("output", TensorProto.FLOAT, [2])], |
| [initializer], |
| ) |
| model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 18)]) |
| graph_path = root / "model.onnx" |
| onnx.save_model(model, graph_path, save_as_external_data=True, all_tensors_to_one_file=True, location="weights.onnx.data") |
| tokenizer = root / "tokenizer" |
| tokenizer.mkdir() |
| (tokenizer / "tokenizer.json").write_text("{}\n", encoding="utf-8") |
| return graph_path, tokenizer |
|
|
|
|
| def test_manifest_inventories_external_data_and_verifies_bytes(tmp_path: Path) -> None: |
| graph, tokenizer = _write_external_model(tmp_path) |
| output = tmp_path / "bundle-manifest.v1.json" |
| build_bundle_manifest(graph, tokenizer, output) |
| result = verify_bundle_manifest(output) |
| payload = json.loads(output.read_text(encoding="utf-8")) |
|
|
| assert result["pass"] is True |
| assert result["external_data_files"] == 1 |
| assert {item["role"] for item in payload["files"]} >= {"model_graph", "model_external_data", "tokenizer"} |
| assert payload["summary"]["model_asset_bytes"] == sum( |
| item["bytes"] for item in payload["files"] if item["model_asset"] |
| ) |
|
|
| external = tmp_path / "weights.onnx.data" |
| external.write_bytes(external.read_bytes() + b"changed") |
| with pytest.raises(BundleManifestError, match="changed"): |
| verify_bundle_manifest(output) |
|
|
|
|
| def test_manifest_rejects_missing_external_data_and_empty_tokenizer(tmp_path: Path) -> None: |
| graph, tokenizer = _write_external_model(tmp_path) |
| (tmp_path / "weights.onnx.data").unlink() |
| with pytest.raises(BundleManifestError, match="external-data file does not exist"): |
| build_bundle_manifest(graph, tokenizer, tmp_path / "manifest.json") |
|
|
| graph, tokenizer = _write_external_model(tmp_path / "second") |
| for child in tokenizer.iterdir(): |
| child.unlink() |
| with pytest.raises(BundleManifestError, match="contains no regular files"): |
| build_bundle_manifest(graph, tokenizer, tokenizer.parent / "manifest.json") |
|
|
|
|
| def test_manifest_rejects_external_data_traversal(tmp_path: Path) -> None: |
| onnx = pytest.importorskip("onnx") |
| from onnx import TensorProto, helper |
|
|
| initializer = helper.make_tensor("weight", TensorProto.FLOAT, [1], [1.0]) |
| initializer.data_location = TensorProto.EXTERNAL |
| initializer.external_data.add(key="location", value="../outside.onnx.data") |
| graph = helper.make_graph([], "test", [], [], [initializer]) |
| model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 18)]) |
| graph_path = tmp_path / "model.onnx" |
| onnx.save_model(model, graph_path) |
| tokenizer = tmp_path / "tokenizer" |
| tokenizer.mkdir() |
| (tokenizer / "tokenizer.json").write_text("{}\n", encoding="utf-8") |
|
|
| with pytest.raises(BundleManifestError, match="traversal"): |
| build_bundle_manifest(graph_path, tokenizer, tmp_path / "manifest.json") |
|
|
|
|
| def test_manifest_rejects_symlinked_tokenizer(tmp_path: Path) -> None: |
| graph, tokenizer = _write_external_model(tmp_path) |
| target = tmp_path / "outside.txt" |
| target.write_text("outside", encoding="utf-8") |
| (tokenizer / "escape.txt").symlink_to(target) |
|
|
| with pytest.raises(BundleManifestError, match="symlink"): |
| build_bundle_manifest(graph, tokenizer, tmp_path / "manifest.json") |
|
|
|
|
| def test_source_snapshot_ignores_generated_python_caches(tmp_path: Path) -> None: |
| source = tmp_path / "source" |
| source.mkdir() |
| (source / "module.py").write_text("value = 1\n", encoding="utf-8") |
| ignored = source / "__pycache__" |
| ignored.mkdir() |
| (ignored / "module.cpython-313.pyc").write_bytes(b"first") |
| first = _snapshot_hash(source) |
| (ignored / "module.cpython-313.pyc").write_bytes(b"second") |
| (source / ".pytest_cache").mkdir() |
| (source / ".pytest_cache" / "state").write_bytes(b"cache") |
| assert _snapshot_hash(source) == first |
|
|
|
|
| def test_source_snapshot_can_pin_an_explicit_file_list(tmp_path: Path) -> None: |
| source = tmp_path / "source" |
| source.mkdir() |
| (source / "module.py").write_text("value = 1\n", encoding="utf-8") |
| (source / "config.json").write_text("{}\n", encoding="utf-8") |
|
|
| first = _snapshot_hash(source, ["module.py"]) |
| assert first["scope"] == "explicit" |
| assert first["paths"] == ["module.py"] |
| (source / "config.json").write_text('{"changed": true}\n', encoding="utf-8") |
| assert _snapshot_hash(source, ["module.py"]) == first |
| (source / "module.py").write_text("value = 2\n", encoding="utf-8") |
| assert _snapshot_hash(source, ["module.py"]) != first |
|
|
|
|
| def test_source_snapshot_rejects_explicit_paths_outside_root(tmp_path: Path) -> None: |
| source = tmp_path / "source" |
| source.mkdir() |
| outside = tmp_path / "outside.py" |
| outside.write_text("value = 1\n", encoding="utf-8") |
|
|
| with pytest.raises(BundleManifestError, match="escapes"): |
| _snapshot_hash(source, [outside]) |
|
|