diff --git a/docs/HLSD/filament_id.md b/docs/HLSD/filament_id.md index 92d75ca8ca..0aaf845909 100644 --- a/docs/HLSD/filament_id.md +++ b/docs/HLSD/filament_id.md @@ -129,7 +129,7 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame | Color | never a new id | | Second diameter of the same product (1.75 + 2.85) | sibling filament, new id | | "High-speed" tuned for a *different printer model* | same id (it is a printer variant) | -| "High-speed" selectable *alongside* the normal preset on one printer | new id (it is a product line) | +| "High-speed" selectable *alongside* the normal preset on one printer | new name, so a new id (it is a product line) | ## Structure rules @@ -149,7 +149,9 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame are both the filament `MyBrand PLA`. 3. **Within one filament, variants' `compatible_printers` are pairwise disjoint** — per printer, at most one compatible instantiated preset per id, or AMS matching turns ambiguous. The - C++ validator's `-f` check enforces this. + C++ validator's `-f` check enforces this, tree-wide in CI. Since one product carries one id + and cannot be split onto two, this rule is the *only* remedy for such an ambiguity: narrow + the `compatible_printers`, or retire the preset that duplicates another. 4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits `Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library preset on your printers, and it is what makes its triple — and so its id — the library's) @@ -194,12 +196,17 @@ Snapmaker bundles alike; the OFL generic `Generic/PLA/Generic PLA` mints `OFDSrz by 35 bundles — most by independent declarations converging on the same mint, the rest purely through inheritance from the OFL preset. -On the rare collision with an existing id, the minter salts the input (`…/1`, -`…/2`, …) until free, and the result is frozen in the profile file. Salting is also used -deliberately: a *salt split* keeps two presets of one product on distinct ids where a single -id would be AMS-ambiguous on the same printer — the "selectable alongside" situation from the -table above, resolved without inventing a second filament name. The tooling recognizes salt -iterations of a triple as conformant and preserves such splits across re-mints. +Nothing but the triple feeds the mint — not the rest of the tree, not the snapshot, not what +another preset of the product happens to carry. Determined triple, determined id: one product +carries one id and there is no second acceptable value for it, so any other value on a preset +is a mismatch `--check` reports and `--generate` pulls back. Two *different* products whose +triples mint the same base62 value would be a collision (a roughly 36-bit id space against a +few thousand products); nothing salts past it: `--check` reports it naming both products, +`--generate` refuses to write it, and the remedy is a rename so their triples differ. Where +two presets of one product would be AMS-ambiguous on a printer, the fix is likewise in the +profiles — make their `compatible_printers` disjoint (structure rule 3), retire the redundant +preset, or, if they really are different products, give them different names so their triples +differ. Never a second id for one triple. Workflow for a new filament: @@ -217,10 +224,10 @@ python scripts/orca_extra_profile_check.py # 6. ...and everything else CI `(filament_vendor, filament_type, filament name)` triple: it inserts one where an instantiated filament resolves none, and re-derives one that does not match. A preset that *inherits* a mismatching id is the one case left to the author — check 3b names it, and the fix is to inherit -a preset of the same filament or to give the preset its own key. It converges on an id the same -product already holds and salts only past ids *other* products hold, so an id already equal to a -salt iteration of its own triple is left alone and deliberate salt splits survive. The same run -assigns `generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process +a preset of the same filament or to give the preset its own key. A declaration is left alone +exactly when it already equals the one id its triple mints, and a collision (check 3d) is +reported and left unwritten. The same run assigns +`generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process and machine preset of every vendor except BBL, which keeps its authoritative `G*` ids, strips `setting_id` from base profiles, and fixes the misspelled `settings_id` key — dropped, or, for BBL, whose ids have no formula to fall back on, restored under the correct name. It is idempotent and @@ -231,9 +238,9 @@ check CI runs over both id kinds, of which `--check` is the `filament_id` half. - `--filament-id` limits the run to `filament_id`. - `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to write both. -- `--vendor VENDOR` confines the run to that bundle; repeatable. The ids are still derived - tree-wide, so a narrowed run writes exactly what a full one would — and reports any duplicate - it was not allowed to clear, since only a run covering both bundles can. +- `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the + triple alone, so a narrowed run writes exactly what a full one would; `--check` reports + whatever it left outside. - `--dry-run` reports what `--generate` would do and writes nothing; with no mode of its own it implies `--generate`, so `--dry-run --vendor ` previews just that bundle. - `--profiles DIR` points the tooling at a different profile tree (default @@ -309,10 +316,10 @@ one-to-one in both directions. it from **BambuStudio's own shipped BBL bundle** — a sparse shallow clone of upstream `master`, or `--bambustudio-dir `. Our BBL bundle is a fork of Bambu's, tuned and extended independently, so it is not the source of truth for Bambu's ids. -A row's key is whatever id our tree already mints for that same -`(filament_vendor, filament_type, filament name)` triple; a product we do not ship gets a freshly -generated key and the row sits inert until some bundle claims that triple — `OFdyfQvU` / -`GFG03`, "Bambu PETG Matte", is such a row today. +A row's key is the id the product's `(filament_vendor, filament_type, filament name)` triple +mints — the same id any bundle of ours carries for it, since the id is a function of the triple +alone; the row of a product we do not ship sits inert until some bundle claims that triple — +`OFdyfQvU` / `GFG03`, "Bambu PETG Matte", is such a row today. **Regenerate it in the same commit as every BBL profile sync**, and read the drift report it prints. Two lines, both informational, neither blocking the write: @@ -437,11 +444,12 @@ The checks, in brief: its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change surfaces as a snapshot diff. - **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the - mint (or a low salt iteration) of its declarer's own triple; the id an instantiated preset - *inherits* must equal the mint of *its* own triple, however it inherits it (a root, a real - filament, a library preset — structure rule 1); and every instantiated system filament must - resolve an effective id at all (recall: an id-less one is a hard load error in C++ that - discards the whole vendor bundle). The errors print the expected id. + one id its declarer's own triple mints, with no second acceptable value; the id an + instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits + it (a root, a real filament, a library preset — structure rule 1); and every instantiated + system filament must resolve an effective id at all (recall: an id-less one is a hard load + error in C++ that discards the whole vendor bundle); and no two products mint one id (a + base62 collision, resolved by renaming one of them). The errors print the expected id. - **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor, BBL and Qidi included. - **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and diff --git a/scripts/orca_id_tool.py b/scripts/orca_id_tool.py index 8d08bde4ab..956aec3569 100755 --- a/scripts/orca_id_tool.py +++ b/scripts/orca_id_tool.py @@ -24,8 +24,11 @@ filament_id policy (see docs/HLSD/filament_id.md): filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, "filament_product///") ) 8 chars total, which satisfies the AMS length limit. Nobody invents ids by - hand; on the astronomically rare collision with another product's id the - input is salted ("/1", "/2", ...) until free and the result is frozen in file. + 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 + 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. Identity changes (a filament rename, a filament_vendor/filament_type fix) change the id BY DESIGN. * Reserved id spaces that are never minted into or altered: @@ -120,8 +123,6 @@ USER_CUSTOM_ID_RE = re.compile(r"^P[0-9A-Fa-f]{7}$", re.IGNORECASE) # Filament name = preset base name: strip the first "@..." suffix. The space before # "@" is optional because names like "Afinia PLA@HS" exist. BASE_NAME_RE = re.compile(r"\s?@.*$") -# Salt iterations accepted by the identity check (check 3). -MAX_CHECK_SALT = 8 # A JSON string literal, for the byte-preserving key edits. _JSON_STR = r'"(?:[^"\\]|\\.)*"' @@ -190,43 +191,20 @@ def base_name(name): return BASE_NAME_RE.sub("", name, count=1) -def generate_filament_id(filament_vendor, filament_type, filament_name, salt=0): +def generate_filament_id(filament_vendor, filament_type, filament_name): """Deterministic "OF" + 6-char base62 filament_id for a filament product. - input = "filament_product///" - (+ "/" when salted); u = uuid5(FILAMENT_ID_NAMESPACE, input); the id - tail is the low FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"), - most-significant first — the same derivation as generate_preset_setting_id. + The triple is the only input: no salt, no state, no second value. + input = "filament_product///"; + u = uuid5(FILAMENT_ID_NAMESPACE, input); the id tail is the low + FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"), most-significant + first — the same derivation as generate_preset_setting_id. """ key = f"filament_product/{filament_vendor}/{filament_type}/{filament_name}" - if salt: - key = f"{key}/{salt}" u = uuid.uuid5(FILAMENT_ID_NAMESPACE, key) return "OF" + _base62_tail(int.from_bytes(u.bytes, "big"), FILAMENT_ID_LENGTH) -def mint_filament_id(filament_vendor, filament_type, filament_name, taken): - """Mint the product's id: the first salt iteration not in `taken`. - - Bounded by MAX_CHECK_SALT, the last iteration the identity check accepts, so - the tool can never write an id its own --check would reject. Exhausting it - would take nine base62 collisions on one triple; it means something is wrong - with `taken`, not that a tenth salt is needed. - """ - for salt in range(MAX_CHECK_SALT + 1): - candidate = generate_filament_id(filament_vendor, filament_type, filament_name, salt) - if candidate not in taken: - return candidate - raise RuntimeError( - f"could not mint a free filament_id for {filament_vendor}/{filament_type}/" - f"{filament_name}: salts 0..{MAX_CHECK_SALT} are all taken") - - -def mint_iterations(triple): - """The ids that count as the mint of `triple`: salt 0..MAX_CHECK_SALT.""" - return {generate_filament_id(*triple, salt=s) for s in range(MAX_CHECK_SALT + 1)} - - # --------------------------------------------------------------------------- # Tree loading + loader-faithful effective-id resolution # --------------------------------------------------------------------------- @@ -439,12 +417,15 @@ def analyze_tree(profiles_dir): triples = {} # fid -> set of triples of its declarers declarer_triples = [] # (vendor, rec, fid, triple) per declarer filament_triples = {} # (vendor, filament_name) -> {triple: [declarers]} + mints = {} # minted id -> triples minting it (declarers + instantiated) for vendor, filaments in vendors.items(): occurring = vendor_ids.setdefault(vendor, set()) for rec in filaments.values(): triple = resolve_triple(rec["name"], filaments, ofl_filaments) rec["triple"] = triple + if rec.get("filament_id") or rec["instantiation"]: + mints.setdefault(generate_filament_id(*triple), set()).add(triple) if rec.get("filament_id"): fid = rec["filament_id"] occurring.add(fid) @@ -466,16 +447,6 @@ def analyze_tree(profiles_dir): if not rec.get("filament_id") and OF_ID_RE.match(eff): inherited.append((vendor, rec, eff, triple)) - # Identity (check 3b): an inherited id must be the mint of the preset's - # OWN triple. A declared id is held to the same rule as a declarer (3a), - # and an id whose declarer already fails 3a is reported there once, not - # again under every preset inheriting it. - unminted = {fid for _v, _r, fid, triple in declarer_triples - if OF_ID_RE.match(fid) and fid not in mint_iterations(triple)} - id_mismatches = [ - (vendor, rec, eff, triple) for vendor, rec, eff, triple in inherited - if eff not in unminted and eff not in mint_iterations(triple)] - # Cross-bundle triple divergence (check 5, warning only): the same filament # name declared in several bundles with different triples cannot converge # on one id until the divergence is fixed. @@ -495,12 +466,13 @@ def analyze_tree(profiles_dir): "vendor_ids": vendor_ids, "declared_ids": declared_ids, "missing_effective": sorted(missing_effective), - "id_mismatches": id_mismatches, + "inherited": inherited, "triples": {fid: sorted(list(t) for t in ts) for fid, ts in triples.items()}, - "triple_sets": triples, "declarer_triples": declarer_triples, "filament_triples": filament_triples, "cross_bundle_triples": cross_bundle_triples, + # id -> the products (triples) minting it, where there is more than one + "collisions": {fid: sorted(ts) for fid, ts in mints.items() if len(ts) > 1}, } @@ -580,12 +552,14 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, 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. (a) A declared id - must equal the mint of the declarer's own triple or a salted iteration; - (b) the id an instantiated preset inherits must equal the mint of ITS - own triple — how it inherits it (a root, a real filament, an OFL - preset) is irrelevant; (c) every instantiated filament resolves an - effective id at all (an id-less one is a hard load error in C++). + 3. 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 + real filament, an OFL preset) is irrelevant; (c) every instantiated + 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. Reserved namespaces (GF*/QD_*/P-hex/"null", all ownerless) must not be claimed by any vendor. 5. Triple integrity: (a) every declarer resolves non-empty filament_vendor @@ -664,22 +638,36 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, errors += 1 # -- 3. 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 + # presets of one product that would be AMS-ambiguous on a printer are fixed + # in the profiles, by making their compatible_printers disjoint or by + # retiring the redundant one. for vendor, rec, fid, triple in sorted( analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])): - if not OF_ID_RE.match(fid) or fid in mint_iterations(triple): - continue + want = generate_filament_id(*triple) + if not OF_ID_RE.match(fid) or fid == want: + continue # a non-OF id is check 1's error print_error( f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does ' f'not match the mint of its triple "{"/".join(triple)}": expected ' - f'"{generate_filament_id(*triple)}" (or a salted iteration); paste the ' - f'expected id, or fix the triple and run "{GENERATE_CMD} --vendor {vendor}" ' - f"(preview with --dry-run), then --update-snapshot") + 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") errors += 1 + # (3b) 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. for vendor, rec, eff, triple in sorted( - analysis["id_mismatches"], key=lambda x: (x[0], x[1]["file"])): + analysis["inherited"], key=lambda x: (x[0], x[1]["file"])): + want = generate_filament_id(*triple) + if eff == want: + continue print_error( f'preset "{rec["name"]}" ({rec["file"]}) inherits filament_id "{eff}" but ' - f'its own triple "{"/".join(triple)}" mints "{generate_filament_id(*triple)}"; ' + f'its own triple "{"/".join(triple)}" mints "{want}"; ' f"a preset carries the id of its own product: inherit a preset of the " f"same filament, or declare its own key") errors += 1 @@ -691,7 +679,15 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, f'instantiated filament "{name}" ({file}) resolves no filament_id anywhere ' f"in its inherits chain — this is a hard load error in the C++ loader; " f'run "{GENERATE_CMD}" (expected id for filament ' - f'"{vendor}/{base_name(name)}": "{expected}", salted if taken)') + f'"{vendor}/{base_name(name)}": "{expected}")') + errors += 1 + # (3d) 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( + f'filament_id "{fid}" is the mint of {len(ts)} different products ' + f'({"; ".join("/".join(t) for t in ts)}): a base62 collision; rename one ' + f"of them so their triples differ") errors += 1 # -- 4. reserved namespaces ---------------------------------------------- @@ -993,75 +989,36 @@ def rewrite_filament_id(path, old_id, new_id, dry_run=False): # --generate # --------------------------------------------------------------------------- -def make_want_id(analysis, snapshot=None): - """Build the id policy: want_id(triple) -> the id that triple must carry. - - A triple's id is its first mint iteration that no OTHER product holds. An id - is blocked when the tree or the snapshot records it under a triple set other - than exactly {triple}, or when this run already handed it to a different - triple. Same-triple reuse is therefore convergence — one product, one id in - every bundle — and salting only ever steps past another product's id. - - Only a CONFORMANT record holds an id: a declaration whose value is not a mint - iteration of its own triple is transient — this run rewrites it — so it must - not block the product that legitimately mints the id it is squatting on. That - also makes the policy independent of which vendors a run writes, so a - --vendor-narrowed run picks the same ids as a full one. - """ - holders = {} # id -> the set of triples that legitimately hold it - for fid, ts in analysis["triple_sets"].items(): - conformant = {t for t in ts if fid in mint_iterations(t)} - if conformant: - holders[fid] = conformant - for fid, entry in (snapshot or {}).get("ids", {}).items(): - triple = tuple(snapshot_triple(entry)) - if fid in mint_iterations(triple): - holders.setdefault(fid, set()).add(triple) - - assigned = {} # triple -> the id chosen for it this run - run_taken = {} # id -> the triple this run gave it to - - def want_id(triple): - if triple not in assigned: - blocked = {fid for fid, ts in holders.items() if ts != {triple}} - blocked |= {fid for fid, t in run_taken.items() if t != triple} - cand = mint_filament_id(*triple, taken=blocked) - assigned[triple] = cand - run_taken[cand] = triple - return assigned[triple] - - return want_id - - def _incomplete_triple(triple): """The name(s) of the empty mint-key fields, or "" when both are present.""" return " and ".join(k for k, v in (("filament_vendor", triple[0]), ("filament_type", triple[1])) if not v) -def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, - vendors=None, dry_run=False, changed_paths=None): +def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False, + changed_paths=None): """Make every filament carry the id its own triple mints. One rule, applied to declarations and to id-less filaments alike: - * a declared id that is not a mint iteration of the declarer's own triple — - a wrong OF id, or a foreign one such as a Bambu "GF*" arriving with an - upstream sync — is replaced in place; + * a declared id that is not the one its own triple mints — a wrong OF id, + or a foreign one such as a Bambu "GF*" arriving with an upstream sync — + is replaced in place; * 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). - An id already equal to ANY salt iteration of its own triple is conformant - (check 3) and left alone, so deliberate salt splits — distinct presets of one - product kept apart for per-printer AMS matching — survive. + 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 + and left unwritten: nothing salts past a collision, a rename resolves it. - `vendors` restricts what is WRITTEN; the analysis and the id policy always - span the whole tree, so a narrowed run mints exactly what a full one would. - `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 touches the snapshot — run --update-snapshot afterwards and review the - diff. Returns (files_changed, errors). + `vendors` restricts what is WRITTEN; the id is a function of the triple + alone, so a narrowed run writes exactly what a full one would, and --check + 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). """ _utf8_console() analysis = analyze_tree(profiles_dir) @@ -1080,24 +1037,26 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH print_error(f'unknown vendor "{v}" in {profiles_dir}') return 0, errors + len(unknown) - want_id = make_want_id(analysis, load_snapshot(snapshot_path)) + colliding = set() # triples no run may write an id for + for fid, ts in sorted(analysis["collisions"].items()): + print_error( + f'cannot write filament_id "{fid}": it is the mint of {len(ts)} different ' + f'products ({"; ".join("/".join(t) for t in ts)}), a base62 collision; ' + f"rename one of them so their triples differ") + errors += 1 + colliding.update(ts) + verb = "would " if dry_run else "" files_changed = 0 reminted = 0 inserted = 0 - held_here = set() # ids the filaments this run may write legitimately hold - left_standing = {} # id -> files still declaring one non-conformantly # 1. Declarations that are not the mint of their own triple. for vendor, rec, fid, triple in sorted( analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])): - in_scope = wanted is None or vendor in wanted - if fid in mint_iterations(triple): - if in_scope: - held_here.add(fid) - continue - if not in_scope: - left_standing.setdefault(fid, []).append(rec["file"]) + want = generate_filament_id(*triple) + if (fid == want or (wanted is not None and vendor not in wanted) + or triple in colliding): continue missing = _incomplete_triple(triple) if missing: @@ -1106,21 +1065,15 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH f'{missing}; the mint key needs both (generic materials use ' f'filament_vendor "Generic")') errors += 1 - left_standing.setdefault(fid, []).append(rec["file"]) - continue - want = want_id(triple) - if fid == want: continue try: rewrite_filament_id(rec["path"], fid, want, dry_run) except (OSError, RuntimeError, ValueError) as e: print_error(str(e)) errors += 1 - left_standing.setdefault(fid, []).append(rec["file"]) continue files_changed += 1 reminted += 1 - held_here.add(want) if changed_paths is not None: # Index sub_paths are "/"-joined even on Windows, where the # setting_id pass reaches the same file through os.walk: normalize @@ -1166,6 +1119,8 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH errors += 1 continue triple = (*next(iter(fields)), filament_name) + if triple in colliding: + continue # reported above missing = _incomplete_triple(triple) if missing: print_error( @@ -1174,7 +1129,7 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH f'filament_vendor "Generic")') errors += 1 continue - new_id = want_id(triple) + new_id = generate_filament_id(*triple) for name in sorted(roots): root = roots[name] try: @@ -1185,24 +1140,11 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH continue files_changed += 1 inserted += 1 - held_here.add(new_id) if changed_paths is not None: changed_paths.add(os.path.normpath(root["path"])) print_info(f'{verb}insert filament "{vendor}/{filament_name}": filament_id ' f'"{new_id}" -> {root["file"]}') - # A declaration that is not the mint of its own triple holds no id, so it - # never blocks the product the id belongs to — that is what makes a narrowed - # run mint exactly what a full one would. When the run is not allowed to - # rewrite that declaration, though, it stays behind on an id just handed to - # its rightful owner, and only a run that covers both can clear it. - for fid in sorted(held_here & set(left_standing)): - print_error( - f'filament_id "{fid}" belongs to a filament this run covers but is also ' - f'declared by {", ".join(sorted(left_standing[fid]))}, which it did not ' - f'rewrite; run "{GENERATE_CMD}" over both to clear the duplicate') - errors += 1 - print_info(f"filament_ids inserted : {inserted}") print_info(f"filament_ids re-minted : {reminted}") return files_changed, errors @@ -1376,10 +1318,10 @@ def build_parser(): narrow.add_argument("--setting-id", action="store_true", help="write setting_id only, skipping filament_id") narrow.add_argument("--vendor", metavar="VENDOR", action="append", default=[], - help="write only in this vendor bundle; repeatable. The ids are " - "still derived tree-wide, so a narrowed run writes exactly " - "what a full one would, and reports what it was not allowed " - "to fix") + help="write only in this vendor bundle; repeatable. The id is a " + "function of the triple alone, so a narrowed run writes " + "exactly what a full one would; --check reports whatever " + "it left outside") parser.add_argument("--dry-run", "--dryrun", dest="dry_run", action="store_true", help="report what would change and write nothing; with no mode of " "its own it previews --generate") @@ -1462,7 +1404,7 @@ def main(argv=None): filament_files = errors = 0 if do_filament: filament_files, e = generate_filament_ids( - args.profiles, snapshot_path, vendors, args.dry_run, changed) + args.profiles, vendors, args.dry_run, changed) errors += e if do_setting: _n, e = generate_setting_ids(args.profiles, vendors, args.dry_run, changed) diff --git a/scripts/tests/test_filament_id.py b/scripts/tests/test_filament_id.py index b7646c4b64..3e7a6d2aec 100644 --- a/scripts/tests/test_filament_id.py +++ b/scripts/tests/test_filament_id.py @@ -155,15 +155,13 @@ class SyntheticTree: def assign(self, vendors=None, dry_run=False): buf = io.StringIO() with contextlib.redirect_stdout(buf): - changed, errors = afi.generate_filament_ids( - self.profiles, self.snapshot, vendors, dry_run) + changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run) return changed, errors, buf.getvalue() def remint(self, vendors, dry_run=False): buf = io.StringIO() with contextlib.redirect_stdout(buf): - changed, errors = afi.generate_filament_ids( - self.profiles, self.snapshot, vendors, dry_run) + changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run) return changed, errors, buf.getvalue() def cli(self, *flags): @@ -240,8 +238,6 @@ class TestMint(unittest.TestCase): # layout ("filament_product///") and base62 tail. self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA"), "OF5CgdDq") - self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA", - salt=1), "OFD9mV8H") self.assertEqual(afi.generate_filament_id("Generic", "PLA", "Generic PLA"), "OFDSrzZ8") @@ -262,95 +258,6 @@ class TestMint(unittest.TestCase): self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PETG", "PolyLite PLA")) self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA Pro")) - def test_salt_changes_id(self): - base = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA") - salted = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA", salt=1) - self.assertNotEqual(base, salted) - self.assertRegex(salted, r"^OF[0-9A-Za-z]{6}$") - - def test_mint_salt_iteration(self): - triple = ("Polymaker", "PLA", "PolyLite PLA") - taken = {afi.generate_filament_id(*triple, salt=s) for s in range(2)} - self.assertEqual(afi.mint_filament_id(*triple, taken), - afi.generate_filament_id(*triple, salt=2)) - self.assertEqual(afi.mint_filament_id(*triple, set()), - afi.generate_filament_id(*triple)) - - def test_mint_never_returns_a_salt_the_identity_check_rejects(self): - # mint_filament_id stops at MAX_CHECK_SALT, the last iteration check 3 - # accepts, so the tool can never write an id its own --check rejects. - triple = ("Polymaker", "PLA", "PolyLite PLA") - all_taken = afi.mint_iterations(triple) - self.assertEqual(len(all_taken), afi.MAX_CHECK_SALT + 1) - last = afi.mint_filament_id(*triple, all_taken - {afi.generate_filament_id( - *triple, salt=afi.MAX_CHECK_SALT)}) - self.assertIn(last, afi.mint_iterations(triple)) - with self.assertRaises(RuntimeError): - afi.mint_filament_id(*triple, all_taken) - - def test_want_id_policy(self): - # The whole id policy: a triple's id is its first mint iteration that no - # OTHER product holds, in the tree or in the snapshot. - triple = ("Polymaker", "PLA", "PolyLite PLA") - other = ("Elegoo", "PETG", "Rapid PETG") - salt0 = afi.generate_filament_id(*triple) - salt1 = afi.generate_filament_id(*triple, salt=1) - - def snapshot(**entries): - return {"ids": {fid: {"filaments": [], "name": t[2], - "filament_type": t[1], "filament_vendor": t[0]} - for fid, t in entries.items()}} - - # nobody holds it: salt 0 - self.assertEqual(afi.make_want_id({"triple_sets": {}})(triple), salt0) - # the tree holds it under this very triple: reuse is convergence - self.assertEqual( - afi.make_want_id({"triple_sets": {salt0: {triple}}})(triple), salt0) - # Another triple "holds" it, but salt0 is not a mint iteration of THAT - # triple, so the holder is a wrong declaration this very run rewrites. - # A transient squatter must not push a product off its own id. - self.assertEqual( - afi.make_want_id({"triple_sets": {salt0: {other}}})(triple), salt0) - self.assertEqual( - afi.make_want_id({"triple_sets": {salt0: {triple, other}}})(triple), salt0) - # The snapshot is read the same way: only a conformant record holds. - self.assertEqual( - afi.make_want_id({"triple_sets": {}}, snapshot(**{salt0: other}))(triple), - salt0) - # ... and the snapshot's own triple reuses it - self.assertEqual( - afi.make_want_id({"triple_sets": {}}, snapshot(**{salt0: triple}))(triple), - salt0) - # Unused here, but salting must still be reachable: see - # test_want_id_salts_past_a_genuine_collision. - self.assertNotEqual(salt0, salt1) - # memoized: one triple keeps one id for the whole run - want_id = afi.make_want_id({"triple_sets": {}}) - self.assertEqual(want_id(triple), want_id(triple)) - # ... and a second triple never gets the id this run already handed out - self.assertNotEqual(want_id(triple), want_id(other)) - - def test_want_id_salts_past_a_genuine_collision(self): - # Salting exists for one case only: two products whose triples mint the - # same id. That needs a base62 collision, so force one — salt 0 of every - # triple collapses to a single value while the salted iterations stay - # distinct. `other` then CONFORMANTLY holds the shared salt-0 id, and - # `triple` must step past it instead of stealing it. - triple = ("V", "PLA", "X") - other = ("W", "ABS", "Y") - real = afi.generate_filament_id - - def colliding(vendor, ftype, name, salt=0): - return "OFcolid" if salt == 0 else real(vendor, ftype, name, salt) - - afi.generate_filament_id = colliding - try: - self.assertIn("OFcolid", afi.mint_iterations(other)) # holder conforms - got = afi.make_want_id({"triple_sets": {"OFcolid": {other}}})(triple) - finally: - afi.generate_filament_id = real - self.assertEqual(got, real(*triple, salt=1)) - class TestBaseName(unittest.TestCase): def test_filament_name_derivation(self): @@ -589,12 +496,13 @@ class TestChecks(OfCleanTreeCase): 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 once on the root, not again under the variant inheriting it. + # 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, 1, out) + 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() @@ -613,16 +521,6 @@ 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_salted_mint_is_accepted(self): - salted = afi.generate_filament_id("BV", "PLA", "BNEW", salt=3) - self.t.write_preset("VendorA", preset("BNEW @base", filament_id=salted, - instantiation=False, - filament_vendor="BV", filament_type="PLA")) - 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 the mint", 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", @@ -633,8 +531,9 @@ class TestChecks(OfCleanTreeCase): rc, _out = self.t.update_snapshot() self.assertEqual(rc, 0) errors, out = self.t.check() - self.assertEqual(errors, 1, out) + 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): # A preset of another filament inheriting APLA's root takes APLA's id, @@ -654,6 +553,78 @@ class TestChecks(OfCleanTreeCase): errors, out = self.t.check() self.assertEqual(errors, 1, out) + def test_check3_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 + # folded into the declarer's error. + self.t.write_preset("VendorA", preset("DNEW @base", filament_id="OFZZZZZZ", + instantiation=False, + filament_vendor="DV", filament_type="PLA")) + self.t.write_preset("VendorA", preset("DNEW @P1", inherits="DNEW @base", + compatible_printers=["P1"])) + self.t.write_preset("VendorA", preset("Other DNEW @P1", inherits="DNEW @base", + compatible_printers=["P1 0.4 nozzle"])) + errors, out = self.t.check() + self.assertIn("does not match the mint of its triple", out) + 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) + + def test_check3_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 + # is a second error, not a reason to leave the first unreported. + apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA") + self.t.write_preset("VendorA", preset("Tuned PLA @base", filament_id=apla_id, + instantiation=False, + filament_vendor="AVendor", + filament_type="PLA")) + self.t.write_preset("VendorA", preset("Tuned PLA @P1", inherits="APLA @base", + compatible_printers=["P1"])) + errors, out = self.t.check() + self.assertIn('"Tuned PLA @base" (VendorA/filament/Tuned PLA @base.json) does ' + '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) + + def test_check3_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. + collide = {("V", "PLA", "X"), ("W", "ABS", "Y")} + real = afi.generate_filament_id + + def colliding(vendor, ftype, name): + return "OFcolid0" if (vendor, ftype, name) in collide else real(vendor, ftype, name) + + afi.generate_filament_id = colliding + self.addCleanup(setattr, afi, "generate_filament_id", real) + for vendor, ftype, name in sorted(collide): + self.t.write_preset("VendorA", preset(f"{name} @base", filament_id="OFcolid0", + instantiation=False, + filament_vendor=vendor, + filament_type=ftype)) + self.t.write_preset("VendorA", preset(f"{name} @P1", inherits=f"{name} @base", + compatible_printers=["P1"])) + errors, out = self.t.check() + self.assertIn("collision", out) + 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. + self.assertNotIn("does not match the mint", out) + self.assertNotIn("inherits filament_id", out) + self.assertEqual(errors, 2, out) + def test_check3_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", @@ -1144,9 +1115,9 @@ class TestAssign(OfCleanTreeCase): self.assertEqual(b2["filament_id"], want) def test_converges_on_an_existing_tree_id_for_the_same_triple(self): - # BEHAVIOUR CHANGE: an id-less filament whose product is already shipped - # (with its conforming id) in another bundle converges on that id - # instead of salting past it. One product, one id, in every bundle. + # An id-less filament whose product is already shipped (with its + # conforming id) in another bundle converges on that id. One product, + # one id, in every bundle. want = afi.generate_filament_id("CV", "PLA", "CPLA") self.t.add_vendor("VendorB", [ preset("CPLA @base", filament_id=want, instantiation=False, @@ -1166,9 +1137,8 @@ class TestAssign(OfCleanTreeCase): def test_a_squatted_id_is_still_minted_for_its_own_product(self): # VendorB's "Other" declares the id that belongs to VendorA's "DPLA" — - # a copy-paste, not a real claim, since it is not the mint of Other's - # own triple. That declaration is transient (--generate rewrites it), so - # it must NOT push DPLA onto a salted id: DPLA gets its canonical one. + # a copy-paste, not a claim. The id is the mint of DPLA's triple and + # nothing else, so DPLA gets it whatever VendorB carries. want0 = afi.generate_filament_id("DV", "PLA", "DPLA") self.t.add_vendor("VendorB", [ preset("Other @base", filament_id=want0, instantiation=False, @@ -1180,27 +1150,51 @@ class TestAssign(OfCleanTreeCase): self.t.write_preset("VendorA", preset("DPLA @P1", inherits="DPLA @base", compatible_printers=["P1"])) changed, errors, out = self.t.assign(["VendorA"]) - # VendorB is outside the write set, so the id DPLA just took is still - # declared there: the run says so rather than leaving a silent duplicate. - self.assertEqual((changed, errors), (1, 1), out) - self.assertIn("declared by VendorB/filament/Other @base.json", out) - # ...and keeps saying so until a run that covers VendorB clears it. - self.assertEqual(self.t.assign(["VendorA"])[:2], (0, 1)) + self.assertEqual((changed, errors), (1, 0), out) root = load_json_file(self.t.preset_path("VendorA", "DPLA @base")) self.assertEqual(root["filament_id"], want0) - # A full run re-mints Other to its own triple and the duplicate is gone. - other = load_json_file(self.t.preset_path("VendorB", "Other @base")) - self.assertEqual(other["filament_id"], want0) - changed, errors, out = self.t.assign() + # VendorB's declaration is VendorB's own mismatch: a run not allowed to + # touch it leaves it to --check, and a run over VendorB re-mints it. + _errors, out = self.t.check() + self.assertIn('"Other @base" (VendorB/filament/Other @base.json) does not match ' + 'the mint of its triple', out) + changed, errors, out = self.t.assign(["VendorB"]) self.assertEqual((changed, errors), (1, 0), out) other = load_json_file(self.t.preset_path("VendorB", "Other @base")) self.assertEqual(other["filament_id"], afi.generate_filament_id("OV", "ABS", "Other")) + def test_generate_refuses_to_write_into_a_collision(self): + # "X" ships on the id its triple mints; a new product "Y" whose triple + # mints the very same id is a base62 collision. The run does not salt + # past it: it reports both products, writes nothing for "Y", and leaves + # the remedy — a rename, so the triples differ — to the author. + collide = {("V", "PLA", "X"), ("W", "ABS", "Y")} + real = afi.generate_filament_id + + def colliding(vendor, ftype, name): + return "OFcolid0" if (vendor, ftype, name) in collide else real(vendor, ftype, name) + + afi.generate_filament_id = colliding + self.addCleanup(setattr, afi, "generate_filament_id", real) + self.t.write_preset("VendorA", preset("X @base", filament_id="OFcolid0", + instantiation=False, + filament_vendor="V", filament_type="PLA")) + self.t.write_preset("VendorA", preset("X @P1", inherits="X @base", + compatible_printers=["P1"])) + self.t.write_preset("VendorA", preset("Y @base", instantiation=False, + filament_vendor="W", filament_type="ABS")) + self.t.write_preset("VendorA", preset("Y @P1", inherits="Y @base", + compatible_printers=["P1"])) + before = self.t.bytes_map() + changed, errors, out = self.t.assign() + self.assertEqual((changed, errors), (0, 1), out) + self.assertIn("V/PLA/X", out) + self.assertIn("W/ABS/Y", out) + self.assertEqual(self.t.bytes_map(), before) + def test_mismatching_of_declaration_is_re_derived(self): - # BEHAVIOUR CHANGE: the default run used to leave an OF-format id alone - # and needed a separate mode to fix it. One rule now: an id that is not - # a mint iteration of its own triple is rewritten. + # One rule: an id that is not the mint of its own triple is rewritten. self.t.write_preset("VendorA", preset("KNEW @base", filament_id="OFZZZZZZ", instantiation=False, filament_vendor="KV", @@ -1445,20 +1439,19 @@ class TestRemint(SyntheticTreeCase): changed, errors, _out = self.t.remint(["VendorA"]) self.assertEqual((changed, errors), (0, 0)) - def test_salted_declaration_survives_remint(self): - # A deliberate salt split (a second preset of one product kept on its - # own id for per-printer AMS disambiguation) is mint-conformant and - # must not be converged back onto salt 0. - salt1 = afi.generate_filament_id(*self.TRIPLE, salt=1) - self.t.write_preset("VendorA", preset("APLA @legacy", - filament_id=salt1, + def test_second_id_of_one_product_is_converged_by_remint(self): + # A second preset of one product kept on an id of its own is not a + # sanctioned split: the triple determines the id, so --generate pulls it + # back. An AMS ambiguity this exposes is fixed in the profiles instead. + want = afi.generate_filament_id(*self.TRIPLE) + self.t.write_preset("VendorA", preset("APLA @legacy", filament_id="OFlegac1", inherits="APLA @base", compatible_printers=["P2 0.4 nozzle"])) changed, errors, out = self.t.remint(["VendorA"]) self.assertEqual(errors, 0, out) - self.assertEqual(changed, 1) # only the non-conformant AX01 root + self.assertEqual(changed, 2) # the non-conformant AX01 root and @legacy legacy = load_json_file(self.t.preset_path("VendorA", "APLA @legacy")) - self.assertEqual(legacy["filament_id"], salt1) + self.assertEqual(legacy["filament_id"], want) def test_same_triple_converges_within_run(self): self.t.add_vendor("VendorB", [ @@ -1490,22 +1483,24 @@ class TestRemint(SyntheticTreeCase): self.assertEqual(a["filament_id"], want) def test_not_blocked_by_a_non_conformant_occurrence(self): - # VendorB carries APLA's id under a PETG triple of its own, so that - # declaration is wrong and this run's job is to rewrite it. It must not - # cost VendorA's APLA the id its triple actually mints. + # VendorB carries APLA's id under a PETG triple of its own: that + # declaration is wrong, and it costs VendorA's APLA nothing — the id is + # the mint of APLA's triple, whoever else is squatting on it. want0 = afi.generate_filament_id(*self.TRIPLE) self.t.add_vendor("VendorB", [ preset("BPLA @base", filament_id=want0, instantiation=False, filament_vendor="BV", filament_type="PETG"), ]) changed, errors, out = self.t.remint(["VendorA"]) - self.assertEqual((changed, errors), (1, 1), out) - self.assertIn("declared by VendorB/filament/BPLA @base.json", out) + self.assertEqual((changed, errors), (1, 0), out) root = load_json_file(self.t.preset_path("VendorA", "APLA @base")) self.assertEqual(root["filament_id"], want0) - # Widening the run to both bundles clears it. - changed, errors, out = self.t.remint(["VendorA", "VendorB"]) + # A run over VendorB re-mints the squatter to its own triple. + changed, errors, out = self.t.remint(["VendorB"]) self.assertEqual((changed, errors), (1, 0), out) + self.assertEqual( + load_json_file(self.t.preset_path("VendorB", "BPLA @base"))["filament_id"], + afi.generate_filament_id("BV", "PETG", "BPLA")) def test_bbl_is_reminted_like_any_vendor(self): self.t.add_vendor("BBL", [ @@ -1578,8 +1573,7 @@ class TestCli(unittest.TestCase): # Named explicitly, both modes run against that tree. rc, out = self.t.cli("--update-snapshot") self.assertEqual(rc, 0, out) - # --generate only reads the snapshot as a list of ids not to reuse, so - # it keeps working without one. + # --generate never reads the snapshot, so it keeps working without one. buf = io.StringIO() with contextlib.redirect_stdout(buf): rc = afi.main(["--dry-run", "--profiles", self.t.profiles]) @@ -1762,8 +1756,7 @@ class TestReviewFixes(OfCleanTreeCase): touched = set() buf = io.StringIO() with contextlib.redirect_stdout(buf): - afi.generate_filament_ids(self.t.profiles, self.t.snapshot, - None, False, touched) + afi.generate_filament_ids(self.t.profiles, None, False, touched) afi.generate_setting_ids(self.t.profiles, None, False, touched) for name in ("SoloPLA @P1", "WrongPLA @P1"): self.assertEqual([t for t in touched if t.endswith(name + ".json")], @@ -1841,58 +1834,21 @@ class TestBambuMap(unittest.TestCase): return filaments def test_one_row_per_filament(self): - rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]), {}) + rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")])) self.assertEqual(rows, {afi.generate_filament_id("Bambu Lab", "ABS", "Bambu ABS"): {"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}}) def test_shared_bambu_id_is_an_error(self): with self.assertRaises(SystemExit): - ubfi.derive_rows(self._bs_tree([("A", "GFX00", "V", "PLA"), ("B", "GFX00", "V", "PLA")]), {}) - - def test_reuses_the_id_we_ship_for_that_triple(self): - rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]), - {("Bambu Lab", "ABS", "Bambu ABS"): "OFsalted1"}) - self.assertIn("OFsalted1", rows) + ubfi.derive_rows(self._bs_tree([("A", "GFX00", "V", "PLA"), ("B", "GFX00", "V", "PLA")])) @unittest.skipUnless(os.path.isdir("/Users/lijiang/codes/BambuStudio/resources/profiles"), "no local clone") def test_local_clone_yields_the_catalog(self): - rows = ubfi.derive_rows(afi.load_vendor_filaments("/Users/lijiang/codes/BambuStudio/resources/profiles", "BBL")[0], {}) + rows = ubfi.derive_rows(afi.load_vendor_filaments("/Users/lijiang/codes/BambuStudio/resources/profiles", "BBL")[0]) self.assertEqual(len(rows), 100) self.assertEqual(len({r["bambu_id"] for r in rows.values()}), 100) -class TestOrcaTriplesFromAnalysis(unittest.TestCase): - def _tree(self): - t = SyntheticTree() - self.addCleanup(t.cleanup) - t.add_vendor("VendorA", [ - # Same triple declared under two different ids on purpose: mirrors - # a real, sanctioned case (Cubicon's xCeler line keeps its own id - # per printer instead of inheriting the filament's @base id). - # Ambiguous, but must stay harmless unless something needs it. - preset("Ambig PLA @base", filament_id="OF111111", instantiation=False, - filament_vendor="V", filament_type="PLA"), - preset("Ambig PLA @P1", filament_id="OF222222", inherits="Ambig PLA @base", - compatible_printers=["P1"]), - # An unambiguous filament elsewhere in the same tree. - preset("Needed PLA @base", filament_id="OF333333", instantiation=False, - filament_vendor="V2", filament_type="PLA"), - preset("Needed PLA @P1", inherits="Needed PLA @base", - compatible_printers=["P1"]), - ]) - return afi.analyze_tree(t.profiles) - - def test_ambiguity_outside_needed_triples_is_ignored(self): - analysis = self._tree() - result = ubfi.orca_triples_from_analysis(analysis, {("V2", "PLA", "Needed PLA")}) - self.assertEqual(result, {("V2", "PLA", "Needed PLA"): "OF333333"}) - - def test_ambiguity_inside_needed_triples_is_an_error(self): - analysis = self._tree() - with self.assertRaises(SystemExit): - ubfi.orca_triples_from_analysis(analysis, {("V", "PLA", "Ambig PLA")}) - - class TestWriteMap(unittest.TestCase): def test_format(self): d = tempfile.mkdtemp(prefix="bambu_map_test_") diff --git a/scripts/tests/test_setting_id.py b/scripts/tests/test_setting_id.py index ad9d06a2f0..95e447d03c 100644 --- a/scripts/tests/test_setting_id.py +++ b/scripts/tests/test_setting_id.py @@ -141,13 +141,10 @@ class SettingTree: return changed, errors, buf.getvalue() def run_filament_ids(self, vendors=None, dry_run=False): - """The OTHER half of --generate, against this tree's own (absent) - snapshot, so the id policy never consults the repo's real one.""" + """The OTHER half of --generate.""" buf = io.StringIO() with contextlib.redirect_stdout(buf): - changed, errors = afi.generate_filament_ids( - self.profiles, os.path.join(self.dir, "filament_id_snapshot.json"), - vendors, dry_run) + changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run) return changed, errors, buf.getvalue() diff --git a/scripts/update_bambu_filament_ids.py b/scripts/update_bambu_filament_ids.py index 93047895e1..539376599e 100644 --- a/scripts/update_bambu_filament_ids.py +++ b/scripts/update_bambu_filament_ids.py @@ -15,9 +15,9 @@ has to be hand-maintained. See docs/HLSD/filament_id.md. One row per BambuStudio filament PRODUCT: one named spool product = one "@base"-declared filament_id, shared by every per-printer/per-nozzle instantiation of it (BambuStudio follows the same one-product-one-id shape -Orca's own filament_id policy does). A row's key reuses whatever OF id Orca -already ships for that same (filament_vendor, filament_type, filament) triple; -a triple Orca does not ship anywhere yet gets a freshly generated one. +Orca's own filament_id policy does). A row's key is the OF id that product's +(filament_vendor, filament_type, filament) triple mints — the id Orca carries +for it wherever it ships it, since the id is a function of the triple alone. Map format: { @@ -57,7 +57,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from orca_id_tool import ( # noqa: E402 BAMBU_MAP_PATH, OFL, - OF_ID_RE, PROFILES_DIR, analyze_tree, base_name, @@ -77,8 +76,8 @@ BAMBUSTUDIO_REPO = "https://github.com/bambulab/BambuStudio" # Row derivation # --------------------------------------------------------------------------- -def derive_rows(bs_filaments, orca_triples): - """orca_triples: {(vendor, type, name): orca_id} from analyze_tree()["triples"].""" +def derive_rows(bs_filaments): + """One row per BambuStudio product, keyed by the OF id its triple mints.""" by_filament = {} # filament_name -> (bambu_id, triple) for rec in bs_filaments.values(): if not rec["instantiation"]: @@ -98,50 +97,11 @@ def derive_rows(bs_filaments, orca_triples): raise SystemExit(f"Bambu id {bambu_id} is shared by " f"{seen[bambu_id]!r} and {filament_name!r}") seen[bambu_id] = filament_name - orca_id = orca_triples.get(triple) or generate_filament_id(*triple) # reuse a salted id if we ship one - rows[orca_id] = {"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]} + rows[generate_filament_id(*triple)] = { + "bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]} return rows -def orca_triples_from_analysis(orca_analysis, needed_triples): - """{(vendor, type, name): orca_id}, inverted from analyze_tree()["triples"] - (id -> [[vendor, type, name], ...]) and restricted to `needed_triples` - (the triples BambuStudio's own bundle ships, i.e. the only ones derive_rows - will ever look up). - - Kept to ids matching the OF format: our own BBL bundle still declares - Bambu's GF ids today (analyze_tree already excludes those as BBL-island - declarations), but filtering here too means a future triple shipped under - a non-OF scheme can never key a map row, which must always be keyed by - the OF id the BBL bundle receives once it is re-minted onto the OF space. - - A needed triple owned by more than one OF id would make derive_rows's - reuse step a guess, so that is a hard error rather than a silent pick. - Restricting the scan to needed_triples matters for that check: elsewhere - in the tree one triple legitimately resolves two different ids on purpose - (e.g. Cubicon's xCeler line declares its own id per printer instead of - inheriting its root's, sanctioned in scripts/filament_id_snapshot.json) - and that pre-existing, Bambu-unrelated divergence must not block a map - that never reads it. - """ - needed_triples = set(needed_triples) - result = {} - for fid, triple_list in orca_analysis["triples"].items(): - if not OF_ID_RE.match(fid): - continue - for triple in triple_list: - triple = tuple(triple) - if triple not in needed_triples: - continue - owner = result.get(triple) - if owner and owner != fid: - raise SystemExit( - f"Orca triple {triple} resolves to more than one filament_id: " - f"{owner!r} and {fid!r}") - result[triple] = fid - return result - - # --------------------------------------------------------------------------- # Drift report (informational only) # --------------------------------------------------------------------------- @@ -269,16 +229,11 @@ def main(argv=None): print_error(e) raise SystemExit("unreadable BambuStudio filament profile(s)") - orca_analysis = analyze_tree(PROFILES_DIR) - needed_triples = {resolve_triple(rec["name"], bs_filaments, {}) - for rec in bs_filaments.values() if rec["instantiation"]} - orca_triples = orca_triples_from_analysis(orca_analysis, needed_triples) - - rows = derive_rows(bs_filaments, orca_triples) + rows = derive_rows(bs_filaments) write_map(args.output, rows, commit, datetime.date.today().isoformat()) print_success(f"wrote {len(rows)} row(s) to {args.output} (BambuStudio @ {commit})") - for line in drift_report(rows, orca_analysis): + for line in drift_report(rows, analyze_tree(PROFILES_DIR)): print_info(line) finally: if workdir: