216 lines
8.7 KiB
Python
216 lines
8.7 KiB
Python
"""Offline packager regressions; Go compilation and SDK admission are separate."""
|
|
|
|
import importlib.util
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
SPEC = importlib.util.spec_from_file_location(
|
|
"package_rust3_compat",
|
|
Path(__file__).resolve().parents[1] / "scripts/package_rust3_compat.py",
|
|
)
|
|
packager = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(packager)
|
|
|
|
|
|
class Rust3PackagerTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temporary.cleanup)
|
|
self.root = Path(self.temporary.name)
|
|
self.plugin = self.root / "device-test"
|
|
self.plugin.mkdir()
|
|
self.manifest = {
|
|
"schema_version": 2,
|
|
"id": "artemis-device-test",
|
|
"version": packager.DEVICE_TEST_VERSION,
|
|
"artemis_version": packager.HOST_RANGE,
|
|
"license": "Apache-2.0",
|
|
}
|
|
(self.plugin / "plugin.json").write_bytes(packager.encoded(self.manifest))
|
|
for name in ("LICENSE", "SOURCE.md", "VERIFICATION.md", "server.py"):
|
|
(self.plugin / name).write_bytes((name + "\n").encode())
|
|
self.update_market()
|
|
|
|
def update_market(self):
|
|
hashes = {
|
|
path.relative_to(self.plugin).as_posix(): packager.sha256(path.read_bytes())
|
|
for path in self.plugin.rglob("*")
|
|
if path.is_file() and path.name != "MARKET.json"
|
|
}
|
|
fingerprint = packager.sha256(
|
|
json.dumps(
|
|
hashes, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
).encode()
|
|
)
|
|
(self.plugin / "MARKET.json").write_bytes(
|
|
packager.encoded(
|
|
{
|
|
"plugin_id": self.manifest["id"],
|
|
"plugin_version": self.manifest["version"],
|
|
"content_sha256": fingerprint,
|
|
}
|
|
)
|
|
)
|
|
|
|
def assemble(self, name, tag=packager.DEFAULT_RELEASE_TAG):
|
|
output = self.root / name
|
|
catalog = self.root / (name + "-catalog.json")
|
|
with (
|
|
patch.object(
|
|
packager,
|
|
"base_runtime",
|
|
return_value=({}, {"plugin_version": packager.BASE_VERSION}),
|
|
),
|
|
patch.object(
|
|
packager,
|
|
"build_adapter",
|
|
return_value=(b"adapter fixture", b"Go license fixture", "go fixture"),
|
|
),
|
|
patch.object(
|
|
packager.subprocess, "check_output", side_effect=["f" * 40, b""]
|
|
),
|
|
patch("builtins.print"),
|
|
):
|
|
packager.main(
|
|
[
|
|
"--cua-base-archive",
|
|
str(self.root / "base.zip"),
|
|
"--go",
|
|
str(self.root / "go.exe"),
|
|
"--output",
|
|
str(output),
|
|
"--catalog-output",
|
|
str(catalog),
|
|
"--device-test-source",
|
|
str(self.plugin),
|
|
"--release-tag",
|
|
tag,
|
|
]
|
|
)
|
|
return output, catalog
|
|
|
|
def test_three_archives_catalog_and_bundle_bind_the_same_final_bytes(self):
|
|
output, catalog_path = self.assemble("candidate")
|
|
catalog = json.loads(catalog_path.read_bytes())
|
|
mapping = json.loads((output / "bundle-map.json").read_bytes())
|
|
self.assertEqual(len(catalog["plugins"]), 3)
|
|
self.assertEqual(len(mapping["packages"]), 3)
|
|
self.assertEqual(
|
|
mapping["catalog_sha256"], packager.sha256(catalog_path.read_bytes())
|
|
)
|
|
self.assertEqual(
|
|
(output / mapping["catalog"]).read_bytes(), catalog_path.read_bytes()
|
|
)
|
|
for entry, record in zip(catalog["plugins"], mapping["packages"], strict=True):
|
|
archive = output / record["file"]
|
|
self.assertEqual(entry["sha256"], packager.sha256(archive.read_bytes()))
|
|
self.assertEqual(record["sha256"], entry["sha256"])
|
|
self.assertEqual(record["archive_url"], entry["archive_url"])
|
|
self.assertIn(
|
|
"/" + packager.DEFAULT_RELEASE_TAG + "/", entry["archive_url"]
|
|
)
|
|
self.assertTrue(entry["archive_url"].startswith(packager.RELEASES + "/"))
|
|
with zipfile.ZipFile(archive) as package:
|
|
self.assertEqual(
|
|
json.loads(package.read("plugin.json")), entry["manifest"]
|
|
)
|
|
device = mapping["packages"][2]
|
|
self.assertEqual(device["version"], "0.5.0+artemis.7")
|
|
with zipfile.ZipFile(output / device["file"]) as package:
|
|
self.assertEqual(
|
|
package.read("server.py"), (self.plugin / "server.py").read_bytes()
|
|
)
|
|
self.assertEqual(
|
|
package.read("MARKET.json"), (self.plugin / "MARKET.json").read_bytes()
|
|
)
|
|
sums = (output / "SHA256SUMS.txt").read_text("ascii").splitlines()
|
|
self.assertEqual(len(sums), 3)
|
|
|
|
def test_repeated_candidate_is_deterministic_and_custom_tag_is_shared(self):
|
|
first, _ = self.assemble("first", "rust3-preview-test")
|
|
second, _ = self.assemble("second", "rust3-preview-test")
|
|
for name in ("SHA256SUMS.txt", "catalog-v5.json", "bundle-map.json"):
|
|
self.assertEqual((first / name).read_bytes(), (second / name).read_bytes())
|
|
catalog = json.loads((first / "catalog-v5.json").read_bytes())
|
|
self.assertTrue(
|
|
all("/rust3-preview-test/" in e["archive_url"] for e in catalog["plugins"])
|
|
)
|
|
|
|
def test_rejects_stale_market_hash_and_mismatched_version(self):
|
|
(self.plugin / "server.py").write_bytes(b"changed")
|
|
with self.assertRaisesRegex(ValueError, "MARKET identity or content hash"):
|
|
packager.device_test_files(self.plugin)
|
|
self.manifest["version"] = "0.5.0+artemis.6"
|
|
(self.plugin / "plugin.json").write_bytes(packager.encoded(self.manifest))
|
|
self.update_market()
|
|
with self.assertRaisesRegex(ValueError, "identity or compatibility"):
|
|
packager.device_test_files(self.plugin)
|
|
|
|
def test_rejects_missing_distribution_material_and_runtime_files(self):
|
|
(self.plugin / "LICENSE").unlink()
|
|
with self.assertRaisesRegex(ValueError, "distribution material"):
|
|
packager.device_test_files(self.plugin)
|
|
(self.plugin / "LICENSE").write_bytes(b"license")
|
|
(self.plugin / ".env").write_bytes(b"private fixture")
|
|
with self.assertRaisesRegex(ValueError, "Non-distributable"):
|
|
packager.device_test_files(self.plugin)
|
|
|
|
def test_source_change_during_build_fails_before_any_archive(self):
|
|
output = self.root / "changing"
|
|
|
|
def change_source(*_):
|
|
(self.plugin / "server.py").write_bytes(b"changed during build")
|
|
return b"adapter", b"license", "fixture"
|
|
|
|
with (
|
|
patch.object(packager, "base_runtime", return_value=({}, {})),
|
|
patch.object(packager, "build_adapter", side_effect=change_source),
|
|
self.assertRaisesRegex(ValueError, "MARKET identity or content hash"),
|
|
):
|
|
packager.main(
|
|
[
|
|
"--cua-base-archive",
|
|
"base.zip",
|
|
"--go",
|
|
"go.exe",
|
|
"--output",
|
|
str(output),
|
|
"--catalog-output",
|
|
str(self.root / "catalog.json"),
|
|
"--device-test-source",
|
|
str(self.plugin),
|
|
]
|
|
)
|
|
self.assertEqual(list(output.glob("*.zip")), [])
|
|
|
|
def test_refuses_existing_outputs_and_unsafe_release_tags(self):
|
|
output, catalog = self.assemble("existing")
|
|
original = catalog.read_bytes()
|
|
with self.assertRaisesRegex(ValueError, "new or empty"):
|
|
packager.main(
|
|
[
|
|
"--cua-base-archive",
|
|
"base.zip",
|
|
"--go",
|
|
"go.exe",
|
|
"--output",
|
|
str(output),
|
|
"--catalog-output",
|
|
str(catalog),
|
|
]
|
|
)
|
|
self.assertEqual(catalog.read_bytes(), original)
|
|
with self.assertRaisesRegex(ValueError, "Refusing to overwrite an archive"):
|
|
packager.write_zip(output / "task-review-1.1.1.zip", {"file": b"changed"})
|
|
for tag in ("../main", "https://invalid", "", "x#fragment", "x y"):
|
|
with self.subTest(tag=tag), self.assertRaises(ValueError):
|
|
packager.release_url(tag, "plugin.zip")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|