mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-01 14:27:00 +00:00
Phase 0 of the filament_id cleanup (filament_id_plan.md): - scripts/assign_filament_ids.py: mint OF-prefixed 8-char ids as uuid5(FILAMENT_ID_NAMESPACE, filament_family/<vendor>/<family>) in the base62 derivation of the setting_id precedent; loader-faithful effective-id resolver (vendor inherits chain + OrcaFilamentLibrary fallback); CLI: default assign run (idempotent no-op on a fully-idded tree), --mint Vendor/Family, --update-snapshot, --check. - scripts/filament_id_snapshot.json: the sanctioned-state ledger (1092 ids, 1965 family claims over the current tree). The tree must equal it exactly, both directions, so every id/claim change lands as a reviewable diff; a PR snapshot diff is the maintainer gate. scripts/retired_filament_ids.json is the append-only retirement ledger; --update-snapshot refuses to resurrect retired ids and to sanction new reserved-namespace claims (GF*/QD_*/P-hex/ null) for non-owner vendors without --allow-shared-catalog. - orca_extra_profile_check.py: runs check_filament_ids() tree-wide (format, snapshot equality, mint conformance, retired reuse, alias hygiene for tuned OFL generics, reserved namespaces, structure ratchet). The pre-existing check_filament_id() 8-char rule stays BBL/OFL-scoped: grandfathered longer ids exist elsewhere (e.g. Prusa's 36-char name ids) and are frozen via the snapshot instead. - PresetBundle::check_duplicate_filament_subtypes now includes OrcaFilamentLibrary presets in every vendor's per-printer duplicate check; alias-shadowed library presets are excluded via the existing m_excluded_from population (verified live in the validator load path), so only genuinely visible duplicates are flagged. AMS tray-id resolution logs a warning when a filament_id matches 2+ compatible presets (pick unchanged). - doc/developer-reference/filament_id.md: the authoring rule; PR template gains a no-hand-written-ids checkbox. - scripts/tests/test_filament_id.py: 46 stdlib-unittest tests (mint vectors, resolver semantics, every check firing/silent, ledger round-trips, byte preservation, real-tree smoke). Verified: python -m unittest discover -s scripts/tests (46 OK); python scripts/orca_extra_profile_check.py exit 0 on the unmigrated tree; assign run is a no-op; rebuilt OrcaSlicer_profile_validator -l 2 exit 0; extended -f is strictly additive vs baseline (every baseline group preserved, all new groups involve library presets, alias exclusion proven by BBL-mirror absence on BBL printers).
670 lines
30 KiB
Python
670 lines
30 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for scripts/assign_filament_ids.py (stdlib unittest, no external deps).
|
|
|
|
Run from the repo root: python -m unittest discover -s scripts/tests -v
|
|
"""
|
|
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import uuid
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
|
|
import assign_filament_ids as afi # noqa: E402
|
|
|
|
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
REAL_PROFILES = os.path.join(REPO_ROOT, "resources", "profiles")
|
|
|
|
OFL = "OrcaFilamentLibrary"
|
|
|
|
|
|
def load_json_file(path):
|
|
with open(path, encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# helpers: synthetic profile trees
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def preset(name, filament_id=None, inherits=None, instantiation=True,
|
|
compatible_printers=None):
|
|
data = {"type": "filament", "name": name}
|
|
if inherits is not None:
|
|
data["inherits"] = inherits
|
|
if filament_id is not None:
|
|
data["filament_id"] = filament_id
|
|
data["instantiation"] = "true" if instantiation else "false"
|
|
if compatible_printers is not None:
|
|
data["compatible_printers"] = compatible_printers
|
|
return data
|
|
|
|
|
|
class SyntheticTree:
|
|
"""A throwaway resources/profiles-shaped directory plus ledger paths."""
|
|
|
|
def __init__(self):
|
|
self.dir = tempfile.mkdtemp(prefix="filament_id_test_")
|
|
self.profiles = os.path.join(self.dir, "profiles")
|
|
os.makedirs(self.profiles)
|
|
self.snapshot = os.path.join(self.dir, "filament_id_snapshot.json")
|
|
self.retired = os.path.join(self.dir, "retired_filament_ids.json")
|
|
|
|
def cleanup(self):
|
|
shutil.rmtree(self.dir, ignore_errors=True)
|
|
|
|
def preset_path(self, vendor, name):
|
|
return os.path.join(self.profiles, vendor, "filament", name + ".json")
|
|
|
|
def add_vendor(self, vendor, presets):
|
|
vendor_dir = os.path.join(self.profiles, vendor, "filament")
|
|
os.makedirs(vendor_dir, exist_ok=True)
|
|
index = {"name": vendor, "version": "01.00.00.00", "filament_list": []}
|
|
for data in presets:
|
|
fname = data["name"] + ".json"
|
|
with open(os.path.join(vendor_dir, fname), "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
index["filament_list"].append(
|
|
{"name": data["name"], "sub_path": f"filament/{fname}"})
|
|
with open(os.path.join(self.profiles, vendor + ".json"), "w",
|
|
encoding="utf-8") as f:
|
|
json.dump(index, f, indent=4, ensure_ascii=False)
|
|
|
|
def add_to_index(self, vendor, name):
|
|
idx_path = os.path.join(self.profiles, vendor + ".json")
|
|
with open(idx_path, encoding="utf-8") as f:
|
|
index = json.load(f)
|
|
index["filament_list"].append(
|
|
{"name": name, "sub_path": f"filament/{name}.json"})
|
|
with open(idx_path, "w", encoding="utf-8") as f:
|
|
json.dump(index, f, indent=4, ensure_ascii=False)
|
|
|
|
def write_preset(self, vendor, data, register=True):
|
|
path = self.preset_path(vendor, data["name"])
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
if register:
|
|
self.add_to_index(vendor, data["name"])
|
|
|
|
def remove_preset(self, vendor, name):
|
|
os.remove(self.preset_path(vendor, name))
|
|
idx_path = os.path.join(self.profiles, vendor + ".json")
|
|
with open(idx_path, encoding="utf-8") as f:
|
|
index = json.load(f)
|
|
index["filament_list"] = [
|
|
e for e in index["filament_list"] if e["name"] != name]
|
|
with open(idx_path, "w", encoding="utf-8") as f:
|
|
json.dump(index, f, indent=4, ensure_ascii=False)
|
|
|
|
# -- pipeline wrappers ---------------------------------------------------
|
|
|
|
def update_snapshot(self, allow_shared_catalog=False):
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
rc = afi.update_snapshot(self.profiles, self.snapshot, self.retired,
|
|
allow_shared_catalog=allow_shared_catalog)
|
|
return rc, buf.getvalue()
|
|
|
|
def check(self):
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
errors = afi.check_filament_ids(self.profiles, self.snapshot, self.retired)
|
|
return errors, buf.getvalue()
|
|
|
|
def assign(self):
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
changed, errors = afi.assign_missing_ids(self.profiles, self.snapshot,
|
|
self.retired)
|
|
return changed, errors, buf.getvalue()
|
|
|
|
|
|
def make_clean_tree():
|
|
"""Baseline tree: OFL base+generic, a vendor family, a clean tuned generic."""
|
|
t = SyntheticTree()
|
|
t.add_vendor(OFL, [
|
|
preset("fdm_pla", filament_id="OGFL99", instantiation=False),
|
|
preset("Generic PLA @System", inherits="fdm_pla",
|
|
compatible_printers=[]),
|
|
])
|
|
t.add_vendor("VendorA", [
|
|
preset("APLA @base", filament_id="AX01", instantiation=False),
|
|
preset("APLA @P1", inherits="APLA @base",
|
|
compatible_printers=["P1 0.4 nozzle"]),
|
|
# Correctly tuned OFL generic: keeps the OFL base name, claims a printer.
|
|
preset("Generic PLA @P1", inherits="Generic PLA @System",
|
|
compatible_printers=["P1 0.4 nozzle"]),
|
|
])
|
|
rc, _out = t.update_snapshot()
|
|
assert rc == 0
|
|
return t
|
|
|
|
|
|
class SyntheticTreeCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.t = make_clean_tree()
|
|
self.addCleanup(self.t.cleanup)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestMint(unittest.TestCase):
|
|
def test_namespace_literal(self):
|
|
# Frozen: derived from the setting_id namespace; baked into the ledger.
|
|
self.assertEqual(afi.FILAMENT_ID_NAMESPACE,
|
|
uuid.UUID("c4d3ff49-4c32-5534-a3e3-00894157ab97"))
|
|
|
|
def test_known_vector(self):
|
|
# Hardcoded, independently computed vector: freezes prefix, input string
|
|
# layout ("filament_family/<vendor>/<family>") and base62 derivation.
|
|
self.assertEqual(afi.generate_filament_id("Qidi", "HATCHBOX PLA"), "OFiJYDPC")
|
|
|
|
def test_determinism_and_format(self):
|
|
for vendor, family in [("Qidi", "HATCHBOX PLA"), ("Creality", "CR PLA"),
|
|
("OrcaFilamentLibrary", "Generic ABS"),
|
|
("BBL", "拓竹 PLA")]: # unicode family
|
|
a = afi.generate_filament_id(vendor, family)
|
|
b = afi.generate_filament_id(vendor, family)
|
|
self.assertEqual(a, b)
|
|
self.assertRegex(a, r"^OF[0-9A-Za-z]{6}$")
|
|
self.assertEqual(len(a), 8)
|
|
|
|
def test_salt_changes_id(self):
|
|
base = afi.generate_filament_id("Qidi", "HATCHBOX PLA")
|
|
salted = afi.generate_filament_id("Qidi", "HATCHBOX PLA", salt=1)
|
|
self.assertNotEqual(base, salted)
|
|
self.assertRegex(salted, r"^OF[0-9A-Za-z]{6}$")
|
|
|
|
def test_mint_salt_iteration(self):
|
|
v, fam = "Qidi", "HATCHBOX PLA"
|
|
taken = {afi.generate_filament_id(v, fam, s) for s in range(2)}
|
|
self.assertEqual(afi.mint_filament_id(v, fam, taken),
|
|
afi.generate_filament_id(v, fam, salt=2))
|
|
self.assertEqual(afi.mint_filament_id(v, fam, set()),
|
|
afi.generate_filament_id(v, fam))
|
|
|
|
|
|
class TestBaseName(unittest.TestCase):
|
|
def test_family_derivation(self):
|
|
cases = [
|
|
("X @base", "X"),
|
|
("Afinia PLA@HS", "Afinia PLA"),
|
|
("PolyTerra PLA", "PolyTerra PLA"),
|
|
("HATCHBOX PLA @Qidi X-Plus 4 0.6 nozzle", "HATCHBOX PLA"),
|
|
("A @B @C", "A"), # first @ wins
|
|
("Filár PLA 拓竹 @0.4 nozzle", "Filár PLA 拓竹"),
|
|
]
|
|
for name, family in cases:
|
|
self.assertEqual(afi.base_name(name), family, msg=name)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolver (loader-faithful semantics)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestResolver(unittest.TestCase):
|
|
@staticmethod
|
|
def rec(name, filament_id=None, inherits=None):
|
|
return {"name": name, "filament_id": filament_id, "inherits": inherits}
|
|
|
|
def resolve(self, name, vendor_recs, ofl_recs, **kw):
|
|
fmap = {r["name"]: r for r in vendor_recs}
|
|
omap = {r["name"]: r for r in ofl_recs}
|
|
return afi.resolve_filament_id(name, fmap, omap, **kw)
|
|
|
|
def test_own_id(self):
|
|
fid, src, entry = self.resolve("A", [self.rec("A", "ID1")], [])
|
|
self.assertEqual((fid, src, entry), ("ID1", "own", None))
|
|
|
|
def test_inherited_within_vendor(self):
|
|
fid, src, entry = self.resolve(
|
|
"A", [self.rec("A", inherits="B"), self.rec("B", inherits="C"),
|
|
self.rec("C", "ID3")], [])
|
|
self.assertEqual((fid, src, entry), ("ID3", "inherited", None))
|
|
|
|
def test_ofl_fallback(self):
|
|
# Vendor preset inherits a name that only exists in the OFL map.
|
|
fid, src, entry = self.resolve(
|
|
"A", [self.rec("A", inherits="Generic PLA @System")],
|
|
[self.rec("Generic PLA @System", inherits="fdm_pla"),
|
|
self.rec("fdm_pla", "OGFL99")])
|
|
self.assertEqual(fid, "OGFL99")
|
|
self.assertEqual(entry, "Generic PLA @System")
|
|
|
|
def test_ofl_stays_in_ofl(self):
|
|
# Once a chain enters OFL it stays there: a vendor file sharing an
|
|
# OFL-internal hop's name must not shadow it.
|
|
fid, _src, entry = self.resolve(
|
|
"A",
|
|
[self.rec("A", inherits="ofl_entry"), self.rec("fdm_pla", "WRONG")],
|
|
[self.rec("ofl_entry", inherits="fdm_pla"), self.rec("fdm_pla", "RIGHT")])
|
|
self.assertEqual(fid, "RIGHT")
|
|
self.assertEqual(entry, "ofl_entry")
|
|
|
|
def test_dead_end_retries_parent_in_ofl(self):
|
|
# The vendor chain dead-ends id-less on a parent that also exists in
|
|
# OFL: the loader re-consults the OFL map for that direct parent.
|
|
fid, _src, entry = self.resolve(
|
|
"A", [self.rec("A", inherits="shared"), self.rec("shared")],
|
|
[self.rec("shared", "OFLID1")])
|
|
self.assertEqual(fid, "OFLID1")
|
|
self.assertEqual(entry, "shared")
|
|
|
|
def test_cycle(self):
|
|
fid, src, _e = self.resolve(
|
|
"A", [self.rec("A", inherits="B"), self.rec("B", inherits="A")], [])
|
|
self.assertEqual((fid, src), (None, "cycle"))
|
|
|
|
def test_dangling_parent(self):
|
|
fid, src, _e = self.resolve("A", [self.rec("A", inherits="nope")], [])
|
|
self.assertEqual((fid, src), (None, "dangling"))
|
|
|
|
def test_missing_id(self):
|
|
fid, src, _e = self.resolve("A", [self.rec("A")], [])
|
|
self.assertEqual((fid, src), (None, "missing"))
|
|
|
|
def test_skip_own_resolves_inherited(self):
|
|
fid, _src, _e = self.resolve(
|
|
"A", [self.rec("A", "OWN", inherits="B"), self.rec("B", "PARENT")], [],
|
|
skip_own=True)
|
|
self.assertEqual(fid, "PARENT")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# reserved namespaces
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestReservedSpaces(unittest.TestCase):
|
|
def test_owners(self):
|
|
self.assertEqual(afi.reserved_space_owner("GFL99"), (True, "BBL"))
|
|
self.assertEqual(afi.reserved_space_owner("QD_X4_PLA"), (True, "Qidi"))
|
|
self.assertEqual(afi.reserved_space_owner("P1234abc"), (True, None))
|
|
self.assertEqual(afi.reserved_space_owner("pAbCdEf1"), (True, None)) # case-insensitive
|
|
self.assertEqual(afi.reserved_space_owner("null"), (True, None))
|
|
self.assertEqual(afi.reserved_space_owner("OFiJYDPC"), (False, None))
|
|
self.assertEqual(afi.reserved_space_owner("P1234abcd"), (False, None)) # 8 hex chars: not the user space
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# checks on synthetic trees
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestChecks(SyntheticTreeCase):
|
|
def test_clean_tree_is_silent(self):
|
|
errors, out = self.t.check()
|
|
self.assertEqual(errors, 0, out)
|
|
self.assertNotIn("[ERROR]", out)
|
|
|
|
def test_check1_unknown_non_of_id(self):
|
|
self.t.write_preset("VendorA", preset("BPLA @base", filament_id="BOGUS_9",
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset("BPLA @P1", inherits="BPLA @base",
|
|
compatible_printers=["P1"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("neither grandfathered in the snapshot", out)
|
|
self.assertIn("BOGUS_9", out)
|
|
|
|
def test_check2_new_claim_needs_snapshot_update(self):
|
|
self.t.write_preset("VendorA", preset("ANEW @P2", inherits="APLA @base",
|
|
compatible_printers=["P2"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn('claim "VendorA/ANEW" is not sanctioned', out)
|
|
self.assertIn("--update-snapshot", out)
|
|
|
|
def test_check2_vanished_claim_is_stability_error(self):
|
|
self.t.remove_preset("VendorA", "APLA @P1")
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("stability", out)
|
|
self.assertIn('"VendorA/APLA"', out)
|
|
|
|
def test_check3_of_id_must_match_mint(self):
|
|
self.t.write_preset("VendorA", preset("BNEW @base", filament_id="OFZZZZZZ",
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset("BNEW @P1", inherits="BNEW @base",
|
|
compatible_printers=["P1"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("does not match its mint", out)
|
|
self.assertIn(afi.generate_filament_id("VendorA", "BNEW"), out)
|
|
|
|
def test_check3_salted_mint_is_accepted(self):
|
|
salted = afi.generate_filament_id("VendorA", "BNEW", salt=3)
|
|
self.t.write_preset("VendorA", preset("BNEW @base", filament_id=salted,
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset("BNEW @P1", inherits="BNEW @base",
|
|
compatible_printers=["P1"]))
|
|
_errors, out = self.t.check() # check 2 still wants a snapshot update
|
|
self.assertNotIn("does not match its mint", out)
|
|
|
|
def test_check4_retired_id_reuse(self):
|
|
afi.write_ledger(self.t.retired, {"retired": {"AX01": ["VendorA/APLA"]}})
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("retired", out)
|
|
self.assertIn("AX01", out)
|
|
|
|
def test_check5_renamed_tuned_generic(self):
|
|
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
|
|
inherits="Generic PLA @System",
|
|
compatible_printers=["P1"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("rename re-exposes the OFL preset", out)
|
|
self.assertIn("Tuned PLA @P1", out)
|
|
|
|
def test_check5_empty_compatible_printers(self):
|
|
self.t.write_preset("VendorA", preset("Generic PLA @P2",
|
|
inherits="Generic PLA @System",
|
|
compatible_printers=[]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("cannot shadow the OFL preset", out)
|
|
|
|
def test_check5_exception_is_grandfathered(self):
|
|
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
|
|
inherits="Generic PLA @System",
|
|
compatible_printers=["P1"]))
|
|
rc, _out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
errors, out = self.t.check()
|
|
self.assertEqual(errors, 0, out)
|
|
|
|
def test_check6_reserved_namespace_claims(self):
|
|
for fid, marker in [("GFX99", "owned by BBL"),
|
|
("QD_X_PLA", "owned by Qidi"),
|
|
("P1a2b3c4", "user-custom"),
|
|
("null", "user-custom")]:
|
|
with self.subTest(fid=fid):
|
|
name = f"R{fid} @base"
|
|
self.t.write_preset("VendorA", preset(name, filament_id=fid,
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset(f"R{fid} @P1", inherits=name,
|
|
compatible_printers=["P1"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("reserved id space", out)
|
|
self.assertIn(marker, out)
|
|
|
|
def test_check7a_instantiated_preset_with_own_key(self):
|
|
self.t.write_preset("VendorA", preset("APLA @P1", filament_id="AX01",
|
|
inherits="APLA @base",
|
|
compatible_printers=["P1 0.4 nozzle"]),
|
|
register=False)
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("declares its own filament_id key", out)
|
|
|
|
def test_check7b_declared_vs_inherited_drift(self):
|
|
self.t.write_preset("VendorA", preset("APLA @P1", filament_id="AX02",
|
|
inherits="APLA @base",
|
|
compatible_printers=["P1 0.4 nozzle"]),
|
|
register=False)
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn('declares filament_id "AX02" but its inherits chain resolves "AX01"', out)
|
|
|
|
def test_check7c_unresolvable_instantiated_filament(self):
|
|
self.t.write_preset("VendorA", preset("DNEW @P1", compatible_printers=["P1"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("resolves no filament_id", out)
|
|
self.assertIn("hard load error", out)
|
|
|
|
def test_missing_snapshot_is_an_error(self):
|
|
os.remove(self.t.snapshot)
|
|
errors, out = self.t.check()
|
|
self.assertEqual(errors, 1)
|
|
self.assertIn("snapshot not found", out)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --update-snapshot
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestUpdateSnapshot(SyntheticTreeCase):
|
|
def test_idempotent_and_deterministic(self):
|
|
with open(self.t.snapshot, "rb") as f:
|
|
first = f.read()
|
|
rc, out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
self.assertIn("nothing changed", out)
|
|
with open(self.t.snapshot, "rb") as f:
|
|
self.assertEqual(f.read(), first)
|
|
self.assertTrue(first.endswith(b"\n"))
|
|
self.assertNotIn(b"\r", first)
|
|
snap = json.loads(first.decode("utf-8"))
|
|
self.assertEqual(list(snap["ids"]), sorted(snap["ids"]))
|
|
self.assertEqual(snap["ids"]["AX01"], ["VendorA/APLA"])
|
|
self.assertEqual(snap["ids"]["OGFL99"],
|
|
["OrcaFilamentLibrary/Generic PLA", "VendorA/Generic PLA"])
|
|
|
|
def test_vanished_id_is_retired(self):
|
|
self.t.remove_preset("VendorA", "APLA @base")
|
|
self.t.remove_preset("VendorA", "APLA @P1")
|
|
rc, out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
self.assertIn("AX01", out)
|
|
retired = load_json_file(self.t.retired)
|
|
self.assertEqual(retired["retired"], {"AX01": ["VendorA/APLA"]})
|
|
snap = load_json_file(self.t.snapshot)
|
|
self.assertNotIn("AX01", snap["ids"])
|
|
# append-only + idempotent: a second run keeps the ledger intact
|
|
rc, out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
self.assertIn("nothing changed", out)
|
|
retired2 = load_json_file(self.t.retired)
|
|
self.assertEqual(retired, retired2)
|
|
# ... and a retired id may never be minted again
|
|
self.assertNotIn(afi.mint_filament_id("VendorA", "APLA",
|
|
set(retired["retired"])), retired["retired"])
|
|
|
|
def test_refuses_new_reserved_namespace_claims(self):
|
|
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="GFX99",
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base",
|
|
compatible_printers=["P1"]))
|
|
with open(self.t.snapshot, "rb") as f:
|
|
before = f.read()
|
|
rc, out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 1)
|
|
self.assertIn("refusing to sanction", out)
|
|
with open(self.t.snapshot, "rb") as f:
|
|
self.assertEqual(f.read(), before) # nothing written on refusal
|
|
rc, _out = self.t.update_snapshot(allow_shared_catalog=True)
|
|
self.assertEqual(rc, 0)
|
|
snap = load_json_file(self.t.snapshot)
|
|
self.assertEqual(snap["ids"]["GFX99"], ["VendorA/CNEW"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# default run: mint + insert
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestAssign(SyntheticTreeCase):
|
|
def test_noop_on_fully_idded_tree(self):
|
|
changed, errors, out = self.t.assign()
|
|
self.assertEqual((changed, errors), (0, 0))
|
|
self.assertIn("nothing to do (0 files changed)", out)
|
|
|
|
def test_mints_into_family_root_and_rootless_member(self):
|
|
self.t.write_preset("VendorA", preset("FNEW @base", instantiation=False))
|
|
self.t.write_preset("VendorA", preset("FNEW @P1", inherits="FNEW @base",
|
|
compatible_printers=["P1"]))
|
|
self.t.write_preset("VendorA", preset("FNEW @P2", inherits="FNEW @base",
|
|
compatible_printers=["P2"]))
|
|
self.t.write_preset("VendorA", preset("GNEW @P1", compatible_printers=["P1"]))
|
|
changed, errors, _out = self.t.assign()
|
|
self.assertEqual(errors, 0)
|
|
self.assertEqual(changed, 2) # one root + one root-less member
|
|
root = load_json_file(self.t.preset_path("VendorA", "FNEW @base"))
|
|
self.assertEqual(root["filament_id"],
|
|
afi.generate_filament_id("VendorA", "FNEW"))
|
|
member = load_json_file(self.t.preset_path("VendorA", "GNEW @P1"))
|
|
self.assertEqual(member["filament_id"],
|
|
afi.generate_filament_id("VendorA", "GNEW"))
|
|
# variants themselves never get the key
|
|
child = load_json_file(self.t.preset_path("VendorA", "FNEW @P1"))
|
|
self.assertNotIn("filament_id", child)
|
|
# idempotent: second run is a no-op
|
|
changed, errors, out = self.t.assign()
|
|
self.assertEqual((changed, errors), (0, 0))
|
|
self.assertIn("nothing to do", out)
|
|
|
|
def test_shared_root_between_families_is_refused(self):
|
|
self.t.write_preset("VendorA", preset("shared_base", instantiation=False))
|
|
self.t.write_preset("VendorA", preset("HNEW @P1", inherits="shared_base",
|
|
compatible_printers=["P1"]))
|
|
self.t.write_preset("VendorA", preset("INEW @P1", inherits="shared_base",
|
|
compatible_printers=["P1"]))
|
|
changed, errors, out = self.t.assign()
|
|
self.assertEqual(changed, 0)
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn("shared with famil", out)
|
|
|
|
|
|
class TestInsertEditing(unittest.TestCase):
|
|
CRLF_TEXT = (
|
|
'{\r\n'
|
|
'\t"type": "filament",\r\n'
|
|
'\t"name": "JNEW @base",\r\n'
|
|
'\t"inherits": "fdm_pla",\r\n'
|
|
'\t"from": "system",\r\n'
|
|
'\t"instantiation": "false",\r\n'
|
|
'\t"filament_type": [\r\n'
|
|
'\t\t"PLA"\r\n'
|
|
'\t]\r\n'
|
|
'}\r\n'
|
|
)
|
|
|
|
def test_insert_before_instantiation_preserves_bytes(self):
|
|
text, n = afi.insert_filament_id(self.CRLF_TEXT, "OFabc123")
|
|
self.assertEqual(n, 1)
|
|
json.loads(text)
|
|
inserted = '\t"filament_id": "OFabc123",\r\n'
|
|
self.assertIn(inserted + '\t"instantiation"', text)
|
|
# every original byte is preserved: removing the inserted line restores
|
|
# the input exactly (CRLF stays CRLF, tabs stay tabs)
|
|
self.assertEqual(text.replace(inserted, "", 1), self.CRLF_TEXT)
|
|
|
|
def test_insert_after_name_when_no_instantiation_line(self):
|
|
lf_text = '{\n "type": "filament",\n "name": "K",\n "from": "system"\n}\n'
|
|
text, n = afi.insert_filament_id(lf_text, "OFabc123")
|
|
self.assertEqual(n, 1)
|
|
json.loads(text)
|
|
self.assertIn('"name": "K",\n "filament_id": "OFabc123",\n', text)
|
|
self.assertNotIn("\r", text)
|
|
|
|
def test_insert_no_anchor_fails(self):
|
|
_text, n = afi.insert_filament_id('{"type": "filament"}', "OFabc123")
|
|
self.assertEqual(n, 0)
|
|
|
|
def test_write_filament_id_keeps_crlf_on_disk(self):
|
|
t = SyntheticTree()
|
|
self.addCleanup(t.cleanup)
|
|
t.add_vendor("VendorA", [])
|
|
path = t.preset_path("VendorA", "JNEW @base")
|
|
with open(path, "wb") as f:
|
|
f.write(self.CRLF_TEXT.encode("utf-8"))
|
|
t.add_to_index("VendorA", "JNEW @base")
|
|
afi.write_filament_id(path, "OFabc123")
|
|
with open(path, "rb") as f:
|
|
raw = f.read()
|
|
self.assertEqual(raw.count(b"\n"), raw.count(b"\r\n")) # still CRLF-only
|
|
self.assertEqual(
|
|
raw.replace(b'\t"filament_id": "OFabc123",\r\n', b"", 1),
|
|
self.CRLF_TEXT.encode("utf-8"))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# the real tree
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@unittest.skipUnless(os.path.isdir(REAL_PROFILES), "resources/profiles not present")
|
|
class TestRealTree(unittest.TestCase):
|
|
def test_shipped_snapshot_matches_tree(self):
|
|
buf = io.StringIO()
|
|
with contextlib.redirect_stdout(buf):
|
|
errors = afi.check_filament_ids(REAL_PROFILES)
|
|
self.assertEqual(errors, 0, buf.getvalue())
|
|
|
|
def test_every_instantiated_filament_resolves_an_id(self):
|
|
analysis = afi.analyze_tree(REAL_PROFILES)
|
|
self.assertEqual(analysis["missing_effective"], [])
|
|
self.assertEqual(analysis["read_errors"], [])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# review-fix regressions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestReviewFixes(SyntheticTreeCase):
|
|
def test_update_snapshot_refuses_retired_id_reuse(self):
|
|
fid = afi.generate_filament_id("VendorA", "LONER")
|
|
self.t.write_preset("VendorA", preset("LONER @base", filament_id=fid,
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset("LONER @P1", inherits="LONER @base",
|
|
compatible_printers=["P1 0.4 nozzle"]))
|
|
rc, _ = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
self.t.remove_preset("VendorA", "LONER @base")
|
|
self.t.remove_preset("VendorA", "LONER @P1")
|
|
rc, out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0, out)
|
|
self.assertIn(fid, afi.load_retired(self.t.retired))
|
|
# Resurrect the same id: --update-snapshot must refuse, not sanction.
|
|
self.t.write_preset("VendorA", preset("LONER @base", filament_id=fid,
|
|
instantiation=False))
|
|
self.t.write_preset("VendorA", preset("LONER @P1", inherits="LONER @base",
|
|
compatible_printers=["P1 0.4 nozzle"]))
|
|
rc, out = self.t.update_snapshot()
|
|
self.assertEqual(rc, 1)
|
|
self.assertIn("retired", out)
|
|
self.assertNotIn(fid, load_json_file(self.t.snapshot)["ids"])
|
|
|
|
def test_check3_skips_of_id_inherited_from_other_vendor(self):
|
|
# Post-Phase-1 world: an OFL generic carries its own minted OF id and a
|
|
# vendor tunes it correctly (same base name, non-empty printers). The
|
|
# new claim must trip only the snapshot gate, never mint conformance.
|
|
fid = afi.generate_filament_id(OFL, "Generic PLA Matte")
|
|
self.t.write_preset(OFL, preset("Generic PLA Matte @base", filament_id=fid,
|
|
instantiation=False))
|
|
self.t.write_preset(OFL, preset("Generic PLA Matte @System",
|
|
inherits="Generic PLA Matte @base",
|
|
compatible_printers=[]))
|
|
rc, _ = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
self.t.write_preset("VendorA", preset("Generic PLA Matte @P1",
|
|
inherits="Generic PLA Matte @System",
|
|
compatible_printers=["P1 0.4 nozzle"]))
|
|
errors, out = self.t.check()
|
|
self.assertNotIn("does not match its mint", out)
|
|
self.assertIn("not sanctioned", out)
|
|
self.assertEqual(errors, 1, out)
|
|
# After sanctioning the claim the tree is fully green again.
|
|
rc, _ = self.t.update_snapshot()
|
|
self.assertEqual(rc, 0)
|
|
errors, out = self.t.check()
|
|
self.assertEqual(errors, 0, out)
|
|
|
|
def test_check7c_prints_expected_mint(self):
|
|
self.t.write_preset("VendorA", preset("Orphan PLA @P1",
|
|
compatible_printers=["P1 0.4 nozzle"]))
|
|
errors, out = self.t.check()
|
|
self.assertGreater(errors, 0)
|
|
self.assertIn(afi.generate_filament_id("VendorA", "Orphan PLA"), out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|