mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-13 12:07:41 +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 |
|
| Color | never a new id |
|
||||||
| Second diameter of the same product (1.75 + 2.85) | sibling filament, 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" 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
|
## Structure rules
|
||||||
|
|
||||||
@@ -149,7 +149,9 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame
|
|||||||
are both the filament `MyBrand PLA`.
|
are both the filament `MyBrand PLA`.
|
||||||
3. **Within one filament, variants' `compatible_printers` are pairwise disjoint** — per printer,
|
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
|
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
|
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
|
`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)
|
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
|
by 35 bundles — most by independent declarations converging on the same mint, the rest
|
||||||
purely through inheritance from the OFL preset.
|
purely through inheritance from the OFL preset.
|
||||||
|
|
||||||
On the rare collision with an existing id, the minter salts the input (`…/1`,
|
Nothing but the triple feeds the mint — not the rest of the tree, not the snapshot, not what
|
||||||
`…/2`, …) until free, and the result is frozen in the profile file. Salting is also used
|
another preset of the product happens to carry. Determined triple, determined id: one product
|
||||||
deliberately: a *salt split* keeps two presets of one product on distinct ids where a single
|
carries one id and there is no second acceptable value for it, so any other value on a preset
|
||||||
id would be AMS-ambiguous on the same printer — the "selectable alongside" situation from the
|
is a mismatch `--check` reports and `--generate` pulls back. Two *different* products whose
|
||||||
table above, resolved without inventing a second filament name. The tooling recognizes salt
|
triples mint the same base62 value would be a collision (a roughly 36-bit id space against a
|
||||||
iterations of a triple as conformant and preserves such splits across re-mints.
|
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:
|
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_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
|
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
|
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
|
a preset of the same filament or to give the preset its own key. A declaration is left alone
|
||||||
product already holds and salts only past ids *other* products hold, so an id already equal to a
|
exactly when it already equals the one id its triple mints, and a collision (check 3d) is
|
||||||
salt iteration of its own triple is left alone and deliberate salt splits survive. The same run
|
reported and left unwritten. The same run assigns
|
||||||
assigns `generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process
|
`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
|
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
|
`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
|
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`.
|
- `--filament-id` limits the run to `filament_id`.
|
||||||
- `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to
|
- `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to
|
||||||
write both.
|
write both.
|
||||||
- `--vendor VENDOR` confines the run to that bundle; repeatable. The ids are still derived
|
- `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the
|
||||||
tree-wide, so a narrowed run writes exactly what a full one would — and reports any duplicate
|
triple alone, so a narrowed run writes exactly what a full one would; `--check` reports
|
||||||
it was not allowed to clear, since only a run covering both bundles can.
|
whatever it left outside.
|
||||||
- `--dry-run` reports what `--generate` would do and writes nothing; with no mode of its own it
|
- `--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.
|
implies `--generate`, so `--dry-run --vendor <Vendor>` previews just that bundle.
|
||||||
- `--profiles DIR` points the tooling at a different profile tree (default
|
- `--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`,
|
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
|
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.
|
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
|
A row's key is the id the product's `(filament_vendor, filament_type, filament name)` triple
|
||||||
`(filament_vendor, filament_type, filament name)` triple; a product we do not ship gets a freshly
|
mints — the same id any bundle of ours carries for it, since the id is a function of the triple
|
||||||
generated key and the row sits inert until some bundle claims that triple — `OFdyfQvU` /
|
alone; the row of a product we do not ship sits inert until some bundle claims that triple —
|
||||||
`GFG03`, "Bambu PETG Matte", is such a row today.
|
`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
|
**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:
|
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
|
its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change
|
||||||
surfaces as a snapshot diff.
|
surfaces as a snapshot diff.
|
||||||
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
|
- **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
|
one id its declarer's own triple mints, with no second acceptable value; the id an
|
||||||
*inherits* must equal the mint of *its* own triple, however it inherits it (a root, a real
|
instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits
|
||||||
filament, a library preset — structure rule 1); and every instantiated system filament must
|
it (a root, a real filament, a library preset — structure rule 1); and every instantiated
|
||||||
resolve an effective id at all (recall: an id-less one is a hard load error in C++ that
|
system filament must resolve an effective id at all (recall: an id-less one is a hard load
|
||||||
discards the whole vendor bundle). The errors print the expected id.
|
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,
|
- **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor,
|
||||||
BBL and Qidi included.
|
BBL and Qidi included.
|
||||||
- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and
|
- **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_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
|
||||||
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
|
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
|
||||||
8 chars total, which satisfies the AMS length limit. Nobody invents ids by
|
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
|
hand, and nothing but the triple feeds the mint — not the rest of the tree,
|
||||||
input is salted ("/1", "/2", ...) until free and the result is frozen in file.
|
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)
|
Identity changes (a filament rename, a filament_vendor/filament_type fix)
|
||||||
change the id BY DESIGN.
|
change the id BY DESIGN.
|
||||||
* Reserved id spaces that are never minted into or altered:
|
* 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
|
# Filament name = preset base name: strip the first "@..." suffix. The space before
|
||||||
# "@" is optional because names like "Afinia PLA@HS" exist.
|
# "@" is optional because names like "Afinia PLA@HS" exist.
|
||||||
BASE_NAME_RE = re.compile(r"\s?@.*$")
|
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.
|
# A JSON string literal, for the byte-preserving key edits.
|
||||||
_JSON_STR = r'"(?:[^"\\]|\\.)*"'
|
_JSON_STR = r'"(?:[^"\\]|\\.)*"'
|
||||||
|
|
||||||
@@ -190,43 +191,20 @@ def base_name(name):
|
|||||||
return BASE_NAME_RE.sub("", name, count=1)
|
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.
|
"""Deterministic "OF" + 6-char base62 filament_id for a filament product.
|
||||||
|
|
||||||
input = "filament_product/<filament_vendor>/<filament_type>/<filament_name>"
|
The triple is the only input: no salt, no state, no second value.
|
||||||
(+ "/<salt>" when salted); u = uuid5(FILAMENT_ID_NAMESPACE, input); the id
|
input = "filament_product/<filament_vendor>/<filament_type>/<filament_name>";
|
||||||
tail is the low FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"),
|
u = uuid5(FILAMENT_ID_NAMESPACE, input); the id tail is the low
|
||||||
most-significant first — the same derivation as generate_preset_setting_id.
|
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}"
|
key = f"filament_product/{filament_vendor}/{filament_type}/{filament_name}"
|
||||||
if salt:
|
|
||||||
key = f"{key}/{salt}"
|
|
||||||
u = uuid.uuid5(FILAMENT_ID_NAMESPACE, key)
|
u = uuid.uuid5(FILAMENT_ID_NAMESPACE, key)
|
||||||
return "OF" + _base62_tail(int.from_bytes(u.bytes, "big"), FILAMENT_ID_LENGTH)
|
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
|
# Tree loading + loader-faithful effective-id resolution
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -439,12 +417,15 @@ def analyze_tree(profiles_dir):
|
|||||||
triples = {} # fid -> set of triples of its declarers
|
triples = {} # fid -> set of triples of its declarers
|
||||||
declarer_triples = [] # (vendor, rec, fid, triple) per declarer
|
declarer_triples = [] # (vendor, rec, fid, triple) per declarer
|
||||||
filament_triples = {} # (vendor, filament_name) -> {triple: [declarers]}
|
filament_triples = {} # (vendor, filament_name) -> {triple: [declarers]}
|
||||||
|
mints = {} # minted id -> triples minting it (declarers + instantiated)
|
||||||
|
|
||||||
for vendor, filaments in vendors.items():
|
for vendor, filaments in vendors.items():
|
||||||
occurring = vendor_ids.setdefault(vendor, set())
|
occurring = vendor_ids.setdefault(vendor, set())
|
||||||
for rec in filaments.values():
|
for rec in filaments.values():
|
||||||
triple = resolve_triple(rec["name"], filaments, ofl_filaments)
|
triple = resolve_triple(rec["name"], filaments, ofl_filaments)
|
||||||
rec["triple"] = triple
|
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"):
|
if rec.get("filament_id"):
|
||||||
fid = rec["filament_id"]
|
fid = rec["filament_id"]
|
||||||
occurring.add(fid)
|
occurring.add(fid)
|
||||||
@@ -466,16 +447,6 @@ def analyze_tree(profiles_dir):
|
|||||||
if not rec.get("filament_id") and OF_ID_RE.match(eff):
|
if not rec.get("filament_id") and OF_ID_RE.match(eff):
|
||||||
inherited.append((vendor, rec, eff, triple))
|
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
|
# Cross-bundle triple divergence (check 5, warning only): the same filament
|
||||||
# name declared in several bundles with different triples cannot converge
|
# name declared in several bundles with different triples cannot converge
|
||||||
# on one id until the divergence is fixed.
|
# on one id until the divergence is fixed.
|
||||||
@@ -495,12 +466,13 @@ def analyze_tree(profiles_dir):
|
|||||||
"vendor_ids": vendor_ids,
|
"vendor_ids": vendor_ids,
|
||||||
"declared_ids": declared_ids,
|
"declared_ids": declared_ids,
|
||||||
"missing_effective": sorted(missing_effective),
|
"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()},
|
"triples": {fid: sorted(list(t) for t in ts) for fid, ts in triples.items()},
|
||||||
"triple_sets": triples,
|
|
||||||
"declarer_triples": declarer_triples,
|
"declarer_triples": declarer_triples,
|
||||||
"filament_triples": filament_triples,
|
"filament_triples": filament_triples,
|
||||||
"cross_bundle_triples": cross_bundle_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
|
2. Snapshot equality, both directions: every id in the tree, the filaments
|
||||||
claiming it and the triple its declarers resolve must equal the snapshot
|
claiming it and the triple its declarers resolve must equal the snapshot
|
||||||
entry exactly (the snapshot diff is the maintainer gate).
|
entry exactly (the snapshot diff is the maintainer gate).
|
||||||
3. Identity: the id is a function of the triple alone. (a) A declared id
|
3. Identity: the id is a function of the triple alone, and there is no
|
||||||
must equal the mint of the declarer's own triple or a salted iteration;
|
second acceptable value. (a) A declared id must equal the one id the
|
||||||
(b) the id an instantiated preset inherits must equal the mint of ITS
|
declarer's own triple mints; (b) the id an instantiated preset inherits
|
||||||
own triple — how it inherits it (a root, a real filament, an OFL
|
must equal the one ITS own triple mints — how it inherits it (a root, a
|
||||||
preset) is irrelevant; (c) every instantiated filament resolves an
|
real filament, an OFL preset) is irrelevant; (c) every instantiated
|
||||||
effective id at all (an id-less one is a hard load error in C++).
|
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
|
4. Reserved namespaces (GF*/QD_*/P-hex/"null", all ownerless) must not be
|
||||||
claimed by any vendor.
|
claimed by any vendor.
|
||||||
5. Triple integrity: (a) every declarer resolves non-empty filament_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
|
errors += 1
|
||||||
|
|
||||||
# -- 3. identity: the id is a function of the triple alone ---------------
|
# -- 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(
|
for vendor, rec, fid, triple in sorted(
|
||||||
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
|
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
|
||||||
if not OF_ID_RE.match(fid) or fid in mint_iterations(triple):
|
want = generate_filament_id(*triple)
|
||||||
continue
|
if not OF_ID_RE.match(fid) or fid == want:
|
||||||
|
continue # a non-OF id is check 1's error
|
||||||
print_error(
|
print_error(
|
||||||
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
|
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
|
||||||
f'not match the mint of its triple "{"/".join(triple)}": expected '
|
f'not match the mint of its triple "{"/".join(triple)}": expected '
|
||||||
f'"{generate_filament_id(*triple)}" (or a salted iteration); paste the '
|
f'"{want}"; paste the expected id, or fix the triple and run '
|
||||||
f'expected id, or fix the triple and run "{GENERATE_CMD} --vendor {vendor}" '
|
f'"{GENERATE_CMD} --vendor {vendor}" (preview with --dry-run), then '
|
||||||
f"(preview with --dry-run), then --update-snapshot")
|
f"--update-snapshot")
|
||||||
errors += 1
|
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(
|
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(
|
print_error(
|
||||||
f'preset "{rec["name"]}" ({rec["file"]}) inherits filament_id "{eff}" but '
|
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"a preset carries the id of its own product: inherit a preset of the "
|
||||||
f"same filament, or declare its own key")
|
f"same filament, or declare its own key")
|
||||||
errors += 1
|
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'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"in its inherits chain — this is a hard load error in the C++ loader; "
|
||||||
f'run "{GENERATE_CMD}" (expected id for filament '
|
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
|
errors += 1
|
||||||
|
|
||||||
# -- 4. reserved namespaces ----------------------------------------------
|
# -- 4. reserved namespaces ----------------------------------------------
|
||||||
@@ -993,75 +989,36 @@ def rewrite_filament_id(path, old_id, new_id, dry_run=False):
|
|||||||
# --generate
|
# --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):
|
def _incomplete_triple(triple):
|
||||||
"""The name(s) of the empty mint-key fields, or "" when both are present."""
|
"""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]),
|
return " and ".join(k for k, v in (("filament_vendor", triple[0]),
|
||||||
("filament_type", triple[1])) if not v)
|
("filament_type", triple[1])) if not v)
|
||||||
|
|
||||||
|
|
||||||
def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False,
|
||||||
vendors=None, dry_run=False, changed_paths=None):
|
changed_paths=None):
|
||||||
"""Make every filament carry the id its own triple mints.
|
"""Make every filament carry the id its own triple mints.
|
||||||
|
|
||||||
One rule, applied to declarations and to id-less filaments alike:
|
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 declared id that is not the one its own triple mints — a wrong OF id,
|
||||||
a wrong OF id, or a foreign one such as a Bambu "GF*" arriving with an
|
or a foreign one such as a Bambu "GF*" arriving with an upstream sync —
|
||||||
upstream sync — is replaced in place;
|
is replaced in place;
|
||||||
* an instantiated filament that resolves no id at all gets one inserted
|
* 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
|
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
|
inherit, or the member itself (a parent of another filament cannot carry
|
||||||
this filament's id — check 3).
|
this filament's id — check 3).
|
||||||
An id already equal to ANY salt iteration of its own triple is conformant
|
A declaration is left alone exactly when it already equals the one id its
|
||||||
(check 3) and left alone, so deliberate salt splits — distinct presets of one
|
triple mints (check 3). Two products minting one id (check 3d) are reported
|
||||||
product kept apart for per-printer AMS matching — survive.
|
and left unwritten: nothing salts past a collision, a rename resolves it.
|
||||||
|
|
||||||
`vendors` restricts what is WRITTEN; the analysis and the id policy always
|
`vendors` restricts what is WRITTEN; the id is a function of the triple
|
||||||
span the whole tree, so a narrowed run mints exactly what a full one would.
|
alone, so a narrowed run writes exactly what a full one would, and --check
|
||||||
`changed_paths`, when a set is passed, collects the files that changed. A
|
reports whatever it was not allowed to touch. `changed_paths`, when a set is
|
||||||
file whose layout offers no anchor for the edit is reported and counted as an
|
passed, collects the files that changed. A file whose layout offers no
|
||||||
error, so one odd profile cannot abort the pass over all the others.
|
anchor for the edit is reported and counted as an error, so one odd profile
|
||||||
Never touches the snapshot — run --update-snapshot afterwards and review the
|
cannot abort the pass over all the others. Never reads or touches the
|
||||||
diff. Returns (files_changed, errors).
|
snapshot — run --update-snapshot afterwards and review the diff. Returns
|
||||||
|
(files_changed, errors).
|
||||||
"""
|
"""
|
||||||
_utf8_console()
|
_utf8_console()
|
||||||
analysis = analyze_tree(profiles_dir)
|
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}')
|
print_error(f'unknown vendor "{v}" in {profiles_dir}')
|
||||||
return 0, errors + len(unknown)
|
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 ""
|
verb = "would " if dry_run else ""
|
||||||
files_changed = 0
|
files_changed = 0
|
||||||
reminted = 0
|
reminted = 0
|
||||||
inserted = 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.
|
# 1. Declarations that are not the mint of their own triple.
|
||||||
for vendor, rec, fid, triple in sorted(
|
for vendor, rec, fid, triple in sorted(
|
||||||
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
|
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
|
||||||
in_scope = wanted is None or vendor in wanted
|
want = generate_filament_id(*triple)
|
||||||
if fid in mint_iterations(triple):
|
if (fid == want or (wanted is not None and vendor not in wanted)
|
||||||
if in_scope:
|
or triple in colliding):
|
||||||
held_here.add(fid)
|
|
||||||
continue
|
|
||||||
if not in_scope:
|
|
||||||
left_standing.setdefault(fid, []).append(rec["file"])
|
|
||||||
continue
|
continue
|
||||||
missing = _incomplete_triple(triple)
|
missing = _incomplete_triple(triple)
|
||||||
if missing:
|
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'{missing}; the mint key needs both (generic materials use '
|
||||||
f'filament_vendor "Generic")')
|
f'filament_vendor "Generic")')
|
||||||
errors += 1
|
errors += 1
|
||||||
left_standing.setdefault(fid, []).append(rec["file"])
|
|
||||||
continue
|
|
||||||
want = want_id(triple)
|
|
||||||
if fid == want:
|
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
rewrite_filament_id(rec["path"], fid, want, dry_run)
|
rewrite_filament_id(rec["path"], fid, want, dry_run)
|
||||||
except (OSError, RuntimeError, ValueError) as e:
|
except (OSError, RuntimeError, ValueError) as e:
|
||||||
print_error(str(e))
|
print_error(str(e))
|
||||||
errors += 1
|
errors += 1
|
||||||
left_standing.setdefault(fid, []).append(rec["file"])
|
|
||||||
continue
|
continue
|
||||||
files_changed += 1
|
files_changed += 1
|
||||||
reminted += 1
|
reminted += 1
|
||||||
held_here.add(want)
|
|
||||||
if changed_paths is not None:
|
if changed_paths is not None:
|
||||||
# Index sub_paths are "/"-joined even on Windows, where the
|
# Index sub_paths are "/"-joined even on Windows, where the
|
||||||
# setting_id pass reaches the same file through os.walk: normalize
|
# 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
|
errors += 1
|
||||||
continue
|
continue
|
||||||
triple = (*next(iter(fields)), filament_name)
|
triple = (*next(iter(fields)), filament_name)
|
||||||
|
if triple in colliding:
|
||||||
|
continue # reported above
|
||||||
missing = _incomplete_triple(triple)
|
missing = _incomplete_triple(triple)
|
||||||
if missing:
|
if missing:
|
||||||
print_error(
|
print_error(
|
||||||
@@ -1174,7 +1129,7 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
|
|||||||
f'filament_vendor "Generic")')
|
f'filament_vendor "Generic")')
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
new_id = want_id(triple)
|
new_id = generate_filament_id(*triple)
|
||||||
for name in sorted(roots):
|
for name in sorted(roots):
|
||||||
root = roots[name]
|
root = roots[name]
|
||||||
try:
|
try:
|
||||||
@@ -1185,24 +1140,11 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
|
|||||||
continue
|
continue
|
||||||
files_changed += 1
|
files_changed += 1
|
||||||
inserted += 1
|
inserted += 1
|
||||||
held_here.add(new_id)
|
|
||||||
if changed_paths is not None:
|
if changed_paths is not None:
|
||||||
changed_paths.add(os.path.normpath(root["path"]))
|
changed_paths.add(os.path.normpath(root["path"]))
|
||||||
print_info(f'{verb}insert filament "{vendor}/{filament_name}": filament_id '
|
print_info(f'{verb}insert filament "{vendor}/{filament_name}": filament_id '
|
||||||
f'"{new_id}" -> {root["file"]}')
|
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 inserted : {inserted}")
|
||||||
print_info(f"filament_ids re-minted : {reminted}")
|
print_info(f"filament_ids re-minted : {reminted}")
|
||||||
return files_changed, errors
|
return files_changed, errors
|
||||||
@@ -1376,10 +1318,10 @@ def build_parser():
|
|||||||
narrow.add_argument("--setting-id", action="store_true",
|
narrow.add_argument("--setting-id", action="store_true",
|
||||||
help="write setting_id only, skipping filament_id")
|
help="write setting_id only, skipping filament_id")
|
||||||
narrow.add_argument("--vendor", metavar="VENDOR", action="append", default=[],
|
narrow.add_argument("--vendor", metavar="VENDOR", action="append", default=[],
|
||||||
help="write only in this vendor bundle; repeatable. The ids are "
|
help="write only in this vendor bundle; repeatable. The id is a "
|
||||||
"still derived tree-wide, so a narrowed run writes exactly "
|
"function of the triple alone, so a narrowed run writes "
|
||||||
"what a full one would, and reports what it was not allowed "
|
"exactly what a full one would; --check reports whatever "
|
||||||
"to fix")
|
"it left outside")
|
||||||
parser.add_argument("--dry-run", "--dryrun", dest="dry_run", action="store_true",
|
parser.add_argument("--dry-run", "--dryrun", dest="dry_run", action="store_true",
|
||||||
help="report what would change and write nothing; with no mode of "
|
help="report what would change and write nothing; with no mode of "
|
||||||
"its own it previews --generate")
|
"its own it previews --generate")
|
||||||
@@ -1462,7 +1404,7 @@ def main(argv=None):
|
|||||||
filament_files = errors = 0
|
filament_files = errors = 0
|
||||||
if do_filament:
|
if do_filament:
|
||||||
filament_files, e = generate_filament_ids(
|
filament_files, e = generate_filament_ids(
|
||||||
args.profiles, snapshot_path, vendors, args.dry_run, changed)
|
args.profiles, vendors, args.dry_run, changed)
|
||||||
errors += e
|
errors += e
|
||||||
if do_setting:
|
if do_setting:
|
||||||
_n, e = generate_setting_ids(args.profiles, vendors, args.dry_run, changed)
|
_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):
|
def assign(self, vendors=None, dry_run=False):
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
with contextlib.redirect_stdout(buf):
|
with contextlib.redirect_stdout(buf):
|
||||||
changed, errors = afi.generate_filament_ids(
|
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
|
||||||
self.profiles, self.snapshot, vendors, dry_run)
|
|
||||||
return changed, errors, buf.getvalue()
|
return changed, errors, buf.getvalue()
|
||||||
|
|
||||||
def remint(self, vendors, dry_run=False):
|
def remint(self, vendors, dry_run=False):
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
with contextlib.redirect_stdout(buf):
|
with contextlib.redirect_stdout(buf):
|
||||||
changed, errors = afi.generate_filament_ids(
|
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
|
||||||
self.profiles, self.snapshot, vendors, dry_run)
|
|
||||||
return changed, errors, buf.getvalue()
|
return changed, errors, buf.getvalue()
|
||||||
|
|
||||||
def cli(self, *flags):
|
def cli(self, *flags):
|
||||||
@@ -240,8 +238,6 @@ class TestMint(unittest.TestCase):
|
|||||||
# layout ("filament_product/<vendor>/<type>/<name>") and base62 tail.
|
# layout ("filament_product/<vendor>/<type>/<name>") and base62 tail.
|
||||||
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA"),
|
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA"),
|
||||||
"OF5CgdDq")
|
"OF5CgdDq")
|
||||||
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA",
|
|
||||||
salt=1), "OFD9mV8H")
|
|
||||||
self.assertEqual(afi.generate_filament_id("Generic", "PLA", "Generic PLA"),
|
self.assertEqual(afi.generate_filament_id("Generic", "PLA", "Generic PLA"),
|
||||||
"OFDSrzZ8")
|
"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", "PETG", "PolyLite PLA"))
|
||||||
self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA Pro"))
|
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):
|
class TestBaseName(unittest.TestCase):
|
||||||
def test_filament_name_derivation(self):
|
def test_filament_name_derivation(self):
|
||||||
@@ -589,12 +496,13 @@ class TestChecks(OfCleanTreeCase):
|
|||||||
self.assertIn('which records "AVendor/PLA/APLA"', out)
|
self.assertIn('which records "AVendor/PLA/APLA"', out)
|
||||||
# Sanctioning the new triple is not enough: the old id is no longer its
|
# 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,
|
# 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()
|
rc, _out = self.t.update_snapshot()
|
||||||
self.assertEqual(rc, 0)
|
self.assertEqual(rc, 0)
|
||||||
errors, out = self.t.check()
|
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("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"])
|
_changed, errors, out = self.t.remint(["VendorA"])
|
||||||
self.assertEqual(errors, 0, out)
|
self.assertEqual(errors, 0, out)
|
||||||
rc, _out = self.t.update_snapshot()
|
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("does not match the mint of its triple", out)
|
||||||
self.assertIn(afi.generate_filament_id("BV", "PLA", "BNEW"), 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):
|
def test_check3_no_grandfathering_of_a_wrong_declaration(self):
|
||||||
# Sanctioning the tree does not excuse a declaration from its mint.
|
# Sanctioning the tree does not excuse a declaration from its mint.
|
||||||
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ",
|
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ",
|
||||||
@@ -633,8 +531,9 @@ class TestChecks(OfCleanTreeCase):
|
|||||||
rc, _out = self.t.update_snapshot()
|
rc, _out = self.t.update_snapshot()
|
||||||
self.assertEqual(rc, 0)
|
self.assertEqual(rc, 0)
|
||||||
errors, out = self.t.check()
|
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("does not match the mint of its triple", out)
|
||||||
|
self.assertIn('"CNEW @P1" (VendorA/filament/CNEW @P1.json) inherits filament_id', out)
|
||||||
|
|
||||||
def test_check3_inherited_id_must_be_the_mint_of_own_triple(self):
|
def test_check3_inherited_id_must_be_the_mint_of_own_triple(self):
|
||||||
# A preset of another filament inheriting APLA's root takes APLA's id,
|
# 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()
|
errors, out = self.t.check()
|
||||||
self.assertEqual(errors, 1, out)
|
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):
|
def test_check3_renamed_tuned_generic_is_an_identity_error(self):
|
||||||
# Riding the OFL generic under another base name: same rule, same error.
|
# Riding the OFL generic under another base name: same rule, same error.
|
||||||
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
|
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
|
||||||
@@ -1144,9 +1115,9 @@ class TestAssign(OfCleanTreeCase):
|
|||||||
self.assertEqual(b2["filament_id"], want)
|
self.assertEqual(b2["filament_id"], want)
|
||||||
|
|
||||||
def test_converges_on_an_existing_tree_id_for_the_same_triple(self):
|
def test_converges_on_an_existing_tree_id_for_the_same_triple(self):
|
||||||
# BEHAVIOUR CHANGE: an id-less filament whose product is already shipped
|
# An id-less filament whose product is already shipped (with its
|
||||||
# (with its conforming id) in another bundle converges on that id
|
# conforming id) in another bundle converges on that id. One product,
|
||||||
# instead of salting past it. One product, one id, in every bundle.
|
# one id, in every bundle.
|
||||||
want = afi.generate_filament_id("CV", "PLA", "CPLA")
|
want = afi.generate_filament_id("CV", "PLA", "CPLA")
|
||||||
self.t.add_vendor("VendorB", [
|
self.t.add_vendor("VendorB", [
|
||||||
preset("CPLA @base", filament_id=want, instantiation=False,
|
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):
|
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" —
|
# 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
|
# a copy-paste, not a claim. The id is the mint of DPLA's triple and
|
||||||
# own triple. That declaration is transient (--generate rewrites it), so
|
# nothing else, so DPLA gets it whatever VendorB carries.
|
||||||
# it must NOT push DPLA onto a salted id: DPLA gets its canonical one.
|
|
||||||
want0 = afi.generate_filament_id("DV", "PLA", "DPLA")
|
want0 = afi.generate_filament_id("DV", "PLA", "DPLA")
|
||||||
self.t.add_vendor("VendorB", [
|
self.t.add_vendor("VendorB", [
|
||||||
preset("Other @base", filament_id=want0, instantiation=False,
|
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",
|
self.t.write_preset("VendorA", preset("DPLA @P1", inherits="DPLA @base",
|
||||||
compatible_printers=["P1"]))
|
compatible_printers=["P1"]))
|
||||||
changed, errors, out = self.t.assign(["VendorA"])
|
changed, errors, out = self.t.assign(["VendorA"])
|
||||||
# VendorB is outside the write set, so the id DPLA just took is still
|
self.assertEqual((changed, errors), (1, 0), out)
|
||||||
# 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))
|
|
||||||
root = load_json_file(self.t.preset_path("VendorA", "DPLA @base"))
|
root = load_json_file(self.t.preset_path("VendorA", "DPLA @base"))
|
||||||
self.assertEqual(root["filament_id"], want0)
|
self.assertEqual(root["filament_id"], want0)
|
||||||
# A full run re-mints Other to its own triple and the duplicate is gone.
|
# VendorB's declaration is VendorB's own mismatch: a run not allowed to
|
||||||
other = load_json_file(self.t.preset_path("VendorB", "Other @base"))
|
# touch it leaves it to --check, and a run over VendorB re-mints it.
|
||||||
self.assertEqual(other["filament_id"], want0)
|
_errors, out = self.t.check()
|
||||||
changed, errors, out = self.t.assign()
|
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)
|
self.assertEqual((changed, errors), (1, 0), out)
|
||||||
other = load_json_file(self.t.preset_path("VendorB", "Other @base"))
|
other = load_json_file(self.t.preset_path("VendorB", "Other @base"))
|
||||||
self.assertEqual(other["filament_id"],
|
self.assertEqual(other["filament_id"],
|
||||||
afi.generate_filament_id("OV", "ABS", "Other"))
|
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):
|
def test_mismatching_of_declaration_is_re_derived(self):
|
||||||
# BEHAVIOUR CHANGE: the default run used to leave an OF-format id alone
|
# One rule: an id that is not the mint of its own triple is rewritten.
|
||||||
# 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.
|
|
||||||
self.t.write_preset("VendorA", preset("KNEW @base", filament_id="OFZZZZZZ",
|
self.t.write_preset("VendorA", preset("KNEW @base", filament_id="OFZZZZZZ",
|
||||||
instantiation=False,
|
instantiation=False,
|
||||||
filament_vendor="KV",
|
filament_vendor="KV",
|
||||||
@@ -1445,20 +1439,19 @@ class TestRemint(SyntheticTreeCase):
|
|||||||
changed, errors, _out = self.t.remint(["VendorA"])
|
changed, errors, _out = self.t.remint(["VendorA"])
|
||||||
self.assertEqual((changed, errors), (0, 0))
|
self.assertEqual((changed, errors), (0, 0))
|
||||||
|
|
||||||
def test_salted_declaration_survives_remint(self):
|
def test_second_id_of_one_product_is_converged_by_remint(self):
|
||||||
# A deliberate salt split (a second preset of one product kept on its
|
# A second preset of one product kept on an id of its own is not a
|
||||||
# own id for per-printer AMS disambiguation) is mint-conformant and
|
# sanctioned split: the triple determines the id, so --generate pulls it
|
||||||
# must not be converged back onto salt 0.
|
# back. An AMS ambiguity this exposes is fixed in the profiles instead.
|
||||||
salt1 = afi.generate_filament_id(*self.TRIPLE, salt=1)
|
want = afi.generate_filament_id(*self.TRIPLE)
|
||||||
self.t.write_preset("VendorA", preset("APLA @legacy",
|
self.t.write_preset("VendorA", preset("APLA @legacy", filament_id="OFlegac1",
|
||||||
filament_id=salt1,
|
|
||||||
inherits="APLA @base",
|
inherits="APLA @base",
|
||||||
compatible_printers=["P2 0.4 nozzle"]))
|
compatible_printers=["P2 0.4 nozzle"]))
|
||||||
changed, errors, out = self.t.remint(["VendorA"])
|
changed, errors, out = self.t.remint(["VendorA"])
|
||||||
self.assertEqual(errors, 0, out)
|
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"))
|
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):
|
def test_same_triple_converges_within_run(self):
|
||||||
self.t.add_vendor("VendorB", [
|
self.t.add_vendor("VendorB", [
|
||||||
@@ -1490,22 +1483,24 @@ class TestRemint(SyntheticTreeCase):
|
|||||||
self.assertEqual(a["filament_id"], want)
|
self.assertEqual(a["filament_id"], want)
|
||||||
|
|
||||||
def test_not_blocked_by_a_non_conformant_occurrence(self):
|
def test_not_blocked_by_a_non_conformant_occurrence(self):
|
||||||
# VendorB carries APLA's id under a PETG triple of its own, so that
|
# VendorB carries APLA's id under a PETG triple of its own: that
|
||||||
# declaration is wrong and this run's job is to rewrite it. It must not
|
# declaration is wrong, and it costs VendorA's APLA nothing — the id is
|
||||||
# cost VendorA's APLA the id its triple actually mints.
|
# the mint of APLA's triple, whoever else is squatting on it.
|
||||||
want0 = afi.generate_filament_id(*self.TRIPLE)
|
want0 = afi.generate_filament_id(*self.TRIPLE)
|
||||||
self.t.add_vendor("VendorB", [
|
self.t.add_vendor("VendorB", [
|
||||||
preset("BPLA @base", filament_id=want0, instantiation=False,
|
preset("BPLA @base", filament_id=want0, instantiation=False,
|
||||||
filament_vendor="BV", filament_type="PETG"),
|
filament_vendor="BV", filament_type="PETG"),
|
||||||
])
|
])
|
||||||
changed, errors, out = self.t.remint(["VendorA"])
|
changed, errors, out = self.t.remint(["VendorA"])
|
||||||
self.assertEqual((changed, errors), (1, 1), out)
|
self.assertEqual((changed, errors), (1, 0), out)
|
||||||
self.assertIn("declared by VendorB/filament/BPLA @base.json", out)
|
|
||||||
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
||||||
self.assertEqual(root["filament_id"], want0)
|
self.assertEqual(root["filament_id"], want0)
|
||||||
# Widening the run to both bundles clears it.
|
# A run over VendorB re-mints the squatter to its own triple.
|
||||||
changed, errors, out = self.t.remint(["VendorA", "VendorB"])
|
changed, errors, out = self.t.remint(["VendorB"])
|
||||||
self.assertEqual((changed, errors), (1, 0), out)
|
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):
|
def test_bbl_is_reminted_like_any_vendor(self):
|
||||||
self.t.add_vendor("BBL", [
|
self.t.add_vendor("BBL", [
|
||||||
@@ -1578,8 +1573,7 @@ class TestCli(unittest.TestCase):
|
|||||||
# Named explicitly, both modes run against that tree.
|
# Named explicitly, both modes run against that tree.
|
||||||
rc, out = self.t.cli("--update-snapshot")
|
rc, out = self.t.cli("--update-snapshot")
|
||||||
self.assertEqual(rc, 0, out)
|
self.assertEqual(rc, 0, out)
|
||||||
# --generate only reads the snapshot as a list of ids not to reuse, so
|
# --generate never reads the snapshot, so it keeps working without one.
|
||||||
# it keeps working without one.
|
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
with contextlib.redirect_stdout(buf):
|
with contextlib.redirect_stdout(buf):
|
||||||
rc = afi.main(["--dry-run", "--profiles", self.t.profiles])
|
rc = afi.main(["--dry-run", "--profiles", self.t.profiles])
|
||||||
@@ -1762,8 +1756,7 @@ class TestReviewFixes(OfCleanTreeCase):
|
|||||||
touched = set()
|
touched = set()
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
with contextlib.redirect_stdout(buf):
|
with contextlib.redirect_stdout(buf):
|
||||||
afi.generate_filament_ids(self.t.profiles, self.t.snapshot,
|
afi.generate_filament_ids(self.t.profiles, None, False, touched)
|
||||||
None, False, touched)
|
|
||||||
afi.generate_setting_ids(self.t.profiles, None, False, touched)
|
afi.generate_setting_ids(self.t.profiles, None, False, touched)
|
||||||
for name in ("SoloPLA @P1", "WrongPLA @P1"):
|
for name in ("SoloPLA @P1", "WrongPLA @P1"):
|
||||||
self.assertEqual([t for t in touched if t.endswith(name + ".json")],
|
self.assertEqual([t for t in touched if t.endswith(name + ".json")],
|
||||||
@@ -1841,58 +1834,21 @@ class TestBambuMap(unittest.TestCase):
|
|||||||
return filaments
|
return filaments
|
||||||
|
|
||||||
def test_one_row_per_filament(self):
|
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"):
|
self.assertEqual(rows, {afi.generate_filament_id("Bambu Lab", "ABS", "Bambu ABS"):
|
||||||
{"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}})
|
{"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}})
|
||||||
|
|
||||||
def test_shared_bambu_id_is_an_error(self):
|
def test_shared_bambu_id_is_an_error(self):
|
||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
ubfi.derive_rows(self._bs_tree([("A", "GFX00", "V", "PLA"), ("B", "GFX00", "V", "PLA")]), {})
|
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)
|
|
||||||
|
|
||||||
@unittest.skipUnless(os.path.isdir("/Users/lijiang/codes/BambuStudio/resources/profiles"), "no local clone")
|
@unittest.skipUnless(os.path.isdir("/Users/lijiang/codes/BambuStudio/resources/profiles"), "no local clone")
|
||||||
def test_local_clone_yields_the_catalog(self):
|
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(rows), 100)
|
||||||
self.assertEqual(len({r["bambu_id"] for r in rows.values()}), 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):
|
class TestWriteMap(unittest.TestCase):
|
||||||
def test_format(self):
|
def test_format(self):
|
||||||
d = tempfile.mkdtemp(prefix="bambu_map_test_")
|
d = tempfile.mkdtemp(prefix="bambu_map_test_")
|
||||||
|
|||||||
@@ -141,13 +141,10 @@ class SettingTree:
|
|||||||
return changed, errors, buf.getvalue()
|
return changed, errors, buf.getvalue()
|
||||||
|
|
||||||
def run_filament_ids(self, vendors=None, dry_run=False):
|
def run_filament_ids(self, vendors=None, dry_run=False):
|
||||||
"""The OTHER half of --generate, against this tree's own (absent)
|
"""The OTHER half of --generate."""
|
||||||
snapshot, so the id policy never consults the repo's real one."""
|
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
with contextlib.redirect_stdout(buf):
|
with contextlib.redirect_stdout(buf):
|
||||||
changed, errors = afi.generate_filament_ids(
|
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
|
||||||
self.profiles, os.path.join(self.dir, "filament_id_snapshot.json"),
|
|
||||||
vendors, dry_run)
|
|
||||||
return changed, errors, buf.getvalue()
|
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
|
One row per BambuStudio filament PRODUCT: one named spool product = one
|
||||||
"@base"-declared filament_id, shared by every per-printer/per-nozzle
|
"@base"-declared filament_id, shared by every per-printer/per-nozzle
|
||||||
instantiation of it (BambuStudio follows the same one-product-one-id shape
|
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
|
Orca's own filament_id policy does). A row's key is the OF id that product's
|
||||||
already ships for that same (filament_vendor, filament_type, filament) triple;
|
(filament_vendor, filament_type, filament) triple mints — the id Orca carries
|
||||||
a triple Orca does not ship anywhere yet gets a freshly generated one.
|
for it wherever it ships it, since the id is a function of the triple alone.
|
||||||
|
|
||||||
Map format:
|
Map format:
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||||||
from orca_id_tool import ( # noqa: E402
|
from orca_id_tool import ( # noqa: E402
|
||||||
BAMBU_MAP_PATH,
|
BAMBU_MAP_PATH,
|
||||||
OFL,
|
OFL,
|
||||||
OF_ID_RE,
|
|
||||||
PROFILES_DIR,
|
PROFILES_DIR,
|
||||||
analyze_tree,
|
analyze_tree,
|
||||||
base_name,
|
base_name,
|
||||||
@@ -77,8 +76,8 @@ BAMBUSTUDIO_REPO = "https://github.com/bambulab/BambuStudio"
|
|||||||
# Row derivation
|
# Row derivation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def derive_rows(bs_filaments, orca_triples):
|
def derive_rows(bs_filaments):
|
||||||
"""orca_triples: {(vendor, type, name): orca_id} from analyze_tree()["triples"]."""
|
"""One row per BambuStudio product, keyed by the OF id its triple mints."""
|
||||||
by_filament = {} # filament_name -> (bambu_id, triple)
|
by_filament = {} # filament_name -> (bambu_id, triple)
|
||||||
for rec in bs_filaments.values():
|
for rec in bs_filaments.values():
|
||||||
if not rec["instantiation"]:
|
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 "
|
raise SystemExit(f"Bambu id {bambu_id} is shared by "
|
||||||
f"{seen[bambu_id]!r} and {filament_name!r}")
|
f"{seen[bambu_id]!r} and {filament_name!r}")
|
||||||
seen[bambu_id] = filament_name
|
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[generate_filament_id(*triple)] = {
|
||||||
rows[orca_id] = {"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]}
|
"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]}
|
||||||
return rows
|
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)
|
# Drift report (informational only)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -269,16 +229,11 @@ def main(argv=None):
|
|||||||
print_error(e)
|
print_error(e)
|
||||||
raise SystemExit("unreadable BambuStudio filament profile(s)")
|
raise SystemExit("unreadable BambuStudio filament profile(s)")
|
||||||
|
|
||||||
orca_analysis = analyze_tree(PROFILES_DIR)
|
rows = derive_rows(bs_filaments)
|
||||||
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)
|
|
||||||
write_map(args.output, rows, commit, datetime.date.today().isoformat())
|
write_map(args.output, rows, commit, datetime.date.today().isoformat())
|
||||||
print_success(f"wrote {len(rows)} row(s) to {args.output} (BambuStudio @ {commit})")
|
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)
|
print_info(line)
|
||||||
finally:
|
finally:
|
||||||
if workdir:
|
if workdir:
|
||||||
|
|||||||
Reference in New Issue
Block a user