Stop requiring a filament id snapshot update when filaments change

This commit is contained in:
SoftFever
2026-09-18 11:20:57 +08:00
parent 52f4c68c41
commit a77209af8f
13 changed files with 156 additions and 8454 deletions
+2 -3
View File
@@ -38,8 +38,7 @@
under emulation on ARM64.
.PARAMETER ProfilesDir
Profile tree to validate (default: resources\profiles). profile_tool always looks at the
tree next to the script, so this only redirects the validator checks.
Profile tree to validate (default: resources\profiles).
.PARAMETER Vendor
Check only this vendor, named after its <Vendor>.json (e.g. "Co Print"). validate_custom is
@@ -440,7 +439,7 @@ function Expand-VendorPresets([string] $Zip, [string] $Tree, [string] $Prefix) {
$CheckBodies = @{
profile_tool = {
Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check') + $VendorPyArgs)
Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check', '--profiles', $ProfilesDir) + $VendorPyArgs)
}
validate_system = {
+2 -4
View File
@@ -82,9 +82,7 @@ Options:
Note: profile_tool is the only check that is not the validator binary; it makes the static
checks the validator cannot, because the validator loads the tree the way the slicer does
and so never sees a profile no <vendor>.json indexes, a preset name two files claim, or a
file normalize and update-index would still rewrite. It always looks at the tree next to
the script (<repo>/resources/profiles); --profiles only redirects the validator checks,
because validating another tree's ids needs that tree's own filament_id snapshot too.
file normalize and update-index would still rewrite.
Note: --vendor narrows validate_custom too, by keeping only that vendor's presets in each
fixture tree. The one check it cannot narrow is validate_slice for a vendor that ships no
@@ -366,7 +364,7 @@ resolve_validator() {
# ---------------------------------------------------------------------------- checks
check_profile_tool() {
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --vendor "${VENDOR}"
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --profiles "${PROFILES_DIR}" --vendor "${VENDOR}"
}
check_validate_system() {
File diff suppressed because it is too large Load Diff
+33 -243
View File
@@ -10,23 +10,19 @@ commands:
normalize rewrite profile files into their canonical shape
trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json
update-snapshot re-record scripts/filament_id_snapshot.json
options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
(every command but update-snapshot)
--profile-type TYPE one of machine_model/process/filament/machine; repeatable
(normalize, trim, update-index)
--dry-run report what would change and write nothing (every command
that writes)
--profiles DIR act on another profile tree (default: resources/profiles);
check and update-snapshot then need --snapshot PATH too,
since the snapshot describes resources/profiles alone
--profiles DIR act on another profile tree (default: resources/profiles)
After adding, renaming or deleting profile files, run:
normalize -> update-index -> generate-id -> update-snapshot -> check
normalize -> update-index -> generate-id -> check
normalize supplies missing types; update-index registers presets before id
generation. update-snapshot is needed when filament ids or claims change.
generation.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
@@ -55,8 +51,8 @@ filament_id policy (see docs/HLSD/filament_id.md):
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
8 chars total, which satisfies the AMS length limit. Nobody invents ids by
hand, and nothing but the triple feeds the mint — not the rest of the tree,
not the snapshot. Two products whose triples mint one id (a base62
hand, and nothing but the triple feeds the mint — not the rest of the tree.
Two products whose triples mint one id (a base62
collision; odds ~1e-5 over the whole tree) is an error --check reports and
--generate refuses to write; the remedy is a rename so the triples differ,
never a salted or hand-picked second id.
@@ -70,12 +66,6 @@ filament_id policy (see docs/HLSD/filament_id.md):
the app applies at the printer boundary), the QD_* ids a Qidi box composes at
runtime, and the P+7-hex ids CreatePresetsDialog.cpp gives user-created
filaments all fail the format rule like any other stray value.
* scripts/filament_id_snapshot.json is the sanctioned-state snapshot: one
entry per id, carrying the product triple it is minted from and the
"Vendor/Filament" presets claiming it. It must exactly equal the tree-derived
state at all times, so any id/claim/triple change shows up as a reviewable
diff to that file (the maintainer gate). It sanctions state, never
exceptions: no check consults it to excuse a preset from the rules above.
setting_id policy (see AGENTS.md "Critical Constraints"):
* setting_id is a PRESET id, a pure function of the preset's identity:
@@ -123,7 +113,6 @@ FILAMENT_ID_LENGTH = 6 # base62 digits after the "OF" prefix -> 8 chars total
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
PROFILES_DIR = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "resources", "profiles"))
SNAPSHOT_PATH = os.path.join(SCRIPTS_DIR, "filament_id_snapshot.json")
# The single source of truth for the map path; update_bambu_filament_ids.py
# imports this rather than recomputing it.
BAMBU_MAP_PATH = os.path.normpath(
@@ -193,7 +182,6 @@ _JSON_STR = r'"(?:[^"\\]|\\.)*"'
GENERATE_CMD = "python scripts/orca_profile_tool.py generate-id"
SETTING_ID_CMD = '"python scripts/orca_profile_tool.py generate-id --setting-id"'
UPDATE_HINT = 'run "python scripts/orca_profile_tool.py update-snapshot" and commit the diff for maintainer review'
BAMBU_MAP_HINT = 'regenerate the map with "python scripts/update_bambu_filament_ids.py" and commit the diff for maintainer review'
NORMALIZE_HINT = 'try "python scripts/orca_profile_tool.py normalize" to fix common issues automatically'
@@ -246,8 +234,8 @@ def _base62_tail(n, length):
"""The low `length` base62 digits of n, most-significant first.
The shared tail of both id rules. Its output bytes are pinned by the C++
golden vectors (tests/libslic3r/test_preset_setting_id.cpp) and by the
filament_id snapshot — never change it.
golden vectors (tests/libslic3r/test_preset_setting_id.cpp) and by every
filament_id in the tree — never change it.
"""
digits = []
for _ in range(length):
@@ -483,9 +471,8 @@ def resolve_triple(name, filaments, ofl_filaments):
def analyze_tree(profiles_dir):
"""Load every vendor bundle and derive the full filament_id state.
Returns a dict with the tree-derived snapshot sections plus the working data
the checks and the assign pass need. All claims are "Vendor/Filament" strings
over INSTANTIATED system filaments, tree-wide including OFL and BBL.
Returns a dict of the tree-derived state the checks and the assign pass need,
tree-wide including OFL and BBL.
"""
profiles_dir = str(profiles_dir)
vendor_names = list_vendor_names(profiles_dir)
@@ -507,11 +494,6 @@ def analyze_tree(profiles_dir):
rec["id_source"] = src
vendors[vendor] = filaments
# id -> set of "Vendor/Filament" claims over instantiated presets. Every id
# occurring in the tree is a key; ids only ever DECLARED (e.g. on a root
# none of whose descendants instantiate) keep an empty claim list, so that
# the snapshot exactly equals the tree-derived state.
ids = {}
vendor_ids = {} # vendor -> set of ids occurring there (declared or effective)
declared_ids = {} # vendor -> set of ids DECLARED in that vendor's own files
missing_effective = [] # (vendor, name, file) instantiated presets resolving no id
@@ -532,7 +514,6 @@ def analyze_tree(profiles_dir):
fid = rec["filament_id"]
occurring.add(fid)
declared_ids.setdefault(vendor, set()).add(fid)
ids.setdefault(fid, set())
declarer_triples.append((vendor, rec, fid, triple))
triples.setdefault(fid, set()).add(triple)
filament_triples.setdefault(
@@ -545,11 +526,10 @@ def analyze_tree(profiles_dir):
missing_effective.append((vendor, rec["name"], rec["file"]))
continue
occurring.add(eff)
ids.setdefault(eff, set()).add(f"{vendor}/{base_name(rec['name'])}")
if not rec.get("filament_id") and OF_ID_RE.match(eff):
inherited.append((vendor, rec, eff, triple))
# Cross-bundle triple divergence (check 4, warning only): the same filament
# Cross-bundle triple divergence (check 3, warning only): the same filament
# name declared in several bundles with different triples cannot converge
# on one id until the divergence is fixed.
name_bundles = {}
@@ -564,7 +544,6 @@ def analyze_tree(profiles_dir):
return {
"vendors": vendors,
"read_errors": read_errors,
"ids": {fid: sorted(claims) for fid, claims in ids.items()},
"vendor_ids": vendor_ids,
"declared_ids": declared_ids,
"missing_effective": sorted(missing_effective),
@@ -578,57 +557,16 @@ def analyze_tree(profiles_dir):
}
# ---------------------------------------------------------------------------
# Snapshot IO
# ---------------------------------------------------------------------------
def snapshot_from_analysis(analysis):
"""One entry per id, in id order: the product triple it is minted from and
the "Vendor/Filament" claims on it. Requires exactly one declared triple per
id (update_snapshot refuses any other state; check 3 rejects it anyway)."""
ids = {}
for fid, claims in sorted(analysis["ids"].items()):
[(vendor, ftype, filament_name)] = analysis["triples"][fid]
ids[fid] = {"filaments": sorted(claims), "name": filament_name,
"filament_type": ftype, "filament_vendor": vendor}
return {"ids": ids}
def snapshot_triple(entry):
return [entry["filament_vendor"], entry["filament_type"], entry["name"]]
def load_snapshot(path):
"""Return the snapshot dict, or None when the file does not exist."""
if not os.path.exists(path):
return None
data = load_json(path)
data.setdefault("ids", {})
return data
def write_snapshot(path, obj):
"""Deterministic serialization: snapshot_from_analysis order, indent 1, LF,
trailing newline."""
with open(path, "w", encoding="utf-8", newline="\n") as f:
json.dump(obj, f, indent=1, ensure_ascii=False)
f.write("\n")
# ---------------------------------------------------------------------------
# filament_id validation
# ---------------------------------------------------------------------------
def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
map_path=BAMBU_MAP_PATH):
def check_filament_ids(profiles_dir=PROFILES_DIR, map_path=BAMBU_MAP_PATH):
"""Validate filament_id state across every vendor. Returns the error count.
1. Format: every id occurring in the tree (declared or effective) must
match ^OF[0-9A-Za-z]{6}$. No exceptions: not the snapshot, not BBL.
2. Snapshot equality, both directions: every id in the tree, the filaments
claiming it and the triple its declarers resolve must equal the snapshot
entry exactly (the snapshot diff is the maintainer gate).
3. Identity: the id is a function of the triple alone, and there is no
match ^OF[0-9A-Za-z]{6}$. No exceptions, not even BBL.
2. Identity: the id is a function of the triple alone, and there is no
second acceptable value. (a) A declared id must equal the one id the
declarer's own triple mints; (b) the id an instantiated preset inherits
must equal the one ITS own triple mints — how it inherits it (a root, a
@@ -636,31 +574,22 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
filament resolves an effective id at all (an id-less one is a hard load
error in C++); (d) no two products mint one id (a base62 collision,
resolved by renaming one of them).
4. Triple integrity: (a) every declarer resolves non-empty filament_vendor
3. Triple integrity: (a) every declarer resolves non-empty filament_vendor
and filament_type; (b) declarers of one (bundle, filament) resolve
identical triples; cross-bundle divergence on the same filament name is a
warning only.
5. Bambu catalog map: resources/printers/bambu_filament_ids.json must parse,
4. Bambu catalog map: resources/printers/bambu_filament_ids.json must parse,
carry source/bambustudio_commit/generated, key only OF-format ids, map
each Bambu id at most once, and for every row whose key the tree claims,
the tree's triple for that id must equal the row's (vendor, type, name).
Nothing is grandfathered: the snapshot sanctions state, never exceptions.
"""
_utf8_console()
errors = 0
analysis = analyze_tree(profiles_dir)
snapshot = load_snapshot(snapshot_path)
if snapshot is None:
print_error(f"filament_id snapshot not found at {snapshot_path}; {UPDATE_HINT}")
return 1
for msg in analysis["read_errors"]:
print_error(msg)
errors += 1
snap_ids = snapshot["ids"]
tree_ids = analysis["ids"]
# -- 1. format ----------------------------------------------------------
for vendor in sorted(analysis["vendor_ids"]):
for fid in sorted(analysis["vendor_ids"][vendor]):
@@ -671,47 +600,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'filament ids must come from "{GENERATE_CMD}"')
errors += 1
# -- 2. snapshot equality (both directions) -----------------------------
tree_triples = analysis["triples"]
for fid in sorted(tree_ids):
entry = snap_ids.get(fid)
if entry is None:
print_error(
f'filament_id "{fid}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
continue
for claim in tree_ids[fid]:
if claim not in entry["filaments"]:
print_error(
f'filament_id "{fid}" claim "{claim}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
# Every tree id has at least one declarer; the snapshot records one
# triple per id, so a divergent declarer is a mismatch in both directions.
sanctioned = snapshot_triple(entry)
for t in tree_triples[fid]:
if t != sanctioned:
print_error(
f'filament_id "{fid}" triple "{"/".join(t)}" is not sanctioned by '
f'scripts/filament_id_snapshot.json, which records '
f'"{"/".join(sanctioned)}"; {UPDATE_HINT}')
errors += 1
for fid in sorted(snap_ids):
if fid not in tree_ids:
print_error(
f'filament_id stability: snapshot id "{fid}" vanished from the tree; '
f"{UPDATE_HINT}")
errors += 1
continue
for claim in snap_ids[fid]["filaments"]:
if claim not in tree_ids[fid]:
print_error(
f'filament_id stability: snapshot claim "{claim}" of id "{fid}" '
f"vanished from the tree; {UPDATE_HINT}")
errors += 1
# -- 3. identity: the id is a function of the triple alone ---------------
# -- 2. identity: the id is a function of the triple alone ---------------
# One triple, one id: a declaration must carry exactly the mint of its
# triple, and there is no second acceptable value — not a salt, not a
# hand-picked one, not whatever another preset of the product carries. Two
@@ -727,10 +616,9 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
f'not match the mint of its triple "{"/".join(triple)}": expected '
f'"{want}"; paste the expected id, or fix the triple and run '
f'"{GENERATE_CMD} --vendor {vendor}" (preview with --dry-run), then '
f"--update-snapshot")
f'"{GENERATE_CMD} --vendor {vendor}" (preview with --dry-run)')
errors += 1
# (3b) An inherited id is held to the same single value, and every preset
# (2b) An inherited id is held to the same single value, and every preset
# missing it is listed — a variant under a wrong root as much as a preset
# riding another product's root. Nothing is folded into the declarer's
# error: the report names each preset whose id is wrong.
@@ -755,7 +643,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'run "{GENERATE_CMD}" (expected id for filament '
f'"{vendor}/{base_name(name)}": "{expected}")')
errors += 1
# (3d) The mint is injective over the tree's products, or two of them are
# (2d) The mint is injective over the tree's products, or two of them are
# indistinguishable to every device that matches on the id.
for fid, ts in sorted(analysis["collisions"].items()):
print_error(
@@ -764,7 +652,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f"of them so their triples differ")
errors += 1
# -- 4. triple integrity ---------------------------------------------------
# -- 3. triple integrity ---------------------------------------------------
for vendor, rec, fid, triple in sorted(
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
if triple[0] and triple[1]:
@@ -797,7 +685,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f"({detail}); bundles of one product converge on one id only once "
f"their triples agree")
# -- 6. Bambu catalog map --------------------------------------------------
# -- 4. Bambu catalog map --------------------------------------------------
try:
bambu_map = load_json(map_path)
if not isinstance(bambu_map, dict):
@@ -838,7 +726,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
errors += 1
else:
bambu_id_owners[bambu_id] = fid
claimed = tree_triples.get(fid)
claimed = analysis["triples"].get(fid)
if not claimed:
continue # a product BambuStudio ships that the tree does not (yet)
row_triple = [row.get("vendor", ""), row.get("type", ""), row.get("name", "")]
@@ -1404,7 +1292,7 @@ def check_normalized(profiles_dir, vendor):
# check
# ---------------------------------------------------------------------------
def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSHOT_PATH):
def check_profiles(profiles_dir=PROFILES_DIR, vendors=None):
"""Validate the whole profile tree. Returns the error count.
The per-vendor checks honour `vendors`; the setting_id and filament_id checks are
@@ -1468,7 +1356,7 @@ def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSH
# Cross-vendor checks: setting_id uniqueness and the whole filament_id state,
# both validated over the entire tree regardless of --vendor.
errors_found += check_setting_id_uniqueness(profiles_dir)
errors_found += check_filament_ids(profiles_dir, snapshot_path)
errors_found += check_filament_ids(profiles_dir)
print("\n==================== SUMMARY ====================")
print_info(f"Checked vendors : {len(checked)}")
@@ -1486,69 +1374,6 @@ def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSH
return errors_found
# ---------------------------------------------------------------------------
# update-snapshot
# ---------------------------------------------------------------------------
def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, dry_run=False):
"""Regenerate the snapshot from the tree.
Refuses to sanction a tree it could not read whole, and an id declared under
more than one triple: neither state can be recorded truthfully, so writing it
would only hide the mistake until CI. It does not judge the ids themselves —
the snapshot records state and check judges it, so an id that is not a mint
lands in the diff and fails check 1.
Idempotent: a second run over an unchanged tree changes nothing. Returns 0
on success.
"""
analysis = analyze_tree(profiles_dir)
# A tree that could not be read whole cannot be sanctioned: the snapshot
# would silently drop the unreadable bundle's ids and claims, and the diff
# would read as a deliberate removal.
refusals = len(analysis["read_errors"])
for msg in analysis["read_errors"]:
print_error(msg)
for fid, ts in sorted(analysis["triples"].items()):
if len(ts) > 1:
print_error(
f'refusing to sanction filament_id "{fid}": declared under {len(ts)} '
f'triples ({"; ".join("/".join(t) for t in ts)}); one id names one '
f"product (check 3)")
refusals += 1
if refusals:
return 1
new_snap = snapshot_from_analysis(analysis)
old_snap = load_snapshot(snapshot_path)
old_ids = old_snap["ids"] if old_snap else {}
# Diff summary.
added_ids = sorted(set(new_snap["ids"]) - set(old_ids))
removed_ids = sorted(set(old_ids) - set(new_snap["ids"]))
added_claims = sum(
len(set(entry["filaments"]) - set(old_ids.get(fid, {}).get("filaments", [])))
for fid, entry in new_snap["ids"].items())
removed_claims = sum(
len(set(entry["filaments"]) - set(new_snap["ids"].get(fid, {}).get("filaments", [])))
for fid, entry in old_ids.items())
changed = new_snap != (old_snap or {"ids": {}})
if changed and not dry_run:
write_snapshot(snapshot_path, new_snap)
print_info(f"snapshot ids : {len(new_snap['ids'])} (+{len(added_ids)} / -{len(removed_ids)})")
print_info(f"claims added : {added_claims}")
print_info(f"claims removed : {removed_claims}")
if changed and dry_run:
print_success(f"dry run: {snapshot_path} would be rewritten; nothing written")
elif changed:
print_success(f"snapshot written to {snapshot_path}")
else:
print_success("snapshot already up to date; nothing changed")
return 0
# ---------------------------------------------------------------------------
# Byte-preserving profile edits
# ---------------------------------------------------------------------------
@@ -1692,9 +1517,9 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False
* an instantiated filament that resolves no id at all gets one inserted
into its root(s): the id-less presets of the SAME filament its members
inherit, or the member itself (a parent of another filament cannot carry
this filament's id — check 3).
this filament's id — check 2).
A declaration is left alone exactly when it already equals the one id its
triple mints (check 3). Two products minting one id (check 3d) are reported
triple mints (check 2). Two products minting one id (check 2d) are reported
and left unwritten: nothing salts past a collision, a rename resolves it.
`vendors` restricts what is WRITTEN; the id is a function of the triple
@@ -1702,9 +1527,7 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False
reports whatever it was not allowed to touch. `changed_paths`, when a set is
passed, collects the files that changed. A file whose layout offers no
anchor for the edit is reported and counted as an error, so one odd profile
cannot abort the pass over all the others. Never reads or touches the
snapshot — run --update-snapshot afterwards and review the diff. Returns
(files_changed, errors).
cannot abort the pass over all the others. Returns (files_changed, errors).
"""
_utf8_console()
analysis = analyze_tree(profiles_dir)
@@ -1959,9 +1782,9 @@ def run_generate_id(profiles_dir, vendors, filament_id, setting_id, dry_run):
do_filament = filament_id or not setting_id
do_setting = setting_id or not filament_id
changed = set() # one file the two passes both touch is still one file
filament_files = errors = 0
errors = 0
if do_filament:
filament_files, e = generate_filament_ids(profiles_dir, vendors, dry_run, changed)
_n, e = generate_filament_ids(profiles_dir, vendors, dry_run, changed)
errors += e
if do_setting:
_n, e = generate_setting_ids(profiles_dir, vendors, dry_run, changed)
@@ -1973,11 +1796,6 @@ def run_generate_id(profiles_dir, vendors, filament_id, setting_id, dry_run):
print_error(f"{summary}; {errors} error(s)")
else:
print_success(summary)
if filament_files and not dry_run:
# A filament_id write may or may not move the sanctioned state (an id repaired
# back to the value the snapshot already records does not), so regenerate and
# let the diff - empty or not - say.
print_warning(f"now {UPDATE_HINT}")
return 1 if errors else 0
@@ -2439,13 +2257,11 @@ examples:
preview exactly that; writes nothing
orca_profile_tool.py generate-id --setting-id --vendor Elegoo
setting_id only, and only in that bundle
orca_profile_tool.py update-snapshot
re-record the sanctioned filament_id state after a generate-id run
after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> update-snapshot -> check
normalize -> update-index -> generate-id -> check
normalize supplies missing types; update-index registers presets before id
generation. update-snapshot is needed when filament ids or claims change.
generation.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
"""
@@ -2473,12 +2289,6 @@ def build_parser():
dry_run_opt.add_argument("--dry-run", "--dryrun", dest="dry_run", action="store_true",
help="report what would change and write nothing")
snapshot_opt = argparse.ArgumentParser(add_help=False)
snapshot_opt.add_argument("--snapshot", default=None, metavar="PATH",
help="the sanctioned filament_id state of that tree "
"(default: scripts/filament_id_snapshot.json, which "
"describes resources/profiles and no other tree)")
parser = argparse.ArgumentParser(
prog="orca_profile_tool.py", allow_abbrev=False,
formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -2496,7 +2306,7 @@ def build_parser():
allow_abbrev=False, formatter_class=argparse.RawDescriptionHelpFormatter)
add(
"check", [vendor_opt, snapshot_opt, profiles_opt],
"check", [vendor_opt, profiles_opt],
"validate the whole profile tree -- what CI runs",
"Validate the whole profile tree: preset name uniqueness, index coverage\n"
"both ways, compatible_printers, default-material references, obsolete,\n"
@@ -2560,11 +2370,6 @@ def build_parser():
"Use trim only for deliberate unindexed-file cleanup, previewed with\n"
"--dry-run; it can also delete newly authored presets.")
add("update-snapshot", [dry_run_opt, snapshot_opt, profiles_opt],
"re-record scripts/filament_id_snapshot.json",
"Re-record the sanctioned filament_id state after a generate-id run, and\n"
"commit the diff for maintainer review.")
return parser
@@ -2590,29 +2395,14 @@ def main(argv=None):
return 1
profile_types = tuple(getattr(args, "profile_type", []) or ()) or None
snapshot_path = getattr(args, "snapshot", None)
if snapshot_path is None:
if (args.command in ("check", "update-snapshot")
and os.path.abspath(profiles_dir) != os.path.abspath(PROFILES_DIR)):
# The repo snapshot is the sanctioned state of resources/profiles alone:
# checking another tree against it is meaningless, and re-recording one
# into it would overwrite the tracked file with a foreign tree's state.
parser.error(f"{args.command} reads and writes the sanctioned state of the "
f"tree it is given, so --profiles needs --snapshot PATH for "
f"that tree too")
snapshot_path = SNAPSHOT_PATH
if args.command == "check":
errors = check_profiles(profiles_dir, vendors, snapshot_path)
errors = check_profiles(profiles_dir, vendors)
return 1 if errors else 0
if args.command == "generate-id":
return run_generate_id(profiles_dir, vendors, args.filament_id, args.setting_id,
args.dry_run)
if args.command == "update-snapshot":
return update_snapshot(profiles_dir, snapshot_path, dry_run=args.dry_run)
if args.command == "normalize":
_changed, errors = normalize_profiles(profiles_dir, vendors, profile_types,
force=args.force, dry_run=args.dry_run)
+49 -269
View File
@@ -55,13 +55,12 @@ def preset(name, filament_id=None, inherits=None, instantiation=True,
class SyntheticTree:
"""A throwaway resources/profiles-shaped directory plus a snapshot path."""
"""A throwaway resources/profiles-shaped directory."""
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")
def cleanup(self):
shutil.rmtree(self.dir, ignore_errors=True)
@@ -110,16 +109,6 @@ class SyntheticTree:
with open(idx_path, "w", encoding="utf-8") as f:
json.dump(index, f, indent=4, ensure_ascii=False)
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)
def bytes_map(self):
"""{relative path -> file bytes} over every .json in the tree."""
raw = {}
@@ -135,17 +124,11 @@ class SyntheticTree:
# -- pipeline wrappers ---------------------------------------------------
def update_snapshot(self, dry_run=False):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.update_snapshot(self.profiles, self.snapshot, dry_run)
return rc, buf.getvalue()
def check(self, map_path=None):
buf = io.StringIO()
kwargs = {} if map_path is None else {"map_path": map_path}
with contextlib.redirect_stdout(buf):
errors = afi.check_filament_ids(self.profiles, self.snapshot, **kwargs)
errors = afi.check_filament_ids(self.profiles, **kwargs)
return errors, buf.getvalue()
# assign() and remint() are the same one pass over the tree — every filament
@@ -167,8 +150,6 @@ class SyntheticTree:
def cli(self, *argv):
"""Run main() against this tree, capturing stdout."""
flags = [*argv, "--profiles", self.profiles]
if argv and argv[0] in ("check", "update-snapshot"):
flags += ["--snapshot", self.snapshot]
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.main(flags)
@@ -178,10 +159,10 @@ class SyntheticTree:
def make_clean_tree(apla_id="AX01", generic_id="OGFL99"):
"""Baseline tree: OFL base+generic, a vendor filament, a clean tuned generic.
apla_id/generic_id default to arbitrary non-OF placeholders (sanctioned by
the snapshot below) since most tests only need "already assigned, don't
touch" and never run the checks. TestAssign and the check tests pass real
OF-format ids instead (OfCleanTreeCase).
apla_id/generic_id default to arbitrary non-OF placeholders since most tests
only need "already assigned, don't touch" and never run the checks.
TestAssign and the check tests pass real OF-format ids instead
(OfCleanTreeCase).
"""
t = SyntheticTree()
t.add_vendor(OFL, [
@@ -199,8 +180,6 @@ def make_clean_tree(apla_id="AX01", generic_id="OGFL99"):
preset("Generic PLA @P1", inherits="Generic PLA @System",
compatible_printers=["P1 0.4 nozzle"]),
])
rc, _out = t.update_snapshot()
assert rc == 0
return t
@@ -212,10 +191,9 @@ class SyntheticTreeCase(unittest.TestCase):
class OfCleanTreeCase(unittest.TestCase):
"""Like SyntheticTreeCase, but the baseline filament/generic already carry
real OF-format ids (check 1 now rejects "AX01"/"OGFL99" unconditionally,
with no snapshot exemption), so an otherwise-untouched tree still passes
check_filament_ids. Tests that specifically need a non-OF baseline to
remint (TestRemint, TestUpdateSnapshot) keep using SyntheticTreeCase
real OF-format ids (check 1 rejects "AX01"/"OGFL99"), so an
otherwise-untouched tree passes check_filament_ids. Tests that specifically
need a non-OF baseline to remint (TestRemint) keep using SyntheticTreeCase
instead.
"""
def setUp(self):
@@ -231,7 +209,7 @@ class OfCleanTreeCase(unittest.TestCase):
class TestMint(unittest.TestCase):
def test_namespace_literal(self):
# Frozen: derived from the setting_id namespace; baked into the snapshot.
# Frozen: derived from the setting_id namespace; baked into every shipped id.
self.assertEqual(afi.FILAMENT_ID_NAMESPACE,
uuid.UUID("c4d3ff49-4c32-5534-a3e3-00894157ab97"))
@@ -450,22 +428,10 @@ class TestChecks(OfCleanTreeCase):
self.assertIn('is not a minted "OF" id', 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_check2_triple_change_needs_snapshot_update(self):
def test_check2_triple_change_needs_a_remint(self):
# Correcting a triple changes the product's identity: the old id is no
# longer its mint, reported on the root and again under the variant
# inheriting it, until generate-id re-mints it.
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("APLA @base", filament_id=apla_id,
instantiation=False,
@@ -473,26 +439,15 @@ class TestChecks(OfCleanTreeCase):
filament_type="PETG"),
register=False)
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('triple "AVendor/PETG/APLA" is not sanctioned', out)
self.assertIn('which records "AVendor/PLA/APLA"', out)
# Sanctioning the new triple is not enough: the old id is no longer its
# mint (check 3, nothing grandfathered) — the identity fix is a re-mint,
# reported on the root and again under the variant inheriting it.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 2, out)
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"APLA @P1" (VendorA/filament/APLA @P1.json) inherits filament_id', out)
_changed, errors, out = self.t.remint(["VendorA"])
self.assertEqual(errors, 0, out)
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check3_of_id_must_match_triple_mint(self):
def test_check2_of_id_must_match_triple_mint(self):
self.t.write_preset("VendorA", preset("BNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="BV", filament_type="PLA"))
@@ -503,39 +458,20 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("does not match the mint of its triple", out)
self.assertIn(afi.generate_filament_id("BV", "PLA", "BNEW"), out)
def test_check3_no_grandfathering_of_a_wrong_declaration(self):
# Sanctioning the tree does not excuse a declaration from its mint.
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="CV", filament_type="PLA"))
self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base",
compatible_printers=["P1"]))
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 2, out) # the declaration, and the variant inheriting it
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"CNEW @P1" (VendorA/filament/CNEW @P1.json) inherits filament_id', out)
def test_check3_inherited_id_must_be_the_mint_of_own_triple(self):
def test_check2_inherited_id_must_be_the_mint_of_own_triple(self):
# A preset of another filament inheriting APLA's root takes APLA's id,
# which is not the mint of ITS triple (AVendor/PLA/Tuned PLA).
self.t.write_preset("VendorA", preset("Tuned PLA @P1", inherits="APLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertEqual(errors, 1, out)
self.assertIn('"Tuned PLA @P1" (VendorA/filament/Tuned PLA @P1.json) inherits '
'filament_id "%s"' % afi.generate_filament_id("AVendor", "PLA", "APLA"),
out)
self.assertIn('mints "%s"' % afi.generate_filament_id("AVendor", "PLA", "Tuned PLA"),
out)
# ... and sanctioning the tree does not excuse it either.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
def test_check3_lists_every_preset_inheriting_a_wrong_id(self):
def test_check2_lists_every_preset_inheriting_a_wrong_id(self):
# A wrong declaration is reported under every preset inheriting it, its
# own product's variant and another product alike: each one's effective
# id is not the mint of its own triple, and each is listed. Nothing is
@@ -552,11 +488,10 @@ class TestChecks(OfCleanTreeCase):
self.assertIn('"DNEW @P1" (VendorA/filament/DNEW @P1.json) inherits filament_id', out)
self.assertIn('"Other DNEW @P1" (VendorA/filament/Other DNEW @P1.json) inherits '
'filament_id', out)
# The unsanctioned id (check 2), the declaration (3a), and both presets
# inheriting it (3b).
self.assertEqual(errors, 4, out)
# The declaration (2a), and both presets inheriting it (2b).
self.assertEqual(errors, 3, out)
def test_check3_reports_an_inherited_mismatch_even_when_its_own_product_misdeclares_the_id(self):
def test_check2_reports_an_inherited_mismatch_even_when_its_own_product_misdeclares_the_id(self):
# "Tuned PLA @P1" inherits APLA's root, so it carries APLA's id: wrong
# for its own product however the declarations around it are fixed.
# That "Tuned PLA @base" — its own product — misdeclares that same id
@@ -573,11 +508,11 @@ class TestChecks(OfCleanTreeCase):
'not match the mint of its triple', out)
self.assertIn('"Tuned PLA @P1" (VendorA/filament/Tuned PLA @P1.json) inherits '
'filament_id', out)
# The unsanctioned claim and triple (check 2), the declaration (3a) and
# the inherited id (3b): four distinct errors, nothing folded away.
self.assertEqual(errors, 4, out)
# The declaration (2a) and the inherited id (2b): two distinct errors,
# nothing folded away.
self.assertEqual(errors, 2, out)
def test_check3_reports_a_collision_between_two_products(self):
def test_check2_reports_a_collision_between_two_products(self):
# Two products whose triples mint one id is a base62 collision. There
# is no salted or hand-picked second id to fall back on: the check
# names both products, and the remedy is a rename so the triples differ.
@@ -601,13 +536,12 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("V/PLA/X", out)
self.assertIn("W/ABS/Y", out)
# Each declaration is the mint of its own triple, so the collision is
# the only identity error — no product is pushed off its id — and the
# unsanctioned id (check 2) is the only other one.
# the only error — no product is pushed off its id.
self.assertNotIn("does not match the mint", out)
self.assertNotIn("inherits filament_id", out)
self.assertEqual(errors, 2, out)
self.assertEqual(errors, 1, out)
def test_check3_renamed_tuned_generic_is_an_identity_error(self):
def test_check2_renamed_tuned_generic_is_an_identity_error(self):
# Riding the OFL generic under another base name: same rule, same error.
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
inherits="Generic PLA @System",
@@ -617,7 +551,7 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("Tuned PLA @P1", out)
self.assertIn("inherits filament_id", out)
def test_check3_own_key_on_an_instantiated_preset_is_fine(self):
def test_check2_own_key_on_an_instantiated_preset_is_fine(self):
# Where the id comes from is irrelevant: a variant may carry the key.
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("APLA @P1", filament_id=apla_id,
@@ -627,7 +561,7 @@ class TestChecks(OfCleanTreeCase):
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check3_inheriting_a_real_filament_of_another_product_is_fine(self):
def test_check2_inheriting_a_real_filament_of_another_product_is_fine(self):
# A branded product may inherit the OFL generic (an instantiated
# preset) for its settings; it declares its own triple's id.
fid = afi.generate_filament_id("BV", "PLA", "Branded PLA")
@@ -635,8 +569,6 @@ class TestChecks(OfCleanTreeCase):
inherits="Generic PLA @System",
filament_vendor="BV",
compatible_printers=["P1"]))
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
# With a wrong key it is a plain mint mismatch: the parent plays no
@@ -670,22 +602,16 @@ class TestChecks(OfCleanTreeCase):
self.assertIn(f'filament_id "{fid}"', out)
self.assertIn('is not a minted "OF" id', out)
def test_check3c_unresolvable_instantiated_filament(self):
def test_check2c_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)
class TestCheck5(OfCleanTreeCase):
def test_5a_empty_vendor_is_hard_error(self):
class TestCheck3(OfCleanTreeCase):
def test_3a_empty_vendor_is_hard_error(self):
fid = afi.generate_filament_id("", "PLA", "NVPLA")
self.t.write_preset("VendorA", preset("NVPLA @base", filament_id=fid,
instantiation=False,
@@ -693,17 +619,11 @@ class TestCheck5(OfCleanTreeCase):
self.t.write_preset("VendorA", preset("NVPLA @P1", inherits="NVPLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("resolves empty filament_vendor", out)
self.assertIn('filament_vendor "Generic"', out)
# No grandfathering: sanctioning the tree does not silence check 4a.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertIn("resolves empty filament_vendor", out)
self.assertIn('filament_vendor "Generic"', out)
def test_5b_divergent_filament_triples(self):
def test_3b_divergent_filament_triples(self):
id1 = afi.generate_filament_id("MV", "PLA", "MPLA")
id2 = afi.generate_filament_id("MV", "PETG", "MPLA")
self.t.write_preset("VendorA", preset("MPLA @base1", filament_id=id1,
@@ -715,18 +635,12 @@ class TestCheck5(OfCleanTreeCase):
filament_vendor="MV",
filament_type="PETG"))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertEqual(errors, 1, out)
self.assertIn("divergent triples", out)
self.assertIn("MV/PLA/MPLA", out)
self.assertIn("MV/PETG/MPLA", out)
# No grandfathering: sanctioning the tree does not silence check 4b.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertIn("divergent triples", out)
def test_5_cross_bundle_divergence_is_warning_only(self):
def test_3_cross_bundle_divergence_is_warning_only(self):
fid = afi.generate_filament_id("BV", "PETG", "APLA")
self.t.add_vendor("VendorB", [
preset("APLA @base", filament_id=fid, instantiation=False,
@@ -734,8 +648,6 @@ class TestCheck5(OfCleanTreeCase):
preset("APLA @PB", inherits="APLA @base",
compatible_printers=["PB 0.4 nozzle"]),
])
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
self.assertIn("[WARNING]", out)
@@ -743,7 +655,7 @@ class TestCheck5(OfCleanTreeCase):
self.assertIn('"APLA"', out)
class TestCheck6(OfCleanTreeCase):
class TestCheck4(OfCleanTreeCase):
def _write_map(self, rows):
path = os.path.join(self.t.dir, "bambu_filament_ids.json")
ubfi.write_map(path, rows, "testcommit", "2026-09-04")
@@ -845,104 +757,6 @@ class TestCheck6(OfCleanTreeCase):
self.assertIn('declares no "bambu_id"', 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"]) # state only, no exception lists
self.assertEqual(list(snap["ids"]), sorted(snap["ids"]))
self.assertEqual(snap["ids"]["AX01"], {
"filaments": ["VendorA/APLA"], "name": "APLA",
"filament_type": "PLA", "filament_vendor": "AVendor"})
self.assertEqual(snap["ids"]["OGFL99"], {
"filaments": ["OrcaFilamentLibrary/Generic PLA", "VendorA/Generic PLA"],
"name": "Generic PLA", "filament_type": "PLA", "filament_vendor": "Generic"})
# Key order is part of the on-disk format.
self.assertEqual(list(snap["ids"]["AX01"]),
["filaments", "name", "filament_type", "filament_vendor"])
def test_refuses_a_tree_it_could_not_read(self):
# A bundle that does not parse contributes no ids, so sanctioning the
# rest would record the loss as a deliberate removal.
with open(self.t.snapshot, "rb") as f:
before = f.read()
with open(os.path.join(self.t.profiles, "VendorA",
"filament", "APLA @base.json"), "w",
encoding="utf-8") as f:
f.write("{ not json")
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 1, out)
self.assertIn("unreadable filament profile", out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before)
def test_refuses_an_id_declared_under_two_triples(self):
# VendorB re-declares APLA's id for a different product: one id, two
# triples. No single entry can describe it, and check 3 rejects it anyway.
self.t.add_vendor("VendorB", [
preset("BPLA @base", filament_id="AX01", instantiation=False,
filament_vendor="BVendor", filament_type="PLA"),
preset("BPLA @P1", inherits="BPLA @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 filament_id "AX01": declared under 2 triples '
'(AVendor/PLA/APLA; BVendor/PLA/BPLA)', out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before) # nothing written on refusal
def test_records_an_id_it_cannot_defend_and_lets_check_reject_it(self):
# update-snapshot records state, it does not judge ids: a foreign id
# lands in the diff a maintainer reviews, and fails check 1 straight
# after. Sanctioning it does not grandfather it.
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="GFX99",
instantiation=False,
filament_vendor="CV",
filament_type="PLA"))
self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base",
compatible_printers=["P1"]))
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 0, out)
with open(self.t.snapshot, encoding="utf-8") as f:
self.assertIn("GFX99", json.load(f)["ids"])
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('is not a minted "OF" id', out)
def test_dry_run_reports_without_writing(self):
self.t.write_preset("VendorA", preset("ANEW @P2", inherits="APLA @base",
compatible_printers=["P2"]))
with open(self.t.snapshot, "rb") as f:
before = f.read()
rc, out = self.t.update_snapshot(dry_run=True)
self.assertEqual(rc, 0)
self.assertIn("would be rewritten", out)
self.assertIn("claims added : 1", out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before)
# the real run writes exactly what the dry run reported
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 0)
self.assertIn("snapshot written", out)
snap = load_json_file(self.t.snapshot)
self.assertEqual(snap["ids"]["AX01"]["filaments"],
["VendorA/ANEW", "VendorA/APLA"])
# ---------------------------------------------------------------------------
# --generate: one rule for inserts and rewrites alike
# ---------------------------------------------------------------------------
@@ -1026,7 +840,7 @@ class TestAssign(OfCleanTreeCase):
def test_parent_of_another_filament_never_receives_the_key(self):
# Members whose id-less parent belongs to another filament (here one
# parent shared by two filaments) carry the key themselves: the
# parent's own triple would mint a different id (check 3).
# parent's own triple would mint a different id (check 2).
self.t.write_preset("VendorA", preset("shared_base", instantiation=False,
filament_vendor="SV",
filament_type="PLA"))
@@ -1044,8 +858,6 @@ class TestAssign(OfCleanTreeCase):
parent = load_json_file(self.t.preset_path("VendorA", "shared_base"))
self.assertNotIn("filament_id", parent)
# ... and the tree they leave behind passes the identity check.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
@@ -1538,33 +1350,10 @@ class TestCli(unittest.TestCase):
rc = afi.main([])
self.assertEqual(rc, 0)
self.assertIn("usage:", buf.getvalue())
for command in ("check", "generate-id", "normalize", "trim", "update-index",
"update-snapshot"):
for command in ("check", "generate-id", "normalize", "trim", "update-index"):
self.assertIn(command, buf.getvalue())
self.assertEqual(self.t.bytes_map(), before)
def test_another_tree_needs_its_own_snapshot(self):
# --profiles retargets the tree, but the sanctioned state of that tree
# is not the repo snapshot: checking against it is meaningless and
# re-recording into it would overwrite the tracked file.
with open(afi.SNAPSHOT_PATH, "rb") as f:
repo_snapshot = f.read()
for command in ("check", "update-snapshot"):
with self.assertRaises(SystemExit) as caught:
with contextlib.redirect_stderr(io.StringIO()):
afi.main([command, "--profiles", self.t.profiles])
self.assertEqual(caught.exception.code, 2, command)
with open(afi.SNAPSHOT_PATH, "rb") as f:
self.assertEqual(f.read(), repo_snapshot)
# Named explicitly, both commands run against that tree.
rc, out = self.t.cli("update-snapshot")
self.assertEqual(rc, 0, out)
# generate-id never reads the snapshot, so it keeps working without one.
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.main(["generate-id", "--dry-run", "--profiles", self.t.profiles])
self.assertEqual(rc, 0, buf.getvalue())
def test_filament_id_and_setting_id_together_are_rejected(self):
# Each flag's help promises it skips the other kind, so the pair cannot
# quietly mean "both".
@@ -1667,8 +1456,8 @@ class TestCli(unittest.TestCase):
"setting_id", load_json_file(self.t.preset_path("VendorA", name)))
def test_check_returns_1_on_errors(self):
# What CI keys off: check exits nonzero when the tree does not match the
# snapshot it is validated against.
# What CI keys off: check exits nonzero when the tree breaks a rule, here
# the baseline's ids that are not minted.
before = self.t.bytes_map()
rc, out = self.t.cli("check")
self.assertEqual(rc, 1)
@@ -1695,12 +1484,12 @@ class TestCli(unittest.TestCase):
["--check"], # the pre-subcommand flag
["--update-snapshot"], # the pre-subcommand flag
["nonsense"], # not a command
["update-snapshot"], # removed command
["generate-id", "--filament-id", "--setting-id"],
["generate-id", "--snapshot", "x"], # check's option
["check", "--materials"], # removed flag
["check", "--obsolete-keys"], # removed flag
["check", "--snapshot", "x"], # removed flag
["check", "--filament-id"], # generate-id's option
["normalize", "--snapshot", "x"], # not a snapshot command
["normalize", "--profile-type", "nozzle"]): # not a profile type
with self.subTest(argv=argv):
with self.assertRaises(SystemExit) as cm, \
@@ -1716,7 +1505,7 @@ class TestCli(unittest.TestCase):
@unittest.skipUnless(os.path.isdir(REAL_PROFILES), "resources/profiles not present")
class TestRealTree(unittest.TestCase):
def test_shipped_snapshot_matches_tree(self):
def test_shipped_filament_ids_pass(self):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
errors = afi.check_filament_ids(REAL_PROFILES)
@@ -1778,10 +1567,10 @@ class TestReviewFixes(OfCleanTreeCase):
self.assertIn(path, str(caught.exception))
self.assertIn("test edit", str(caught.exception))
def test_check3_skips_of_id_inherited_from_other_vendor(self):
def test_check2_accepts_an_of_id_inherited_from_another_vendor(self):
# An OFL filament 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.
# correctly (same base name, non-empty printers): the id it inherits is
# the mint of its own triple.
fid = afi.generate_filament_id("Generic", "PLA", "Generic PLA Matte")
self.t.write_preset(OFL, preset("Generic PLA Matte @base", filament_id=fid,
instantiation=False,
@@ -1790,22 +1579,13 @@ class TestReviewFixes(OfCleanTreeCase):
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 the 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_check3c_prints_expected_mint(self):
def test_check2c_prints_expected_mint(self):
self.t.write_preset("VendorA", preset("Orphan PLA @P1",
compatible_printers=["P1 0.4 nozzle"],
filament_vendor="OV",
+8 -24
View File
@@ -527,9 +527,7 @@ class TestCheck(TreeCase):
# `check`, now that the per-vendor pass no longer skips it.
self.t.write(apt.OFL, "filament/Stray.json",
{"type": "filament", "name": "Stray"})
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn(f"{apt.OFL}/filament/Stray.json: no {apt.OFL}.json list "
f"references it", out)
@@ -598,9 +596,7 @@ class TestCheck(TreeCase):
self.t.write("V", "filament/A.json", {
"type": "filament", "name": "A", "silent_mode": "0"})
self.run_command("update-index")
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out) # normalization also rejects the obsolete key
self.assertIn("Obsolete key: 'silent_mode' found in V/filament/A.json", out)
self.assertIn("Files with warnings : 1", out)
@@ -623,9 +619,7 @@ class TestCheck(TreeCase):
"type": "machine", "name": "M 0.4 nozzle",
"default_filament_profile": ["A", "Nope"]})
self.t.index("V", "machine", "M 0.4 nozzle", "machine/M.json")
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn("Missing filament profile: 'Nope'", out)
@@ -635,9 +629,7 @@ class TestCheck(TreeCase):
self.bundle()
for sub in apt.PROFILE_SUBDIRS:
os.makedirs(os.path.join(self.t.profiles, apt.USER_DIR, "default", sub))
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
_rc, out = self.run_command("check", "--snapshot", snapshot)
_rc, out = self.run_command("check")
self.assertIn("Checked vendors : 1", out)
self.assertNotIn("user", out)
@@ -743,9 +735,7 @@ class TestCheck(TreeCase):
self.t.write("V", f"filament/Stray{n}.json",
{"type": "filament", "name": f"Stray{n}"})
self.t.write("V", "filament/NoType.json", {"name": "NoType"})
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertEqual(out.count("update-index\" to add them"), 1, out)
self.assertEqual(out.count("or delete them"), 1, out)
@@ -797,11 +787,6 @@ class TestNormalized(TreeCase):
errors, gaps = apt.check_normalized(self.t.profiles, vendor)
return errors, gaps, buf.getvalue()
def snapshot(self):
path = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", path)
return path
def test_a_bundle_the_two_commands_just_wrote_reports_nothing(self):
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
self.t.write("V", "process/B.json", {"type": "process", "name": "B"})
@@ -862,7 +847,7 @@ class TestNormalized(TreeCase):
# library included.
self.t.write(apt.OFL, "filament/A.json",
{"type": "filament", "name": "A", "version": "01.00.00.00"})
rc, out = self.run_command("check", "--snapshot", self.snapshot())
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn(f"{apt.OFL}/filament/A.json: normalize would remove version", out)
@@ -872,7 +857,7 @@ class TestNormalized(TreeCase):
{"type": "filament", "name": f"A{n}",
"version": "01.00.00.00"})
self.t.write("W", "filament/B.json", {"type": "filament", "name": "B"})
rc, out = self.run_command("check", "--snapshot", self.snapshot())
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn("3 profile file(s) above are not what", out)
self.assertEqual(out.count('normalize" writes: run it and commit'), 1, out)
@@ -898,8 +883,7 @@ class TestDispatch(TreeCase):
def test_an_option_belongs_to_one_command_only(self):
for argv in (["trim", "--force"],
["update-index", "--filament-id"],
["check", "--profile-type", "filament"],
["update-snapshot", "--vendor", "V"]):
["check", "--profile-type", "filament"]):
with self.subTest(argv=argv):
with self.assertRaises(SystemExit) as cm, \
contextlib.redirect_stdout(io.StringIO()), \