mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-08 09:46:55 +00:00
Hold every filament product to exactly one id, with no exceptions
filament_id is the plain mint of the product triple (filament_vendor, filament_type, filament name), and nothing else feeds it. The tooling used to accept any salt iteration of a preset's own triple, and its minting policy stepped past ids that other products held in the tree or in the snapshot, so which id a product got could depend on history. Every "salt split" in the tree masked a redundant preset rather than a real need, and no shipped id is salted, so salting goes entirely: no salt parameter, no id policy object, and --generate no longer reads the snapshot. --check now holds every declared and every inherited id to that one value and lists each preset that misses it, variants under a wrong root included, instead of folding them into the root's error. Two products whose triples mint one id is reported as a collision naming both, and --generate refuses to write it; the remedy is a rename so the triples differ. The Bambu catalog map generator keys its rows by the same mint rather than by what the tree already ships. No id changes: all 913 ids in the tree are already the mint of their triple, so --generate is a no-op and the snapshot is untouched.
This commit is contained in:
@@ -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 <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 <a BambuStudio resources/profiles checkout>`. 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
|
||||
|
||||
@@ -24,8 +24,11 @@ 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; 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/<filament_vendor>/<filament_type>/<filament_name>"
|
||||
(+ "/<salt>" 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/<filament_vendor>/<filament_type>/<filament_name>";
|
||||
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)
|
||||
|
||||
@@ -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/<vendor>/<type>/<name>") 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_")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user