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:
@@ -36,8 +36,25 @@ with tempfile.TemporaryDirectory(prefix="admin-console-boundary-") as directory:
|
||||
"-o", str(path / "certificate")], check=True, timeout=30)
|
||||
subprocess.run([str(path / "certificate")], check=True, timeout=10)
|
||||
ssh = (ROOT / "src/ssh_transport.c").read_text()
|
||||
adapter = ssh[ssh.index("static admin_ssh_console_token_t admin_console_token("):
|
||||
ssh.index("static void *ssh_malloc(")]
|
||||
# Bound the adapter by its own functions, not the allocator helpers now
|
||||
# owned by ssh_memory.c. Fail closed if the reviewed source layout changes.
|
||||
adapter_markers = (
|
||||
"static admin_ssh_console_token_t admin_console_token(",
|
||||
"static void publish_slot(",
|
||||
"static size_t admin_console_snapshot_index_locked(",
|
||||
"static bool admin_console_is_current(",
|
||||
"static bool admin_console_drained(",
|
||||
"static esp_err_t admin_console_perform(",
|
||||
"static const admin_console_owner_t s_admin_console_owner = {",
|
||||
"static bool consume_external_close(",
|
||||
)
|
||||
positions = []
|
||||
for marker in adapter_markers:
|
||||
assert ssh.count(marker) == 1, f"SSH adapter marker missing/ambiguous: {marker}"
|
||||
positions.append(ssh.index(marker))
|
||||
assert positions == sorted(positions), "SSH adapter source order changed"
|
||||
adapter_end = ssh.index("\n}", positions[-1]) + len("\n}")
|
||||
adapter = ssh[positions[0]:adapter_end]
|
||||
unit = ((ROOT / "tests/admin_console_boundary/fakes.h").read_text()
|
||||
+ strip_includes(header) + "\n" + strip_includes(source)
|
||||
+ (ROOT / "tests/admin_console_boundary/adapter.c").read_text()
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline orchestrator fixtures only; never invokes pio or existing suites."""
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
spec = importlib.util.spec_from_file_location('phase9', ROOT / 'tools/validate_phase9.py')
|
||||
runner = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = runner
|
||||
spec.loader.exec_module(runner)
|
||||
|
||||
|
||||
class ValidationTests(unittest.TestCase):
|
||||
def options(self, *args):
|
||||
return runner.parser().parse_args(args)
|
||||
|
||||
def command(self, code, name='fixture', timeout=3, required=()):
|
||||
return runner.Command(name, (sys.executable, '-c', code), timeout, required)
|
||||
|
||||
def execute(self, commands, **kwargs):
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
code = runner.execute(commands, ROOT, **kwargs)
|
||||
return code, output.getvalue()
|
||||
|
||||
def test_default_exact_scope_and_real_paths(self):
|
||||
commands = runner.plan(self.options())
|
||||
self.assertEqual(len(commands), 23)
|
||||
self.assertEqual({c.name for c in commands}, {
|
||||
'security_build_policy', 'ssh_auth_policy', 'ssh_auth_transport', 'hidden_input',
|
||||
'ssh_memory', 'sdk_security_overrides', 'wolfssh_auth_contract', 'ssh_protocol_policy',
|
||||
'wolf_crypto_policy', 'wolfssh_parser_contract', 'wolfssh_order_contract', 'release_notices',
|
||||
'ssh_management', 'admin_console_boundary', 'admin_ssh_policy', 'web_admin_transport',
|
||||
'web_admin_tickets', 'web_httpd_idle', 'web_cookie_auth:base', 'web_cookie_auth:admin',
|
||||
'web_cookie_auth:accounts', 'web_cookie_auth:ssh', 'web_cookie_auth:lifecycle'})
|
||||
for command in commands:
|
||||
self.assertTrue((ROOT / command.argv[2]).is_file())
|
||||
self.assertEqual(command.argv[:2], (sys.executable, '-B'))
|
||||
self.assertEqual(command.timeout, 180)
|
||||
self.assertFalse({'--interop', '--host-only', '--candidate', '--target-contracts',
|
||||
'--pio-adapter', 'pio', '--admission'} & set(command.argv))
|
||||
by_name = {c.name: c for c in commands}
|
||||
database = str(runner.BUILD / 'compile_commands.json')
|
||||
for name in ('wolfssh_auth_contract', 'ssh_protocol_policy', 'wolf_crypto_policy'):
|
||||
self.assertEqual(by_name[name].argv[3:], ('--compile-commands', database))
|
||||
self.assertEqual(by_name['sdk_security_overrides'].argv[3:], ('--build-dir', str(runner.BUILD)))
|
||||
self.assertEqual(by_name['security_build_policy'].argv[3:],
|
||||
('--sdkconfig-header', str(runner.BUILD / 'config/sdkconfig.h')))
|
||||
|
||||
def test_opt_ins_independent_and_timeout(self):
|
||||
commands = runner.plan(self.options('--build', '--timeout', '4', '--build-timeout', '5'))
|
||||
self.assertEqual(commands[0], runner.Command('build', ('pio', 'run'), 5))
|
||||
self.assertTrue(all(c.timeout == 4 for c in commands[1:]))
|
||||
self.assertFalse(any('--interop' in c.argv for c in commands))
|
||||
commands = runner.plan(self.options('--interop'))
|
||||
self.assertNotIn('build', [c.name for c in commands])
|
||||
self.assertEqual([c.name for c in commands if '--interop' in c.argv], ['wolfssh_order_contract'])
|
||||
|
||||
def test_bad_timeout(self):
|
||||
for value in ('0', '-1', 'nan', 'inf', '3601', 'junk'):
|
||||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
|
||||
self.options('--timeout', value)
|
||||
|
||||
def test_dry_run_never_launches_or_checks_prerequisites(self):
|
||||
with patch.object(runner, 'run_command', side_effect=AssertionError('executed')):
|
||||
code, output = self.execute(runner.plan(self.options('--build', '--interop')), dry_run=True)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn('PLAN build', output)
|
||||
self.assertNotIn('PASS ', output)
|
||||
with patch.object(runner, 'run_command', side_effect=AssertionError('executed')):
|
||||
with contextlib.redirect_stdout(io.StringIO()) as output:
|
||||
self.assertEqual(runner.main(['--dry-run']), 0)
|
||||
self.assertIn('SKIP optional build', output.getvalue())
|
||||
self.assertIn('SKIP optional interop', output.getvalue())
|
||||
|
||||
def test_collect_failure_and_failfast_skip(self):
|
||||
commands = [self.command('raise SystemExit(7)', 'bad'), self.command('pass', 'good')]
|
||||
code, output = self.execute(commands)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('FAIL bad: exit 7', output)
|
||||
self.assertIn('PASS good', output)
|
||||
code, output = self.execute(commands, fail_fast=True)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('SKIP good', output)
|
||||
self.assertNotIn('PASS good', output)
|
||||
|
||||
def test_failed_build_blocks_stale_artifact_checks(self):
|
||||
code, output = self.execute([self.command('raise SystemExit(1)', 'build'), self.command('pass', 'host')])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('SKIP host', output)
|
||||
|
||||
def test_prerequisites(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
missing = Path(tmp) / 'missing'
|
||||
commands = [runner.Command('missing-executable', (str(missing),), 1),
|
||||
self.command('raise AssertionError()', 'missing-input', required=(missing,))]
|
||||
code, output = self.execute(commands)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('PREREQ missing-executable', output)
|
||||
self.assertIn('PREREQ missing-input', output)
|
||||
|
||||
def test_environment_and_literal_argv(self):
|
||||
with tempfile.TemporaryDirectory(prefix='phase9 fixture ') as tmp:
|
||||
script = Path(tmp) / 'fixture ; literal.py'
|
||||
script.write_text('import os, sys\nassert os.environ["CCACHE_DISABLE"] == "1"\n'
|
||||
'assert os.environ["PHASE9_FIXTURE"] == "preserved"\n'
|
||||
'assert sys.argv[1] == "a ; $(not-a-command)"\n')
|
||||
command = runner.Command('literal', (sys.executable, str(script), 'a ; $(not-a-command)'), 3)
|
||||
with patch.dict(os.environ, {'PHASE9_FIXTURE': 'preserved', 'CCACHE_DISABLE': '0'}):
|
||||
self.assertEqual(self.execute([command])[0], 0)
|
||||
self.assertEqual(os.environ['CCACHE_DISABLE'], '0')
|
||||
|
||||
def test_timeout_kills_descendant_and_collects(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
marker = Path(tmp) / 'should-not-exist'
|
||||
child = f'import time; from pathlib import Path; time.sleep(1); Path({str(marker)!r}).touch()'
|
||||
parent = f'import subprocess, sys, time; subprocess.Popen([sys.executable, "-c", {child!r}]); time.sleep(10)'
|
||||
started = time.monotonic()
|
||||
code, output = self.execute([self.command(parent, timeout=0.25), self.command('pass', 'next')])
|
||||
self.assertLess(time.monotonic() - started, 4)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('TIMEOUT fixture', output)
|
||||
self.assertIn('PASS next', output)
|
||||
time.sleep(1.1)
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
def test_streams_not_captured_and_stdin_closed(self):
|
||||
with patch.object(runner.subprocess, 'Popen') as popen, patch.object(runner.os, 'killpg'):
|
||||
popen.return_value.wait.return_value = 0
|
||||
code, _ = self.execute([self.command('pass')])
|
||||
self.assertEqual(code, 0)
|
||||
kwargs = popen.call_args.kwargs
|
||||
self.assertNotIn('stdout', kwargs)
|
||||
self.assertNotIn('stderr', kwargs)
|
||||
self.assertNotIn('shell', kwargs)
|
||||
self.assertEqual(kwargs['stdin'], subprocess.DEVNULL)
|
||||
self.assertTrue(kwargs['start_new_session'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -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