Add Phase 9 validation and advisory review
Record the finite dependency search, Wi-Fi maintenance blocker, and pinned icon provenance. Add bounded host orchestration and fixture coverage, and update release documentation with current evidence.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Release notice tool tests
|
||||
|
||||
Run `python3 tests/release_notices/run.py` from the repository root. Uses Python's
|
||||
standard library and isolated temporary fixtures only; no managed package,
|
||||
standard library, isolated temporary fixtures and checked-in icon evidence; no managed package,
|
||||
SDK, toolchain, PlatformIO, network, or device is required. Linux/POSIX path and
|
||||
descriptor semantics match the notice tool.
|
||||
|
||||
@@ -10,8 +10,25 @@ across moved roots/changed mtimes; missing, empty, changed and oversized inputs;
|
||||
source body drift outside excerpts; bounds/schema; traversal and symlinks in
|
||||
input/output ancestry; FIFOs/directories; existing user-data preservation;
|
||||
explicit output requirement; unlisted secret/config/build exclusion; incomplete
|
||||
write behavior; and success/failure CLI exits.
|
||||
write behavior; and success/failure CLI exits. Additional offline icon tests check
|
||||
exact upstream SHA-256/Git blob identities, release and author metadata, absence
|
||||
of NOTICE paths in the complete pinned tree, actual retained SVG coordinates,
|
||||
USB mockup transforms, manual firmware row bytes, and catalog inclusion. A
|
||||
changed coordinate or added transform is rejected; differing mockup Wi-Fi and
|
||||
unproven manual rasterization are not described as exact upstream matches.
|
||||
|
||||
Real installed-input assembly and recipient delivery are separate checks; see
|
||||
`docs/release_packaging.md`. Passing these tests is not license clearance or
|
||||
proof of corresponding-source compliance.
|
||||
Real installed-input assembly is an explicit, separate offline check:
|
||||
|
||||
```sh
|
||||
python3 tests/release_notices/actual_bundle.py \
|
||||
--sdk-root /home/mscholz/.platformio/packages/framework-espidf \
|
||||
--toolchain-root /home/mscholz/.platformio/packages/toolchain-xtensa-esp-elf
|
||||
```
|
||||
|
||||
Use literal paths for the reviewed installed snapshot. This invokes the real
|
||||
CLI twice with fresh temporary outputs outside input roots, verifies catalog
|
||||
identity, every manifest entry and payload hash/size, absence of extra files,
|
||||
and identical relative names/bytes across outputs. Temporary outputs are removed
|
||||
after the check. No fetch, dependency installation, build or device access.
|
||||
Recipient delivery remains separate; see `docs/release_packaging.md`. Passing
|
||||
these tests is not license clearance or corresponding-source compliance.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
"""Assemble two real bundles in temporary directories and verify every byte."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
PROJECT = Path(__file__).absolute().parents[2]
|
||||
|
||||
|
||||
def contents(root):
|
||||
return {p.relative_to(root).as_posix(): p.read_bytes()
|
||||
for p in root.rglob("*") if p.is_file()}
|
||||
|
||||
|
||||
def validate(files, catalog_data):
|
||||
manifest = json.loads(files["manifest.json"])
|
||||
catalog = json.loads(catalog_data)
|
||||
assert manifest["catalog_sha256"] == hashlib.sha256(catalog_data).hexdigest()
|
||||
assert manifest["snapshot"] == catalog["snapshot"]
|
||||
assert len(manifest["inputs"]) == len(catalog["inputs"])
|
||||
expected_entries = {(x["root"], x["path"]): x for x in catalog["inputs"]}
|
||||
expected_files = {"manifest.json"}
|
||||
seen = set()
|
||||
for entry in manifest["inputs"]:
|
||||
key = (entry["root"], entry["path"])
|
||||
assert key not in seen
|
||||
seen.add(key)
|
||||
original = expected_entries[key]
|
||||
assert {k: entry[k] for k in original} == original
|
||||
path = "inputs/" + entry["root"] + "/" + entry["path"]
|
||||
if entry["range"] is not None:
|
||||
path += ".notice.txt"
|
||||
assert entry["output"] == path
|
||||
expected_files.add(path)
|
||||
data = files[path]
|
||||
assert len(data) == entry["output_size"]
|
||||
assert hashlib.sha256(data).hexdigest() == entry["output_sha256"]
|
||||
assert seen == set(expected_entries)
|
||||
assert len(manifest["generated"]) == 1
|
||||
assert manifest["generated"][0]["path"] == "README.txt"
|
||||
for entry in manifest["generated"]:
|
||||
expected_files.add(entry["path"])
|
||||
data = files[entry["path"]]
|
||||
assert len(data) == entry["size"]
|
||||
assert hashlib.sha256(data).hexdigest() == entry["sha256"]
|
||||
assert set(files) == expected_files, "unexpected or missing bundle files"
|
||||
return len(manifest["inputs"])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--sdk-root", type=Path, required=True)
|
||||
parser.add_argument("--toolchain-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
catalog = (PROJECT / "third_party/release-notices/inputs.json").read_bytes()
|
||||
with tempfile.TemporaryDirectory(prefix="actual-release-notices-") as temporary:
|
||||
base = Path(temporary)
|
||||
outputs = []
|
||||
for name in ("first", "second"):
|
||||
output = base / name
|
||||
subprocess.run([
|
||||
sys.executable, str(PROJECT / "tools/release_notices.py"),
|
||||
"--sdk-root", str(args.sdk_root),
|
||||
"--toolchain-root", str(args.toolchain_root), "--output", str(output),
|
||||
], check=True, timeout=60)
|
||||
files = contents(output)
|
||||
count = validate(files, catalog)
|
||||
outputs.append(files)
|
||||
assert outputs[0] == outputs[1], "bundle names/bytes differ"
|
||||
files = outputs[0]
|
||||
print(f"PASS: two actual bundles; {count} inputs; {len(files)} files; "
|
||||
f"{sum(map(len, files.values()))} bytes; all payloads verified and deterministic")
|
||||
print("manifest SHA-256:", hashlib.sha256(files["manifest.json"]).hexdigest())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,173 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
"""Offline checks of actual project icon derivatives, not a raster generator."""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
PROJECT = Path(__file__).absolute().parents[2]
|
||||
ICONS = PROJECT / "third_party/material-design-icons"
|
||||
UPSTREAM = ICONS / "upstream-7.4.47"
|
||||
COMMIT = "9e04201d4557e729822fb57f62a316c3dea1d4a8"
|
||||
TAG = "5edde266e281d26a03dcfa89fb651183cbab0f2e"
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
PINS = {
|
||||
"usb.svg": "c9918e9a983fbd788378ca4c524e7a07a0d5eedcaeff73d19814e6f6ae221f22",
|
||||
"wifi-strength-4.svg": "89d14daf863076b0f73c76d913212875f2e9bcddaaf49c9e5d1825e0b2dc2d5f",
|
||||
"LICENSE": "f3bc8715bad84b26396bb42d2abd11f919cf58163be158eab79ecbaabf84cdf2",
|
||||
"package.json": "ffeb0e4cc17b4cb124408bd0068667b9a35e985971792978e40d4cd69c7ca8b7",
|
||||
"README.md": "db5acca5eb4e2c9113c3408ed1ae7d17c76f653340a5d4e3f8a58d2bcdf50b7d",
|
||||
"meta.json": "ed9d7c7a5ca3ce7dee023fb6c1d62715dbec5b27503d268cbc10b9c8894ef3e8",
|
||||
"tag.json": "f02dc7fc1e862c344c66877828b1914543f0656db8544c05a1ed3b7d990b6d75",
|
||||
"tree.json": "b75710c2b9ee4d0ad3f79016e9767a85103f16df6cbbe5f0629efc3b470d1569",
|
||||
"Apache-2.0.txt": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
||||
}
|
||||
|
||||
|
||||
def load(name):
|
||||
return json.loads((UPSTREAM / name).read_bytes())
|
||||
|
||||
|
||||
def geometry(element):
|
||||
"""Strict comparison: no coordinate normalization or hidden transforms."""
|
||||
if set(element.attrib) != {"id", "viewBox"}:
|
||||
raise ValueError("unexpected root/symbol geometry attributes")
|
||||
if element.attrib["viewBox"] != "0 0 24 24":
|
||||
raise ValueError("unexpected viewBox")
|
||||
if len(element) != 1 or element[0].tag != SVG + "path":
|
||||
raise ValueError("expected exactly one path")
|
||||
if set(element[0].attrib) != {"d"}:
|
||||
raise ValueError("unexpected path geometry attributes")
|
||||
return element[0].attrib["d"]
|
||||
|
||||
|
||||
class IconProvenanceTests(unittest.TestCase):
|
||||
def test_download_pins_and_git_blobs(self):
|
||||
records = load("downloads.json")
|
||||
self.assertEqual({r["path"] for r in records}, set(PINS))
|
||||
self.assertEqual(len(records), len(PINS))
|
||||
tree = {x["path"]: x for x in load("tree.json")["tree"]}
|
||||
for record in records:
|
||||
name = record["path"]
|
||||
with self.subTest(name=name):
|
||||
data = (UPSTREAM / name).read_bytes()
|
||||
self.assertEqual(len(data), record["size"])
|
||||
self.assertEqual(hashlib.sha256(data).hexdigest(), PINS[name])
|
||||
self.assertEqual(record["sha256"], PINS[name])
|
||||
if name not in ("tag.json", "tree.json", "Apache-2.0.txt"):
|
||||
path = "svg/" + name if name.endswith(".svg") else name
|
||||
self.assertEqual(record["url"],
|
||||
"https://raw.githubusercontent.com/Templarian/"
|
||||
f"MaterialDesign-SVG/{COMMIT}/{path}")
|
||||
self.assertEqual(tree[path]["size"], len(data))
|
||||
blob = b"blob " + str(len(data)).encode() + b"\0" + data
|
||||
self.assertEqual(hashlib.sha1(blob).hexdigest(), tree[path]["sha"])
|
||||
for name in ("usb.svg", "wifi-strength-4.svg", "LICENSE"):
|
||||
self.assertEqual((ICONS / name).read_bytes(), (UPSTREAM / name).read_bytes())
|
||||
|
||||
def test_release_license_authors_and_notice_scope(self):
|
||||
tag = load("tag.json")
|
||||
self.assertEqual(tag["sha"], TAG)
|
||||
self.assertEqual(tag["tag"], "v7.4.47")
|
||||
self.assertEqual(tag["object"]["sha"], COMMIT)
|
||||
self.assertEqual(tag["object"]["type"], "commit")
|
||||
self.assertFalse(tag["verification"]["verified"])
|
||||
package = load("package.json")
|
||||
self.assertEqual((package["name"], package["version"], package["license"]),
|
||||
("@mdi/svg", "7.4.47", "Apache-2.0"))
|
||||
expected = {
|
||||
"usb": ("Google", "25033E0B-3AD4-414D-9972-559F2690FC1D", "1.5.54"),
|
||||
"wifi-strength-4": ("Simran", "41B86B22-7245-4A97-9BAA-3E9EBD44CEB0", "2.3.50"),
|
||||
}
|
||||
selected = [x for x in load("meta.json") if x["name"] in expected]
|
||||
self.assertEqual(len(selected), 2)
|
||||
for icon in selected:
|
||||
self.assertEqual((icon["author"], icon["id"], icon["version"]), expected[icon["name"]])
|
||||
self.assertNotIn("license", icon)
|
||||
tree = load("tree.json")
|
||||
self.assertFalse(tree["truncated"])
|
||||
self.assertFalse([x["path"] for x in tree["tree"] if "notice" in x["path"].lower()])
|
||||
|
||||
def test_exact_retained_geometry_and_negative_changes(self):
|
||||
for name in ("usb.svg", "wifi-strength-4.svg"):
|
||||
original = ET.parse(UPSTREAM / name).getroot()
|
||||
local = ET.parse(ICONS / name).getroot()
|
||||
self.assertEqual(geometry(local), geometry(original))
|
||||
changed = copy.deepcopy(local)
|
||||
changed[0].set("d", changed[0].get("d").replace("M", "M0,0L", 1))
|
||||
self.assertNotEqual(geometry(changed), geometry(original))
|
||||
for node in (0, None):
|
||||
changed = copy.deepcopy(local)
|
||||
(changed if node is None else changed[node]).set("transform", "translate(1 0)")
|
||||
with self.assertRaises(ValueError):
|
||||
geometry(changed)
|
||||
|
||||
def test_actual_mockup_paths_and_transforms(self):
|
||||
root = ET.parse(PROJECT / "docs/phase7c_icon_mockup.svg").getroot()
|
||||
parents = {child: parent for parent in root.iter() for child in parent}
|
||||
symbols = {x.get("id"): x for x in root.iter(SVG + "symbol")}
|
||||
self.assertEqual(geometry(symbols["usb"]), geometry(ET.parse(UPSTREAM / "usb.svg").getroot()))
|
||||
self.assertNotEqual(geometry(symbols["wifi"]),
|
||||
geometry(ET.parse(UPSTREAM / "wifi-strength-4.svg").getroot()))
|
||||
uses = [x for x in root.iter(SVG + "use") if x.get("href") == "#usb"]
|
||||
self.assertEqual([x.attrib for x in uses], [
|
||||
{"href": "#usb", "x": "150", "y": "4", "width": "30", "height": "30"},
|
||||
{"href": "#usb", "x": "8", "y": "72", "width": "30", "height": "30"},
|
||||
])
|
||||
|
||||
def ancestry(node):
|
||||
result = []
|
||||
while node in parents:
|
||||
node = parents[node]
|
||||
if "transform" in node.attrib:
|
||||
result.append(node.get("transform"))
|
||||
# Nested SVG viewports would require additional mappings.
|
||||
if node is not root:
|
||||
self.assertNotEqual(node.tag, SVG + "svg")
|
||||
return result
|
||||
|
||||
self.assertEqual(ancestry(uses[0]), [])
|
||||
self.assertEqual(parents[parents[uses[0]]].get("id"), "status")
|
||||
self.assertEqual(ancestry(uses[1]), ["translate(62 438)"])
|
||||
status_uses = [x for x in root.iter(SVG + "use") if x.get("href") == "#status"]
|
||||
self.assertEqual([x.attrib for x in status_uses], [{"href": "#status"}] * 4)
|
||||
self.assertEqual([ancestry(x) for x in status_uses], [
|
||||
["translate(62 98)"], ["translate(592 98)"],
|
||||
["translate(62 438)"], ["translate(592 438)"],
|
||||
])
|
||||
|
||||
def test_actual_manual_masks_not_inferred_rasterization(self):
|
||||
source = (PROJECT / "src/local_status_ui.c").read_text()
|
||||
self.assertIn("hand-rasterized 8x8 derivative", source)
|
||||
self.assertIn('adaptations of "wifi-strength-4"', source)
|
||||
masks = {
|
||||
"usb": "18 3c 18 5a 3e 18 3c 18",
|
||||
"wifi_full": "7e 81 3c 42 18 24 00 18",
|
||||
"wifi_three": "00 00 3c 42 18 24 00 18",
|
||||
"wifi_two": "00 00 00 00 18 24 00 18",
|
||||
"wifi_one": "00 00 00 00 00 00 00 18",
|
||||
}
|
||||
for name, expected in masks.items():
|
||||
match = re.search(r"s_icon_" + name + r"\[LOCAL_STATUS_UI_ICON_SIZE\]\s*=\s*\{([^}]+)\}", source)
|
||||
self.assertIsNotNone(match)
|
||||
actual = re.findall(r"0x([0-9a-fA-F]{2})U", match[1])
|
||||
self.assertEqual(bytes.fromhex(" ".join(actual)), bytes.fromhex(expected))
|
||||
|
||||
def test_all_icon_evidence_is_pinned_in_catalog(self):
|
||||
catalog = json.loads((PROJECT / "third_party/release-notices/inputs.json").read_bytes())
|
||||
entries = {(x["root"], x["path"]): x for x in catalog["inputs"]}
|
||||
paths = [UPSTREAM / name for name in (*PINS, "downloads.json")]
|
||||
paths += [ICONS / name for name in ("LICENSE", "usb.svg", "wifi-strength-4.svg")]
|
||||
paths += [PROJECT / "docs/icon_provenance.md"]
|
||||
for path in paths:
|
||||
with self.subTest(path=path.name):
|
||||
entry = entries[("project", path.relative_to(PROJECT).as_posix())]
|
||||
data = path.read_bytes()
|
||||
self.assertIsNone(entry["range"])
|
||||
self.assertEqual(entry["size"], len(data))
|
||||
self.assertEqual(entry["sha256"], hashlib.sha256(data).hexdigest())
|
||||
self.assertEqual(entry["output_sha256"], entry["sha256"])
|
||||
@@ -14,6 +14,8 @@ import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
from icon_provenance import IconProvenanceTests
|
||||
|
||||
PROJECT = Path(__file__).absolute().parents[2]
|
||||
SPEC = importlib.util.spec_from_file_location("release_notices", PROJECT / "tools/release_notices.py")
|
||||
notices = importlib.util.module_from_spec(SPEC)
|
||||
|
||||
Reference in New Issue
Block a user