Stop requiring a filament id snapshot update when filaments change

This commit is contained in:
SoftFever
2026-09-18 11:20:57 +08:00
parent 52f4c68c41
commit a77209af8f
13 changed files with 156 additions and 8454 deletions
+3 -6
View File
@@ -1,6 +1,6 @@
---
name: orca-profiles
description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id, filament_id and filament_id_snapshot.json. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job.
description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job.
---
# OrcaSlicer system profiles
@@ -21,7 +21,7 @@ Paths below are relative to this skill. Commands run from the repository root.
| Add a printer or nozzle; change models, variants, assets or extruder vectors | [machine-profiles.md](references/machine-profiles.md) |
| Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) |
| Create a vendor bundle; diagnose loading or inheritance; migrate preset names | [vendor-bundle.md](references/vendor-bundle.md) |
| Change ids or snapshot claims; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes |
| Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes |
| Review a profile diff | [review-checklist.md](references/review-checklist.md) |
| Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) |
@@ -71,14 +71,11 @@ Paths below are relative to this skill. Commands run from the repository root.
python3 scripts/orca_profile_tool.py normalize --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py update-index --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py generate-id --vendor "<Vendor>"
python3 scripts/orca_profile_tool.py update-snapshot
python3 scripts/orca_profile_tool.py check
```
Writing commands support `--dry-run`. Inspect their diffs: `normalize` changes content and can
reformat entire files. `update-snapshot` is tree-wide; include its diff whenever a filament id
**or claim** changes, even if no new id was minted. Skip it when filament identity and claims
are unchanged. Stop and resolve command errors before proceeding.
reformat entire files. Stop and resolve command errors before proceeding.
**Do not use `trim` in this workflow:** it can delete newly authored, unindexed profiles.
Do not use `normalize --force` for routine edits.
+14 -48
View File
@@ -6,7 +6,7 @@ Orca-generated ids are deterministic hashes of identity. **Never invent an id or
[BBL's authoritative setting ids](#bbls-exception-precisely).
`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id` — the id landscape, the
snapshot as the maintainer gate, and the Bambu catalog map. This page is the tooling half.
checks CI runs, and the Bambu catalog map. This page is the tooling half.
| | `setting_id` | `filament_id` |
| --- | --- | --- |
@@ -32,12 +32,10 @@ Use `scripts/orca_profile_tool.py` with a subcommand:
| `normalize` | rewrites profile files into their canonical shape |
| `trim` | deletes profile files no `<vendor>.json` list references |
| `update-index` | rebuilds the `*_list` sections from the files on disk |
| `update-snapshot` | re-records `scripts/filament_id_snapshot.json` |
The order after adding, renaming or deleting files — each step feeds the next, so it is not
interchangeable — is `normalize``update-index``generate-id` `update-snapshot` `check`.
The [authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands;
`update-snapshot` is needed when filament ids or claims change.
interchangeable — is `normalize``update-index``generate-id``check`.
The [authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands.
> **`trim` deletes.** It removes every profile file the index does not list — including the one you just
> added and have not registered yet. Register first, or skip `trim` entirely; it is a cleanup sweep, not
@@ -48,15 +46,15 @@ filesystem (the `setting_id` pass walks the filesystem, so a bundle whose index
still assignable). A new filament file is therefore invisible to `generate-id`'s filament_id pass until
it is registered — its `setting_id` is written regardless.
- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`,
`update-snapshot`) and writes nothing.
- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`)
and writes nothing.
- `--filament-id` / `--setting-id` narrow `generate-id`; they exclude each other, and passing neither
writes both.
- `--vendor` is repeatable and narrows **only what is written** — the id is a function of the triple
alone, so a narrowed run writes exactly what a full run would. An unknown vendor exits 1 before any
write. `--vendor` on `check` narrows the per-vendor checks only; the `setting_id` and `filament_id`
passes stay tree-wide. `update-snapshot` takes no `--vendor` at all.
- `--profiles DIR` points any command at another tree — with the `--snapshot` companion rule, see
passes stay tree-wide.
- `--profiles DIR` points any command at another tree — see
[Checking a copy of the tree](validation.md#checking-a-copy-of-the-tree).
- `--profile-type` narrows `normalize`, `trim` and `update-index` to `machine_model`, `process`,
`filament` or `machine`.
@@ -71,8 +69,8 @@ whole files into canonical shape — which is why `check` demands it already be
CRLF committed (OrcaFilamentLibrary, Anycubic and RH3D among them), so a `normalize` pass there rewrites
every line — read the diff before committing it.
On a clean tree `check`, `generate-id --dry-run` and `update-snapshot --dry-run` all exit 0 with zero
findings. That is the baseline to restore before opening a PR.
On a clean tree `check` and `generate-id --dry-run` both exit 0 with zero findings. That is the
baseline to restore before opening a PR.
## What `generate-id` does and does not fix
@@ -88,7 +86,7 @@ Refuses to write (reports only): a base62 collision between two products, an emp
`filament_type`, a broken `inherits` chain, roots of one filament resolving divergent `(vendor, type)`
pairs.
**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 3b, and it is the trap
**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b, and it is the trap
most likely to bite. It happens when a branded filament inherits a generic for its settings:
```jsonc
@@ -107,8 +105,8 @@ Two fixes, in order of preference:
2. **Declare the tool-computed key on the preset itself.** Use the expected value reported by `check`
or compute it with the function below; this is not a manually chosen id. Make sure the preset
resolves the right `filament_vendor` and `filament_type` first — with
neither set, the triple resolves through the generic parent and the branded product is minted, and
then sanctioned in the snapshot, under vendor `Generic`. If you need the id before the file exists:
neither set, the triple resolves through the generic parent and the branded product is minted
under vendor `Generic`. If you need the id before the file exists:
```bash
python3 -c "import sys; sys.path.insert(0,'scripts'); from orca_profile_tool import generate_filament_id as g; print(g('Polymaker','PLA','PolyLite PLA'))"
@@ -119,38 +117,6 @@ Two fixes, in order of preference:
The `setting_id` equivalent is `generate_preset_setting_id('<vendor folder>', '<type>', '<name>')`.
## The snapshot
`scripts/filament_id_snapshot.json` is the sanctioned state: the id landscape derived from the tree must
equal it exactly, in both directions. **Any change to a filament id or its claims must be committed with
the profiles.**
To trace an id from an error, search for it in the snapshot. Each entry records its identity triple
and `<vendor folder>/<name-before-@>` claims, one per bundle/product pair rather than per preset.
```bash
python3 scripts/orca_profile_tool.py update-snapshot
```
Never hand-edit it. It is regenerated deterministically (1-space indent, LF, id-sorted) and
refuses to write two states it could not record truthfully: a tree it could not read whole, and an id
declared under more than one triple. It does **not** judge the ids themselves — it records state, `check`
judges it, so a bad id lands in the diff and fails there instead. `generate-id` never touches the
snapshot, and reminds you with a warning **only when it actually wrote a `filament_id`** — not on a
`--dry-run`, and not when only `setting_id`s changed.
Reviewing a snapshot diff:
| Diff | Means |
| --- | --- |
| new id + new claim | a genuinely new product — confirm it is not a rename in disguise |
| id removed | a product left the tree, or its identity changed — the old id is not forwarded anywhere |
| triple changed under an existing id | `filament_vendor`/`filament_type`/name was edited; deliberate? |
| claim added/removed only | a bundle started or stopped shipping that product |
An entry with an empty `filaments` list is legitimate — declared, but not yet claimed by an instantiated
preset.
## BBL's exception, precisely
`RESERVED_VENDORS = {"BBL"}` covers **`setting_id` assignment only**, keyed on the *folder* name:
@@ -159,8 +125,8 @@ preset.
`setting_id` therefore **cannot be fixed by the tool**, yet the presence rule still applies to it —
carry over Bambu's authoritative id by hand.
- BBL is not exempt from anything else: bases still get their `setting_id` stripped, ids must still be
globally unique, and BBL `filament_id`s are minted like everyone else's — every id the snapshot records
as claimed by BBL is an `OF*`.
globally unique, and BBL `filament_id`s are minted like everyone else's — every one of them is an
`OF*`.
## Ids other systems compose
@@ -51,11 +51,11 @@ and take the whole vendor bundle down; an unindexed file gets reviewed, merged a
## 3. Are ids generated, not written?
No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do
not. A filament id change comes with a `scripts/filament_id_snapshot.json` diff in the same commit.
`check` enforces all of that; what it cannot tell you is whether the identity *should* have moved.
not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved.
Read the snapshot diff as the identity gate: a removed id or a changed triple means a product's identity
moved, and the old id is not forwarded anywhere. Confirm that was intended.
A rewritten or removed `filament_id` means a product's identity moved — a rename, or an edited
`filament_vendor` / `filament_type` and the old id is not forwarded anywhere. Confirm that was
intended, and that a new id is not a rename in disguise.
*Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied
`setting_id` breaks preset identity. See [ids.md](ids.md).
@@ -62,8 +62,7 @@ as SKIP for a vendor with no `machine/` folder.
## `orca_profile_tool.py check`
`check` is one subcommand of the tool that also owns
`generate-id`, `normalize`, `trim`, `update-index` and `update-snapshot`; see [ids.md](ids.md) for the
writing half.
`generate-id`, `normalize`, `trim` and `update-index`; see [ids.md](ids.md) for the writing half.
| Per vendor | Catches |
| --- | --- |
@@ -177,21 +176,14 @@ pre-existing user files may be present.
## Checking a copy of the tree
Use `--profiles DIR` on the Python tool and `-p DIR` on the validator.
`check` and `update-snapshot` describe a tree's sanctioned id state, so pointing them elsewhere also
needs `--snapshot PATH` for that tree — passing `--profiles` without it exits 2 rather than silently
judging the copy against `resources/profiles`'s snapshot.
**The wrappers' `--profiles` / `-ProfilesDir` redirects only their validator checks.** Their
`profile_tool` check still reads this checkout's `resources/profiles`. To validate a copy fully,
run the Python check separately with that tree's snapshot, then name only validator checks:
Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrappers' `--profiles` /
`-ProfilesDir` passes the tree to both, so one run validates a copy fully:
```bash
python3 scripts/orca_profile_tool.py check --profiles "<tree>" --snapshot "<snapshot.json>"
./scripts/check_profile.sh --profiles "<tree>" validate_system validate_slice validate_filament_subtypes validate_custom
./scripts/check_profile.sh --profiles "<tree>"
```
On Windows use `py -3` and `scripts\check_profile.bat -ProfilesDir "<tree>"` with the same check names.
On Windows use `scripts\check_profile.bat -ProfilesDir "<tree>"`.
## Testing in the app
@@ -238,7 +230,6 @@ Keep stems tidy too, but a space immediately before `.json` is not a trailing pa
| `[ERROR] … normalize would <change>` / `<V>.json: update-index would rebuild <lists>` | run that command and commit the result |
| `[ERROR] <V> has N <type> profiles named "<name>"` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup |
| `[ERROR] … must not have a setting_id` / `is missing a setting_id` | `generate-id --setting-id` |
| `[ERROR] filament_id "<id>" is not sanctioned by …snapshot.json` | `update-snapshot`, commit the diff |
| `inherits filament_id "X" but its own triple … mints "Y"` | `generate-id` will **not** fix this — see [ids.md](ids.md) |
| `vendor <V>'s config version: <s> invalid` | the `version` string is not Semver-parseable |
| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or model; see [failure scopes](vendor-bundle.md#failure-modes-ranked-by-blast-radius) |
@@ -260,5 +251,5 @@ once the run is green.
The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer
self-merge a `resources/profiles/<Their vendor>/` PR with no human review — so whatever CI does not check
is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that must update
`scripts/filament_id_snapshot.json` always needs a maintainer.
is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that touches the
tooling always needs a maintainer.
@@ -171,5 +171,5 @@ A separate tree (`Template.json` + `Template/`) holding filament and process tem
scaffold for shipped profiles — `CreatePresetsDialog.cpp` reads it for the in-app "create a custom
printer/filament" wizard, so editing it changes what users get when they create a custom preset.
`check_profile.sh`'s validator checks default to `resources/profiles` (redirectable with `-p`), and so
does `orca_profile_tool.py` (redirectable with `--profiles`, plus `--snapshot` for the id checks);
does `orca_profile_tool.py` (redirectable with `--profiles`);
neither covers this tree.
+2 -2
View File
@@ -80,8 +80,8 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -s -l 2 2>&1 | tee ${{ runner.temp }}/validate_slice.log
exit ${PIPESTATUS[0]}
# All vendors' filament_id collisions were fixed (see scripts/filament_id_snapshot.json),
# so the duplicate-filament-subtype check runs tree-wide.
# All vendors' filament_id collisions were fixed, so the duplicate-filament-subtype
# check runs tree-wide.
- name: validate filament subtype check
id: validate_filament_subtypes
continue-on-error: true
+31 -51
View File
@@ -36,21 +36,17 @@ This page is the rule for authoring `filament_id` in system profiles
> **Never write a `filament_id` value by hand.** A new filament gets its id from
> `python scripts/orca_profile_tool.py generate-id`; one already in the tree has one — inherit it.
## The design, in two pieces
## The design
Because several consumers match **globally by id alone, first hit wins** (see the next
section), any two materials sharing one id feed wrong data somewhere — a wrong tray name, a
wrong support-material flag, a wrong nozzle grouping — and inside one printer a duplicated id
makes AMS spool matching a coin toss. Hand-written ids produce such collisions constantly, so
the system is built to make them impossible:
1. **Deterministic minting.** An id is a pure hash of the product's identity — no registry to
maintain, no next-free-number ceremony, no way for two concurrent PRs to race for the same
number, and no way to get it wrong by hand, because you never write it by hand.
2. **A sanctioned snapshot.** The complete id landscape derived from the tree must equal
`scripts/filament_id_snapshot.json` exactly, so every change to ids, claims (which bundles
ship which id, and for which filament), or product identity surfaces as a reviewable diff to
one file — the maintainer gate.
the system is built to make them impossible: an id is a pure hash of the product's identity —
no registry to maintain, no next-free-number ceremony, no way for two concurrent PRs to race
for the same number, and no way to get it wrong by hand, because you never write it by hand.
CI holds every id in the tree to that rule, so the profiles themselves are the whole record of
which products exist and which bundles ship them.
## Who consumes the id
@@ -160,8 +156,8 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame
different product by rule 5, so it then needs its own id.
5. **Ids follow the product identity.** The id is a pure function of the product triple
`(filament_vendor, filament_type, filament name)`, so correcting any of them re-mints the id
**by design**, applied by `generate-id` (preview with `--dry-run`, confine with `--vendor`)
and gated by the `update-snapshot` diff; the exact sequence is in the FAQ. Nothing forwards
**by design**, applied by `generate-id` (preview with `--dry-run`, confine with `--vendor`);
the exact sequence is in the FAQ. Nothing forwards
the old value, so anything outside the tree that stored it — a device tray, a calibration
record, a saved project — falls back to matching by filament type until the user re-selects
the filament. Re-mint deliberately, and only to fix a genuinely wrong identity.
@@ -196,8 +192,8 @@ 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.
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
Nothing but the triple feeds the mint — not the rest of the tree, 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-id` pulls back. Two *different* products whose
triples mint the same base62 value would be a collision (a roughly 36-bit id space against a
@@ -214,17 +210,15 @@ Workflow for a new filament:
# 1. Author the filament with NO filament_id key anywhere.
python scripts/orca_profile_tool.py generate-id --dry-run # 2. preview the ids — writes nothing
python scripts/orca_profile_tool.py generate-id # 3. apply them to the profile file(s)
python scripts/orca_profile_tool.py update-snapshot # 4. record the new claims in the snapshot
python scripts/orca_profile_tool.py check # 5. validate — everything CI checks
# 6. Commit the profile edits together with scripts/filament_id_snapshot.json, for review.
python scripts/orca_profile_tool.py check # 4. validate — everything CI checks
```
`generate-id` makes every filament's id equal the mint of its own
`(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
mismatching id is the one case left to the author — check 2b names it, and the fix is to inherit
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
exactly when it already equals the one id its triple mints, and a collision (check 2d) 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
@@ -242,9 +236,7 @@ loudly), and a no-op on a tree that already passes `check`.
- `--dry-run` reports what the run would do and writes nothing, so
`generate-id --dry-run --vendor <Vendor>` previews just that bundle.
- `--profiles DIR` points the tooling at a different profile tree (default
`resources/profiles`). `check` and `update-snapshot` read and write the sanctioned state of
the tree they are given, so pointing them elsewhere needs `--snapshot PATH` for that tree too —
`scripts/filament_id_snapshot.json` describes `resources/profiles` and no other tree.
`resources/profiles`).
The tool's other commands maintain the tree around the ids: `fix` normalises profile files,
`trim` drops files no `<vendor>.json` list references, and `update-index` rebuilds those lists.
@@ -252,13 +244,11 @@ They do not touch ids; `--help` documents them.
**Identity fixes need no separate command.** `generate-id` re-derives an id that no longer matches
its triple exactly the way it fills in a missing one, so a rename or a `filament_vendor` /
`filament_type` correction is just: fix the config, run `generate-id` (confine it with
`--vendor`, preview it with `--dry-run`), then `update-snapshot` and review the diff.
`filament_type` correction is just: fix the config and run `generate-id` (confine it with
`--vendor`, preview it with `--dry-run`).
If you skip the tooling, CI fails and prints the remedy: the expected id for your filament and
the instruction to run `python scripts/orca_profile_tool.py generate-id`; once the id is minted,
the snapshot checks likewise point at `update-snapshot` and tell you to commit the resulting
diff.
the instruction to run `python scripts/orca_profile_tool.py generate-id`.
## Ids other systems compose
@@ -339,7 +329,7 @@ OrcaFilamentLibrary. **135 is the number to expect at every regeneration** — 1
one-off size of the transition and stopped being computable from the tree once the BBL bundle
was re-minted, so do not "fix" the report to print it.
**Check 5** lives in `check_filament_ids`, so profile CI runs it alongside the other four. It
**Check 4** lives in `check_filament_ids`, so profile CI runs it alongside the other three. It
holds the file to its contract: it parses, carries `source` / `bambustudio_commit` /
`generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row
whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)`
@@ -428,23 +418,14 @@ map would silently reproduce the bug.
## How CI enforces this
Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via
`scripts/orca_profile_tool.py check`. Its ground truth is
**`scripts/filament_id_snapshot.json` — the sanctioned state**: the id state derived from the
tree must equal the snapshot exactly, in both directions. Any change to the id landscape
therefore surfaces as a diff to that file, and **that snapshot diff is what maintainers review
and gate in a PR**. Never edit the snapshot by hand — `update-snapshot` regenerates it
deterministically (running it twice changes nothing). The snapshot holds one map, `ids`: each
entry is the product the id is minted from (`filament_vendor`, `filament_type`, `name`) and the
`filaments` claiming it (`Vendor/Filament`), and it sanctions *state*, never exceptions: no check
consults it to excuse a preset from a rule, and there is no grandfather list of any kind.
`scripts/orca_profile_tool.py check`. Every check judges the tree against the rules on this
page and nothing else — there is no recorded id state to match and no grandfather list of any
kind.
The checks, in brief:
- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions: not a
snapshot entry, not BBL.
- **Snapshot equality** — tree claims == snapshot claims **and** each id's declared triple ==
its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change
surfaces as a snapshot diff.
- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions, not
even BBL.
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
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
@@ -463,9 +444,8 @@ The checks, in brief:
A profile that declares an id no triple mints — a Bambu catalog id, a composed Qidi one, a
hand-typed value, whatever its vendor — fails the format check. For a Bambu-cataloged product
the catalog map is where the correspondence belongs. New sharing via a *declared* id is caught
by the identity check; sharing through inheritance carries no declaration to check and surfaces
only as a new claim in the snapshot diff — which is exactly why that diff is the gate.
the catalog map is where the correspondence belongs. Two products sharing one id are caught by
the identity check whether the id is declared or inherited.
The same `check` run holds every declared id to the AMS 8-character limit, tree-wide and for
every vendor alike, scoped to the presets a vendor's index actually references (a file the index
@@ -489,19 +469,19 @@ ambiguity check behind structure rule 3.
(or any real filament) for the settings and declare the id of your own filament; run
`python scripts/orca_profile_tool.py generate-id` to mint it. Inheritance never changes the id.
- **I need to fix a filament's `filament_vendor` or `filament_type`.** Fix the config, run
`generate-id --vendor <Vendor>` (preview with `--dry-run`), then `update-snapshot`, and commit
the profile and snapshot diffs together. The id re-derives from the corrected identity, and
`generate-id --vendor <Vendor>` (preview with `--dry-run`), and commit the result. The id
re-derives from the corrected identity, and
nothing forwards the old value, so a tray or record still holding it falls back to matching by
filament type.
- **I need to rename a filament.** Rename the presets (adding `renamed_from`, which keeps the
preset *name* resolving), then `generate-id --vendor <Vendor>` (preview with `--dry-run`), then
`update-snapshot`. The id follows the new filament name; as with any identity fix, the old id
preset *name* resolving), then `generate-id --vendor <Vendor>` (preview with `--dry-run`). The
id follows the new filament name; as with any identity fix, the old id
is not forwarded.
- **Can I reuse a `QD_*` id for a Qidi profile?** No — it is not a mint, so it is not a
`filament_id`. Those values are composed by the box at runtime, and no preset carries one.
Author Qidi filaments like any other vendor's.
- **CI says my filament needs an id.** Run `python scripts/orca_profile_tool.py generate-id`, then
`update-snapshot`, and commit both diffs. Do not type an id by hand.
- **CI says my filament needs an id.** Run `python scripts/orca_profile_tool.py generate-id` and
commit the result. Do not type an id by hand.
For general profile authoring, see the profile development guide on the
[OrcaSlicer wiki](https://www.orcaslicer.com/wiki).
+2 -3
View File
@@ -38,8 +38,7 @@
under emulation on ARM64.
.PARAMETER ProfilesDir
Profile tree to validate (default: resources\profiles). profile_tool always looks at the
tree next to the script, so this only redirects the validator checks.
Profile tree to validate (default: resources\profiles).
.PARAMETER Vendor
Check only this vendor, named after its <Vendor>.json (e.g. "Co Print"). validate_custom is
@@ -440,7 +439,7 @@ function Expand-VendorPresets([string] $Zip, [string] $Tree, [string] $Prefix) {
$CheckBodies = @{
profile_tool = {
Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check') + $VendorPyArgs)
Invoke-Tool -Exe (Resolve-Python) -Arguments (@((Join-Path $RepoRoot 'scripts\orca_profile_tool.py'), 'check', '--profiles', $ProfilesDir) + $VendorPyArgs)
}
validate_system = {
+2 -4
View File
@@ -82,9 +82,7 @@ Options:
Note: profile_tool is the only check that is not the validator binary; it makes the static
checks the validator cannot, because the validator loads the tree the way the slicer does
and so never sees a profile no <vendor>.json indexes, a preset name two files claim, or a
file normalize and update-index would still rewrite. It always looks at the tree next to
the script (<repo>/resources/profiles); --profiles only redirects the validator checks,
because validating another tree's ids needs that tree's own filament_id snapshot too.
file normalize and update-index would still rewrite.
Note: --vendor narrows validate_custom too, by keeping only that vendor's presets in each
fixture tree. The one check it cannot narrow is validate_slice for a vendor that ships no
@@ -366,7 +364,7 @@ resolve_validator() {
# ---------------------------------------------------------------------------- checks
check_profile_tool() {
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --vendor "${VENDOR}"
python3 "${REPO_ROOT}/scripts/orca_profile_tool.py" check --profiles "${PROFILES_DIR}" --vendor "${VENDOR}"
}
check_validate_system() {
File diff suppressed because it is too large Load Diff
+33 -243
View File
@@ -10,23 +10,19 @@ commands:
normalize rewrite profile files into their canonical shape
trim delete profile files no <vendor>.json list references
update-index regenerate the *_list sections of <vendor>.json
update-snapshot re-record scripts/filament_id_snapshot.json
options shared by several commands:
--vendor VENDOR act on one vendor bundle only; repeatable, empty means all
(every command but update-snapshot)
--profile-type TYPE one of machine_model/process/filament/machine; repeatable
(normalize, trim, update-index)
--dry-run report what would change and write nothing (every command
that writes)
--profiles DIR act on another profile tree (default: resources/profiles);
check and update-snapshot then need --snapshot PATH too,
since the snapshot describes resources/profiles alone
--profiles DIR act on another profile tree (default: resources/profiles)
After adding, renaming or deleting profile files, run:
normalize -> update-index -> generate-id -> update-snapshot -> check
normalize -> update-index -> generate-id -> check
normalize supplies missing types; update-index registers presets before id
generation. update-snapshot is needed when filament ids or claims change.
generation.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
@@ -55,8 +51,8 @@ filament_id policy (see docs/HLSD/filament_id.md):
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
8 chars total, which satisfies the AMS length limit. Nobody invents ids by
hand, and nothing but the triple feeds the mint not the rest of the tree,
not the snapshot. Two products whose triples mint one id (a base62
hand, and nothing but the triple feeds the mint not the rest of the tree.
Two products whose triples mint one id (a base62
collision; odds ~1e-5 over the whole tree) is an error --check reports and
--generate refuses to write; the remedy is a rename so the triples differ,
never a salted or hand-picked second id.
@@ -70,12 +66,6 @@ filament_id policy (see docs/HLSD/filament_id.md):
the app applies at the printer boundary), the QD_* ids a Qidi box composes at
runtime, and the P+7-hex ids CreatePresetsDialog.cpp gives user-created
filaments all fail the format rule like any other stray value.
* scripts/filament_id_snapshot.json is the sanctioned-state snapshot: one
entry per id, carrying the product triple it is minted from and the
"Vendor/Filament" presets claiming it. It must exactly equal the tree-derived
state at all times, so any id/claim/triple change shows up as a reviewable
diff to that file (the maintainer gate). It sanctions state, never
exceptions: no check consults it to excuse a preset from the rules above.
setting_id policy (see AGENTS.md "Critical Constraints"):
* setting_id is a PRESET id, a pure function of the preset's identity:
@@ -123,7 +113,6 @@ FILAMENT_ID_LENGTH = 6 # base62 digits after the "OF" prefix -> 8 chars total
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
PROFILES_DIR = os.path.normpath(os.path.join(SCRIPTS_DIR, "..", "resources", "profiles"))
SNAPSHOT_PATH = os.path.join(SCRIPTS_DIR, "filament_id_snapshot.json")
# The single source of truth for the map path; update_bambu_filament_ids.py
# imports this rather than recomputing it.
BAMBU_MAP_PATH = os.path.normpath(
@@ -193,7 +182,6 @@ _JSON_STR = r'"(?:[^"\\]|\\.)*"'
GENERATE_CMD = "python scripts/orca_profile_tool.py generate-id"
SETTING_ID_CMD = '"python scripts/orca_profile_tool.py generate-id --setting-id"'
UPDATE_HINT = 'run "python scripts/orca_profile_tool.py update-snapshot" and commit the diff for maintainer review'
BAMBU_MAP_HINT = 'regenerate the map with "python scripts/update_bambu_filament_ids.py" and commit the diff for maintainer review'
NORMALIZE_HINT = 'try "python scripts/orca_profile_tool.py normalize" to fix common issues automatically'
@@ -246,8 +234,8 @@ def _base62_tail(n, length):
"""The low `length` base62 digits of n, most-significant first.
The shared tail of both id rules. Its output bytes are pinned by the C++
golden vectors (tests/libslic3r/test_preset_setting_id.cpp) and by the
filament_id snapshot never change it.
golden vectors (tests/libslic3r/test_preset_setting_id.cpp) and by every
filament_id in the tree never change it.
"""
digits = []
for _ in range(length):
@@ -483,9 +471,8 @@ def resolve_triple(name, filaments, ofl_filaments):
def analyze_tree(profiles_dir):
"""Load every vendor bundle and derive the full filament_id state.
Returns a dict with the tree-derived snapshot sections plus the working data
the checks and the assign pass need. All claims are "Vendor/Filament" strings
over INSTANTIATED system filaments, tree-wide including OFL and BBL.
Returns a dict of the tree-derived state the checks and the assign pass need,
tree-wide including OFL and BBL.
"""
profiles_dir = str(profiles_dir)
vendor_names = list_vendor_names(profiles_dir)
@@ -507,11 +494,6 @@ def analyze_tree(profiles_dir):
rec["id_source"] = src
vendors[vendor] = filaments
# id -> set of "Vendor/Filament" claims over instantiated presets. Every id
# occurring in the tree is a key; ids only ever DECLARED (e.g. on a root
# none of whose descendants instantiate) keep an empty claim list, so that
# the snapshot exactly equals the tree-derived state.
ids = {}
vendor_ids = {} # vendor -> set of ids occurring there (declared or effective)
declared_ids = {} # vendor -> set of ids DECLARED in that vendor's own files
missing_effective = [] # (vendor, name, file) instantiated presets resolving no id
@@ -532,7 +514,6 @@ def analyze_tree(profiles_dir):
fid = rec["filament_id"]
occurring.add(fid)
declared_ids.setdefault(vendor, set()).add(fid)
ids.setdefault(fid, set())
declarer_triples.append((vendor, rec, fid, triple))
triples.setdefault(fid, set()).add(triple)
filament_triples.setdefault(
@@ -545,11 +526,10 @@ def analyze_tree(profiles_dir):
missing_effective.append((vendor, rec["name"], rec["file"]))
continue
occurring.add(eff)
ids.setdefault(eff, set()).add(f"{vendor}/{base_name(rec['name'])}")
if not rec.get("filament_id") and OF_ID_RE.match(eff):
inherited.append((vendor, rec, eff, triple))
# Cross-bundle triple divergence (check 4, warning only): the same filament
# Cross-bundle triple divergence (check 3, warning only): the same filament
# name declared in several bundles with different triples cannot converge
# on one id until the divergence is fixed.
name_bundles = {}
@@ -564,7 +544,6 @@ def analyze_tree(profiles_dir):
return {
"vendors": vendors,
"read_errors": read_errors,
"ids": {fid: sorted(claims) for fid, claims in ids.items()},
"vendor_ids": vendor_ids,
"declared_ids": declared_ids,
"missing_effective": sorted(missing_effective),
@@ -578,57 +557,16 @@ def analyze_tree(profiles_dir):
}
# ---------------------------------------------------------------------------
# Snapshot IO
# ---------------------------------------------------------------------------
def snapshot_from_analysis(analysis):
"""One entry per id, in id order: the product triple it is minted from and
the "Vendor/Filament" claims on it. Requires exactly one declared triple per
id (update_snapshot refuses any other state; check 3 rejects it anyway)."""
ids = {}
for fid, claims in sorted(analysis["ids"].items()):
[(vendor, ftype, filament_name)] = analysis["triples"][fid]
ids[fid] = {"filaments": sorted(claims), "name": filament_name,
"filament_type": ftype, "filament_vendor": vendor}
return {"ids": ids}
def snapshot_triple(entry):
return [entry["filament_vendor"], entry["filament_type"], entry["name"]]
def load_snapshot(path):
"""Return the snapshot dict, or None when the file does not exist."""
if not os.path.exists(path):
return None
data = load_json(path)
data.setdefault("ids", {})
return data
def write_snapshot(path, obj):
"""Deterministic serialization: snapshot_from_analysis order, indent 1, LF,
trailing newline."""
with open(path, "w", encoding="utf-8", newline="\n") as f:
json.dump(obj, f, indent=1, ensure_ascii=False)
f.write("\n")
# ---------------------------------------------------------------------------
# filament_id validation
# ---------------------------------------------------------------------------
def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
map_path=BAMBU_MAP_PATH):
def check_filament_ids(profiles_dir=PROFILES_DIR, map_path=BAMBU_MAP_PATH):
"""Validate filament_id state across every vendor. Returns the error count.
1. Format: every id occurring in the tree (declared or effective) must
match ^OF[0-9A-Za-z]{6}$. No exceptions: not the snapshot, not BBL.
2. Snapshot equality, both directions: every id in the tree, the filaments
claiming it and the triple its declarers resolve must equal the snapshot
entry exactly (the snapshot diff is the maintainer gate).
3. Identity: the id is a function of the triple alone, and there is no
match ^OF[0-9A-Za-z]{6}$. No exceptions, not even BBL.
2. Identity: the id is a function of the triple alone, and there is no
second acceptable value. (a) A declared id must equal the one id the
declarer's own triple mints; (b) the id an instantiated preset inherits
must equal the one ITS own triple mints how it inherits it (a root, a
@@ -636,31 +574,22 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
filament resolves an effective id at all (an id-less one is a hard load
error in C++); (d) no two products mint one id (a base62 collision,
resolved by renaming one of them).
4. Triple integrity: (a) every declarer resolves non-empty filament_vendor
3. Triple integrity: (a) every declarer resolves non-empty filament_vendor
and filament_type; (b) declarers of one (bundle, filament) resolve
identical triples; cross-bundle divergence on the same filament name is a
warning only.
5. Bambu catalog map: resources/printers/bambu_filament_ids.json must parse,
4. Bambu catalog map: resources/printers/bambu_filament_ids.json must parse,
carry source/bambustudio_commit/generated, key only OF-format ids, map
each Bambu id at most once, and for every row whose key the tree claims,
the tree's triple for that id must equal the row's (vendor, type, name).
Nothing is grandfathered: the snapshot sanctions state, never exceptions.
"""
_utf8_console()
errors = 0
analysis = analyze_tree(profiles_dir)
snapshot = load_snapshot(snapshot_path)
if snapshot is None:
print_error(f"filament_id snapshot not found at {snapshot_path}; {UPDATE_HINT}")
return 1
for msg in analysis["read_errors"]:
print_error(msg)
errors += 1
snap_ids = snapshot["ids"]
tree_ids = analysis["ids"]
# -- 1. format ----------------------------------------------------------
for vendor in sorted(analysis["vendor_ids"]):
for fid in sorted(analysis["vendor_ids"][vendor]):
@@ -671,47 +600,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'filament ids must come from "{GENERATE_CMD}"')
errors += 1
# -- 2. snapshot equality (both directions) -----------------------------
tree_triples = analysis["triples"]
for fid in sorted(tree_ids):
entry = snap_ids.get(fid)
if entry is None:
print_error(
f'filament_id "{fid}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
continue
for claim in tree_ids[fid]:
if claim not in entry["filaments"]:
print_error(
f'filament_id "{fid}" claim "{claim}" is not sanctioned by '
f"scripts/filament_id_snapshot.json; {UPDATE_HINT}")
errors += 1
# Every tree id has at least one declarer; the snapshot records one
# triple per id, so a divergent declarer is a mismatch in both directions.
sanctioned = snapshot_triple(entry)
for t in tree_triples[fid]:
if t != sanctioned:
print_error(
f'filament_id "{fid}" triple "{"/".join(t)}" is not sanctioned by '
f'scripts/filament_id_snapshot.json, which records '
f'"{"/".join(sanctioned)}"; {UPDATE_HINT}')
errors += 1
for fid in sorted(snap_ids):
if fid not in tree_ids:
print_error(
f'filament_id stability: snapshot id "{fid}" vanished from the tree; '
f"{UPDATE_HINT}")
errors += 1
continue
for claim in snap_ids[fid]["filaments"]:
if claim not in tree_ids[fid]:
print_error(
f'filament_id stability: snapshot claim "{claim}" of id "{fid}" '
f"vanished from the tree; {UPDATE_HINT}")
errors += 1
# -- 3. identity: the id is a function of the triple alone ---------------
# -- 2. identity: the id is a function of the triple alone ---------------
# One triple, one id: a declaration must carry exactly the mint of its
# triple, and there is no second acceptable value — not a salt, not a
# hand-picked one, not whatever another preset of the product carries. Two
@@ -727,10 +616,9 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
f'not match the mint of its triple "{"/".join(triple)}": expected '
f'"{want}"; paste the expected id, or fix the triple and run '
f'"{GENERATE_CMD} --vendor {vendor}" (preview with --dry-run), then '
f"--update-snapshot")
f'"{GENERATE_CMD} --vendor {vendor}" (preview with --dry-run)')
errors += 1
# (3b) An inherited id is held to the same single value, and every preset
# (2b) An inherited id is held to the same single value, and every preset
# missing it is listed — a variant under a wrong root as much as a preset
# riding another product's root. Nothing is folded into the declarer's
# error: the report names each preset whose id is wrong.
@@ -755,7 +643,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'run "{GENERATE_CMD}" (expected id for filament '
f'"{vendor}/{base_name(name)}": "{expected}")')
errors += 1
# (3d) The mint is injective over the tree's products, or two of them are
# (2d) The mint is injective over the tree's products, or two of them are
# indistinguishable to every device that matches on the id.
for fid, ts in sorted(analysis["collisions"].items()):
print_error(
@@ -764,7 +652,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f"of them so their triples differ")
errors += 1
# -- 4. triple integrity ---------------------------------------------------
# -- 3. triple integrity ---------------------------------------------------
for vendor, rec, fid, triple in sorted(
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
if triple[0] and triple[1]:
@@ -797,7 +685,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f"({detail}); bundles of one product converge on one id only once "
f"their triples agree")
# -- 6. Bambu catalog map --------------------------------------------------
# -- 4. Bambu catalog map --------------------------------------------------
try:
bambu_map = load_json(map_path)
if not isinstance(bambu_map, dict):
@@ -838,7 +726,7 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
errors += 1
else:
bambu_id_owners[bambu_id] = fid
claimed = tree_triples.get(fid)
claimed = analysis["triples"].get(fid)
if not claimed:
continue # a product BambuStudio ships that the tree does not (yet)
row_triple = [row.get("vendor", ""), row.get("type", ""), row.get("name", "")]
@@ -1404,7 +1292,7 @@ def check_normalized(profiles_dir, vendor):
# check
# ---------------------------------------------------------------------------
def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSHOT_PATH):
def check_profiles(profiles_dir=PROFILES_DIR, vendors=None):
"""Validate the whole profile tree. Returns the error count.
The per-vendor checks honour `vendors`; the setting_id and filament_id checks are
@@ -1468,7 +1356,7 @@ def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSH
# Cross-vendor checks: setting_id uniqueness and the whole filament_id state,
# both validated over the entire tree regardless of --vendor.
errors_found += check_setting_id_uniqueness(profiles_dir)
errors_found += check_filament_ids(profiles_dir, snapshot_path)
errors_found += check_filament_ids(profiles_dir)
print("\n==================== SUMMARY ====================")
print_info(f"Checked vendors : {len(checked)}")
@@ -1486,69 +1374,6 @@ def check_profiles(profiles_dir=PROFILES_DIR, vendors=None, snapshot_path=SNAPSH
return errors_found
# ---------------------------------------------------------------------------
# update-snapshot
# ---------------------------------------------------------------------------
def update_snapshot(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH, dry_run=False):
"""Regenerate the snapshot from the tree.
Refuses to sanction a tree it could not read whole, and an id declared under
more than one triple: neither state can be recorded truthfully, so writing it
would only hide the mistake until CI. It does not judge the ids themselves
the snapshot records state and check judges it, so an id that is not a mint
lands in the diff and fails check 1.
Idempotent: a second run over an unchanged tree changes nothing. Returns 0
on success.
"""
analysis = analyze_tree(profiles_dir)
# A tree that could not be read whole cannot be sanctioned: the snapshot
# would silently drop the unreadable bundle's ids and claims, and the diff
# would read as a deliberate removal.
refusals = len(analysis["read_errors"])
for msg in analysis["read_errors"]:
print_error(msg)
for fid, ts in sorted(analysis["triples"].items()):
if len(ts) > 1:
print_error(
f'refusing to sanction filament_id "{fid}": declared under {len(ts)} '
f'triples ({"; ".join("/".join(t) for t in ts)}); one id names one '
f"product (check 3)")
refusals += 1
if refusals:
return 1
new_snap = snapshot_from_analysis(analysis)
old_snap = load_snapshot(snapshot_path)
old_ids = old_snap["ids"] if old_snap else {}
# Diff summary.
added_ids = sorted(set(new_snap["ids"]) - set(old_ids))
removed_ids = sorted(set(old_ids) - set(new_snap["ids"]))
added_claims = sum(
len(set(entry["filaments"]) - set(old_ids.get(fid, {}).get("filaments", [])))
for fid, entry in new_snap["ids"].items())
removed_claims = sum(
len(set(entry["filaments"]) - set(new_snap["ids"].get(fid, {}).get("filaments", [])))
for fid, entry in old_ids.items())
changed = new_snap != (old_snap or {"ids": {}})
if changed and not dry_run:
write_snapshot(snapshot_path, new_snap)
print_info(f"snapshot ids : {len(new_snap['ids'])} (+{len(added_ids)} / -{len(removed_ids)})")
print_info(f"claims added : {added_claims}")
print_info(f"claims removed : {removed_claims}")
if changed and dry_run:
print_success(f"dry run: {snapshot_path} would be rewritten; nothing written")
elif changed:
print_success(f"snapshot written to {snapshot_path}")
else:
print_success("snapshot already up to date; nothing changed")
return 0
# ---------------------------------------------------------------------------
# Byte-preserving profile edits
# ---------------------------------------------------------------------------
@@ -1692,9 +1517,9 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False
* an instantiated filament that resolves no id at all gets one inserted
into its root(s): the id-less presets of the SAME filament its members
inherit, or the member itself (a parent of another filament cannot carry
this filament's id — check 3).
this filament's id — check 2).
A declaration is left alone exactly when it already equals the one id its
triple mints (check 3). Two products minting one id (check 3d) are reported
triple mints (check 2). Two products minting one id (check 2d) are reported
and left unwritten: nothing salts past a collision, a rename resolves it.
`vendors` restricts what is WRITTEN; the id is a function of the triple
@@ -1702,9 +1527,7 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False
reports whatever it was not allowed to touch. `changed_paths`, when a set is
passed, collects the files that changed. A file whose layout offers no
anchor for the edit is reported and counted as an error, so one odd profile
cannot abort the pass over all the others. Never reads or touches the
snapshot run --update-snapshot afterwards and review the diff. Returns
(files_changed, errors).
cannot abort the pass over all the others. Returns (files_changed, errors).
"""
_utf8_console()
analysis = analyze_tree(profiles_dir)
@@ -1959,9 +1782,9 @@ def run_generate_id(profiles_dir, vendors, filament_id, setting_id, dry_run):
do_filament = filament_id or not setting_id
do_setting = setting_id or not filament_id
changed = set() # one file the two passes both touch is still one file
filament_files = errors = 0
errors = 0
if do_filament:
filament_files, e = generate_filament_ids(profiles_dir, vendors, dry_run, changed)
_n, e = generate_filament_ids(profiles_dir, vendors, dry_run, changed)
errors += e
if do_setting:
_n, e = generate_setting_ids(profiles_dir, vendors, dry_run, changed)
@@ -1973,11 +1796,6 @@ def run_generate_id(profiles_dir, vendors, filament_id, setting_id, dry_run):
print_error(f"{summary}; {errors} error(s)")
else:
print_success(summary)
if filament_files and not dry_run:
# A filament_id write may or may not move the sanctioned state (an id repaired
# back to the value the snapshot already records does not), so regenerate and
# let the diff - empty or not - say.
print_warning(f"now {UPDATE_HINT}")
return 1 if errors else 0
@@ -2439,13 +2257,11 @@ examples:
preview exactly that; writes nothing
orca_profile_tool.py generate-id --setting-id --vendor Elegoo
setting_id only, and only in that bundle
orca_profile_tool.py update-snapshot
re-record the sanctioned filament_id state after a generate-id run
after adding, renaming or deleting profile files, run in this order:
normalize -> update-index -> generate-id -> update-snapshot -> check
normalize -> update-index -> generate-id -> check
normalize supplies missing types; update-index registers presets before id
generation. update-snapshot is needed when filament ids or claims change.
generation.
Use trim only for deliberate cleanup, previewed with --dry-run: it judges against
the current index and can delete newly added, unindexed presets.
"""
@@ -2473,12 +2289,6 @@ def build_parser():
dry_run_opt.add_argument("--dry-run", "--dryrun", dest="dry_run", action="store_true",
help="report what would change and write nothing")
snapshot_opt = argparse.ArgumentParser(add_help=False)
snapshot_opt.add_argument("--snapshot", default=None, metavar="PATH",
help="the sanctioned filament_id state of that tree "
"(default: scripts/filament_id_snapshot.json, which "
"describes resources/profiles and no other tree)")
parser = argparse.ArgumentParser(
prog="orca_profile_tool.py", allow_abbrev=False,
formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -2496,7 +2306,7 @@ def build_parser():
allow_abbrev=False, formatter_class=argparse.RawDescriptionHelpFormatter)
add(
"check", [vendor_opt, snapshot_opt, profiles_opt],
"check", [vendor_opt, profiles_opt],
"validate the whole profile tree -- what CI runs",
"Validate the whole profile tree: preset name uniqueness, index coverage\n"
"both ways, compatible_printers, default-material references, obsolete,\n"
@@ -2560,11 +2370,6 @@ def build_parser():
"Use trim only for deliberate unindexed-file cleanup, previewed with\n"
"--dry-run; it can also delete newly authored presets.")
add("update-snapshot", [dry_run_opt, snapshot_opt, profiles_opt],
"re-record scripts/filament_id_snapshot.json",
"Re-record the sanctioned filament_id state after a generate-id run, and\n"
"commit the diff for maintainer review.")
return parser
@@ -2590,29 +2395,14 @@ def main(argv=None):
return 1
profile_types = tuple(getattr(args, "profile_type", []) or ()) or None
snapshot_path = getattr(args, "snapshot", None)
if snapshot_path is None:
if (args.command in ("check", "update-snapshot")
and os.path.abspath(profiles_dir) != os.path.abspath(PROFILES_DIR)):
# The repo snapshot is the sanctioned state of resources/profiles alone:
# checking another tree against it is meaningless, and re-recording one
# into it would overwrite the tracked file with a foreign tree's state.
parser.error(f"{args.command} reads and writes the sanctioned state of the "
f"tree it is given, so --profiles needs --snapshot PATH for "
f"that tree too")
snapshot_path = SNAPSHOT_PATH
if args.command == "check":
errors = check_profiles(profiles_dir, vendors, snapshot_path)
errors = check_profiles(profiles_dir, vendors)
return 1 if errors else 0
if args.command == "generate-id":
return run_generate_id(profiles_dir, vendors, args.filament_id, args.setting_id,
args.dry_run)
if args.command == "update-snapshot":
return update_snapshot(profiles_dir, snapshot_path, dry_run=args.dry_run)
if args.command == "normalize":
_changed, errors = normalize_profiles(profiles_dir, vendors, profile_types,
force=args.force, dry_run=args.dry_run)
+49 -269
View File
@@ -55,13 +55,12 @@ def preset(name, filament_id=None, inherits=None, instantiation=True,
class SyntheticTree:
"""A throwaway resources/profiles-shaped directory plus a snapshot path."""
"""A throwaway resources/profiles-shaped directory."""
def __init__(self):
self.dir = tempfile.mkdtemp(prefix="filament_id_test_")
self.profiles = os.path.join(self.dir, "profiles")
os.makedirs(self.profiles)
self.snapshot = os.path.join(self.dir, "filament_id_snapshot.json")
def cleanup(self):
shutil.rmtree(self.dir, ignore_errors=True)
@@ -110,16 +109,6 @@ class SyntheticTree:
with open(idx_path, "w", encoding="utf-8") as f:
json.dump(index, f, indent=4, ensure_ascii=False)
def remove_preset(self, vendor, name):
os.remove(self.preset_path(vendor, name))
idx_path = os.path.join(self.profiles, vendor + ".json")
with open(idx_path, encoding="utf-8") as f:
index = json.load(f)
index["filament_list"] = [
e for e in index["filament_list"] if e["name"] != name]
with open(idx_path, "w", encoding="utf-8") as f:
json.dump(index, f, indent=4, ensure_ascii=False)
def bytes_map(self):
"""{relative path -> file bytes} over every .json in the tree."""
raw = {}
@@ -135,17 +124,11 @@ class SyntheticTree:
# -- pipeline wrappers ---------------------------------------------------
def update_snapshot(self, dry_run=False):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.update_snapshot(self.profiles, self.snapshot, dry_run)
return rc, buf.getvalue()
def check(self, map_path=None):
buf = io.StringIO()
kwargs = {} if map_path is None else {"map_path": map_path}
with contextlib.redirect_stdout(buf):
errors = afi.check_filament_ids(self.profiles, self.snapshot, **kwargs)
errors = afi.check_filament_ids(self.profiles, **kwargs)
return errors, buf.getvalue()
# assign() and remint() are the same one pass over the tree — every filament
@@ -167,8 +150,6 @@ class SyntheticTree:
def cli(self, *argv):
"""Run main() against this tree, capturing stdout."""
flags = [*argv, "--profiles", self.profiles]
if argv and argv[0] in ("check", "update-snapshot"):
flags += ["--snapshot", self.snapshot]
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.main(flags)
@@ -178,10 +159,10 @@ class SyntheticTree:
def make_clean_tree(apla_id="AX01", generic_id="OGFL99"):
"""Baseline tree: OFL base+generic, a vendor filament, a clean tuned generic.
apla_id/generic_id default to arbitrary non-OF placeholders (sanctioned by
the snapshot below) since most tests only need "already assigned, don't
touch" and never run the checks. TestAssign and the check tests pass real
OF-format ids instead (OfCleanTreeCase).
apla_id/generic_id default to arbitrary non-OF placeholders since most tests
only need "already assigned, don't touch" and never run the checks.
TestAssign and the check tests pass real OF-format ids instead
(OfCleanTreeCase).
"""
t = SyntheticTree()
t.add_vendor(OFL, [
@@ -199,8 +180,6 @@ def make_clean_tree(apla_id="AX01", generic_id="OGFL99"):
preset("Generic PLA @P1", inherits="Generic PLA @System",
compatible_printers=["P1 0.4 nozzle"]),
])
rc, _out = t.update_snapshot()
assert rc == 0
return t
@@ -212,10 +191,9 @@ class SyntheticTreeCase(unittest.TestCase):
class OfCleanTreeCase(unittest.TestCase):
"""Like SyntheticTreeCase, but the baseline filament/generic already carry
real OF-format ids (check 1 now rejects "AX01"/"OGFL99" unconditionally,
with no snapshot exemption), so an otherwise-untouched tree still passes
check_filament_ids. Tests that specifically need a non-OF baseline to
remint (TestRemint, TestUpdateSnapshot) keep using SyntheticTreeCase
real OF-format ids (check 1 rejects "AX01"/"OGFL99"), so an
otherwise-untouched tree passes check_filament_ids. Tests that specifically
need a non-OF baseline to remint (TestRemint) keep using SyntheticTreeCase
instead.
"""
def setUp(self):
@@ -231,7 +209,7 @@ class OfCleanTreeCase(unittest.TestCase):
class TestMint(unittest.TestCase):
def test_namespace_literal(self):
# Frozen: derived from the setting_id namespace; baked into the snapshot.
# Frozen: derived from the setting_id namespace; baked into every shipped id.
self.assertEqual(afi.FILAMENT_ID_NAMESPACE,
uuid.UUID("c4d3ff49-4c32-5534-a3e3-00894157ab97"))
@@ -450,22 +428,10 @@ class TestChecks(OfCleanTreeCase):
self.assertIn('is not a minted "OF" id', out)
self.assertIn("BOGUS_9", out)
def test_check2_new_claim_needs_snapshot_update(self):
self.t.write_preset("VendorA", preset("ANEW @P2", inherits="APLA @base",
compatible_printers=["P2"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('claim "VendorA/ANEW" is not sanctioned', out)
self.assertIn("update-snapshot", out)
def test_check2_vanished_claim_is_stability_error(self):
self.t.remove_preset("VendorA", "APLA @P1")
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("stability", out)
self.assertIn('"VendorA/APLA"', out)
def test_check2_triple_change_needs_snapshot_update(self):
def test_check2_triple_change_needs_a_remint(self):
# Correcting a triple changes the product's identity: the old id is no
# longer its mint, reported on the root and again under the variant
# inheriting it, until generate-id re-mints it.
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("APLA @base", filament_id=apla_id,
instantiation=False,
@@ -473,26 +439,15 @@ class TestChecks(OfCleanTreeCase):
filament_type="PETG"),
register=False)
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('triple "AVendor/PETG/APLA" is not sanctioned', out)
self.assertIn('which records "AVendor/PLA/APLA"', out)
# Sanctioning the new triple is not enough: the old id is no longer its
# mint (check 3, nothing grandfathered) — the identity fix is a re-mint,
# reported on the root and again under the variant inheriting it.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 2, out)
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"APLA @P1" (VendorA/filament/APLA @P1.json) inherits filament_id', out)
_changed, errors, out = self.t.remint(["VendorA"])
self.assertEqual(errors, 0, out)
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check3_of_id_must_match_triple_mint(self):
def test_check2_of_id_must_match_triple_mint(self):
self.t.write_preset("VendorA", preset("BNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="BV", filament_type="PLA"))
@@ -503,39 +458,20 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("does not match the mint of its triple", out)
self.assertIn(afi.generate_filament_id("BV", "PLA", "BNEW"), out)
def test_check3_no_grandfathering_of_a_wrong_declaration(self):
# Sanctioning the tree does not excuse a declaration from its mint.
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="CV", filament_type="PLA"))
self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base",
compatible_printers=["P1"]))
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 2, out) # the declaration, and the variant inheriting it
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"CNEW @P1" (VendorA/filament/CNEW @P1.json) inherits filament_id', out)
def test_check3_inherited_id_must_be_the_mint_of_own_triple(self):
def test_check2_inherited_id_must_be_the_mint_of_own_triple(self):
# A preset of another filament inheriting APLA's root takes APLA's id,
# which is not the mint of ITS triple (AVendor/PLA/Tuned PLA).
self.t.write_preset("VendorA", preset("Tuned PLA @P1", inherits="APLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertEqual(errors, 1, out)
self.assertIn('"Tuned PLA @P1" (VendorA/filament/Tuned PLA @P1.json) inherits '
'filament_id "%s"' % afi.generate_filament_id("AVendor", "PLA", "APLA"),
out)
self.assertIn('mints "%s"' % afi.generate_filament_id("AVendor", "PLA", "Tuned PLA"),
out)
# ... and sanctioning the tree does not excuse it either.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
def test_check3_lists_every_preset_inheriting_a_wrong_id(self):
def test_check2_lists_every_preset_inheriting_a_wrong_id(self):
# A wrong declaration is reported under every preset inheriting it, its
# own product's variant and another product alike: each one's effective
# id is not the mint of its own triple, and each is listed. Nothing is
@@ -552,11 +488,10 @@ class TestChecks(OfCleanTreeCase):
self.assertIn('"DNEW @P1" (VendorA/filament/DNEW @P1.json) inherits filament_id', out)
self.assertIn('"Other DNEW @P1" (VendorA/filament/Other DNEW @P1.json) inherits '
'filament_id', out)
# The unsanctioned id (check 2), the declaration (3a), and both presets
# inheriting it (3b).
self.assertEqual(errors, 4, out)
# The declaration (2a), and both presets inheriting it (2b).
self.assertEqual(errors, 3, out)
def test_check3_reports_an_inherited_mismatch_even_when_its_own_product_misdeclares_the_id(self):
def test_check2_reports_an_inherited_mismatch_even_when_its_own_product_misdeclares_the_id(self):
# "Tuned PLA @P1" inherits APLA's root, so it carries APLA's id: wrong
# for its own product however the declarations around it are fixed.
# That "Tuned PLA @base" — its own product — misdeclares that same id
@@ -573,11 +508,11 @@ class TestChecks(OfCleanTreeCase):
'not match the mint of its triple', out)
self.assertIn('"Tuned PLA @P1" (VendorA/filament/Tuned PLA @P1.json) inherits '
'filament_id', out)
# The unsanctioned claim and triple (check 2), the declaration (3a) and
# the inherited id (3b): four distinct errors, nothing folded away.
self.assertEqual(errors, 4, out)
# The declaration (2a) and the inherited id (2b): two distinct errors,
# nothing folded away.
self.assertEqual(errors, 2, out)
def test_check3_reports_a_collision_between_two_products(self):
def test_check2_reports_a_collision_between_two_products(self):
# Two products whose triples mint one id is a base62 collision. There
# is no salted or hand-picked second id to fall back on: the check
# names both products, and the remedy is a rename so the triples differ.
@@ -601,13 +536,12 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("V/PLA/X", out)
self.assertIn("W/ABS/Y", out)
# Each declaration is the mint of its own triple, so the collision is
# the only identity error — no product is pushed off its id — and the
# unsanctioned id (check 2) is the only other one.
# the only error — no product is pushed off its id.
self.assertNotIn("does not match the mint", out)
self.assertNotIn("inherits filament_id", out)
self.assertEqual(errors, 2, out)
self.assertEqual(errors, 1, out)
def test_check3_renamed_tuned_generic_is_an_identity_error(self):
def test_check2_renamed_tuned_generic_is_an_identity_error(self):
# Riding the OFL generic under another base name: same rule, same error.
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
inherits="Generic PLA @System",
@@ -617,7 +551,7 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("Tuned PLA @P1", out)
self.assertIn("inherits filament_id", out)
def test_check3_own_key_on_an_instantiated_preset_is_fine(self):
def test_check2_own_key_on_an_instantiated_preset_is_fine(self):
# Where the id comes from is irrelevant: a variant may carry the key.
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("APLA @P1", filament_id=apla_id,
@@ -627,7 +561,7 @@ class TestChecks(OfCleanTreeCase):
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check3_inheriting_a_real_filament_of_another_product_is_fine(self):
def test_check2_inheriting_a_real_filament_of_another_product_is_fine(self):
# A branded product may inherit the OFL generic (an instantiated
# preset) for its settings; it declares its own triple's id.
fid = afi.generate_filament_id("BV", "PLA", "Branded PLA")
@@ -635,8 +569,6 @@ class TestChecks(OfCleanTreeCase):
inherits="Generic PLA @System",
filament_vendor="BV",
compatible_printers=["P1"]))
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
# With a wrong key it is a plain mint mismatch: the parent plays no
@@ -670,22 +602,16 @@ class TestChecks(OfCleanTreeCase):
self.assertIn(f'filament_id "{fid}"', out)
self.assertIn('is not a minted "OF" id', out)
def test_check3c_unresolvable_instantiated_filament(self):
def test_check2c_unresolvable_instantiated_filament(self):
self.t.write_preset("VendorA", preset("DNEW @P1", compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("resolves no filament_id", out)
self.assertIn("hard load error", out)
def test_missing_snapshot_is_an_error(self):
os.remove(self.t.snapshot)
errors, out = self.t.check()
self.assertEqual(errors, 1)
self.assertIn("snapshot not found", out)
class TestCheck5(OfCleanTreeCase):
def test_5a_empty_vendor_is_hard_error(self):
class TestCheck3(OfCleanTreeCase):
def test_3a_empty_vendor_is_hard_error(self):
fid = afi.generate_filament_id("", "PLA", "NVPLA")
self.t.write_preset("VendorA", preset("NVPLA @base", filament_id=fid,
instantiation=False,
@@ -693,17 +619,11 @@ class TestCheck5(OfCleanTreeCase):
self.t.write_preset("VendorA", preset("NVPLA @P1", inherits="NVPLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn("resolves empty filament_vendor", out)
self.assertIn('filament_vendor "Generic"', out)
# No grandfathering: sanctioning the tree does not silence check 4a.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertIn("resolves empty filament_vendor", out)
self.assertIn('filament_vendor "Generic"', out)
def test_5b_divergent_filament_triples(self):
def test_3b_divergent_filament_triples(self):
id1 = afi.generate_filament_id("MV", "PLA", "MPLA")
id2 = afi.generate_filament_id("MV", "PETG", "MPLA")
self.t.write_preset("VendorA", preset("MPLA @base1", filament_id=id1,
@@ -715,18 +635,12 @@ class TestCheck5(OfCleanTreeCase):
filament_vendor="MV",
filament_type="PETG"))
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertEqual(errors, 1, out)
self.assertIn("divergent triples", out)
self.assertIn("MV/PLA/MPLA", out)
self.assertIn("MV/PETG/MPLA", out)
# No grandfathering: sanctioning the tree does not silence check 4b.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertIn("divergent triples", out)
def test_5_cross_bundle_divergence_is_warning_only(self):
def test_3_cross_bundle_divergence_is_warning_only(self):
fid = afi.generate_filament_id("BV", "PETG", "APLA")
self.t.add_vendor("VendorB", [
preset("APLA @base", filament_id=fid, instantiation=False,
@@ -734,8 +648,6 @@ class TestCheck5(OfCleanTreeCase):
preset("APLA @PB", inherits="APLA @base",
compatible_printers=["PB 0.4 nozzle"]),
])
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
self.assertIn("[WARNING]", out)
@@ -743,7 +655,7 @@ class TestCheck5(OfCleanTreeCase):
self.assertIn('"APLA"', out)
class TestCheck6(OfCleanTreeCase):
class TestCheck4(OfCleanTreeCase):
def _write_map(self, rows):
path = os.path.join(self.t.dir, "bambu_filament_ids.json")
ubfi.write_map(path, rows, "testcommit", "2026-09-04")
@@ -845,104 +757,6 @@ class TestCheck6(OfCleanTreeCase):
self.assertIn('declares no "bambu_id"', out)
# ---------------------------------------------------------------------------
# --update-snapshot
# ---------------------------------------------------------------------------
class TestUpdateSnapshot(SyntheticTreeCase):
def test_idempotent_and_deterministic(self):
with open(self.t.snapshot, "rb") as f:
first = f.read()
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 0)
self.assertIn("nothing changed", out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), first)
self.assertTrue(first.endswith(b"\n"))
self.assertNotIn(b"\r", first)
snap = json.loads(first.decode("utf-8"))
self.assertEqual(list(snap), ["ids"]) # state only, no exception lists
self.assertEqual(list(snap["ids"]), sorted(snap["ids"]))
self.assertEqual(snap["ids"]["AX01"], {
"filaments": ["VendorA/APLA"], "name": "APLA",
"filament_type": "PLA", "filament_vendor": "AVendor"})
self.assertEqual(snap["ids"]["OGFL99"], {
"filaments": ["OrcaFilamentLibrary/Generic PLA", "VendorA/Generic PLA"],
"name": "Generic PLA", "filament_type": "PLA", "filament_vendor": "Generic"})
# Key order is part of the on-disk format.
self.assertEqual(list(snap["ids"]["AX01"]),
["filaments", "name", "filament_type", "filament_vendor"])
def test_refuses_a_tree_it_could_not_read(self):
# A bundle that does not parse contributes no ids, so sanctioning the
# rest would record the loss as a deliberate removal.
with open(self.t.snapshot, "rb") as f:
before = f.read()
with open(os.path.join(self.t.profiles, "VendorA",
"filament", "APLA @base.json"), "w",
encoding="utf-8") as f:
f.write("{ not json")
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 1, out)
self.assertIn("unreadable filament profile", out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before)
def test_refuses_an_id_declared_under_two_triples(self):
# VendorB re-declares APLA's id for a different product: one id, two
# triples. No single entry can describe it, and check 3 rejects it anyway.
self.t.add_vendor("VendorB", [
preset("BPLA @base", filament_id="AX01", instantiation=False,
filament_vendor="BVendor", filament_type="PLA"),
preset("BPLA @P1", inherits="BPLA @base", compatible_printers=["P1"]),
])
with open(self.t.snapshot, "rb") as f:
before = f.read()
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 1)
self.assertIn('refusing to sanction filament_id "AX01": declared under 2 triples '
'(AVendor/PLA/APLA; BVendor/PLA/BPLA)', out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before) # nothing written on refusal
def test_records_an_id_it_cannot_defend_and_lets_check_reject_it(self):
# update-snapshot records state, it does not judge ids: a foreign id
# lands in the diff a maintainer reviews, and fails check 1 straight
# after. Sanctioning it does not grandfather it.
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="GFX99",
instantiation=False,
filament_vendor="CV",
filament_type="PLA"))
self.t.write_preset("VendorA", preset("CNEW @P1", inherits="CNEW @base",
compatible_printers=["P1"]))
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 0, out)
with open(self.t.snapshot, encoding="utf-8") as f:
self.assertIn("GFX99", json.load(f)["ids"])
errors, out = self.t.check()
self.assertGreater(errors, 0)
self.assertIn('is not a minted "OF" id', out)
def test_dry_run_reports_without_writing(self):
self.t.write_preset("VendorA", preset("ANEW @P2", inherits="APLA @base",
compatible_printers=["P2"]))
with open(self.t.snapshot, "rb") as f:
before = f.read()
rc, out = self.t.update_snapshot(dry_run=True)
self.assertEqual(rc, 0)
self.assertIn("would be rewritten", out)
self.assertIn("claims added : 1", out)
with open(self.t.snapshot, "rb") as f:
self.assertEqual(f.read(), before)
# the real run writes exactly what the dry run reported
rc, out = self.t.update_snapshot()
self.assertEqual(rc, 0)
self.assertIn("snapshot written", out)
snap = load_json_file(self.t.snapshot)
self.assertEqual(snap["ids"]["AX01"]["filaments"],
["VendorA/ANEW", "VendorA/APLA"])
# ---------------------------------------------------------------------------
# --generate: one rule for inserts and rewrites alike
# ---------------------------------------------------------------------------
@@ -1026,7 +840,7 @@ class TestAssign(OfCleanTreeCase):
def test_parent_of_another_filament_never_receives_the_key(self):
# Members whose id-less parent belongs to another filament (here one
# parent shared by two filaments) carry the key themselves: the
# parent's own triple would mint a different id (check 3).
# parent's own triple would mint a different id (check 2).
self.t.write_preset("VendorA", preset("shared_base", instantiation=False,
filament_vendor="SV",
filament_type="PLA"))
@@ -1044,8 +858,6 @@ class TestAssign(OfCleanTreeCase):
parent = load_json_file(self.t.preset_path("VendorA", "shared_base"))
self.assertNotIn("filament_id", parent)
# ... and the tree they leave behind passes the identity check.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
@@ -1538,33 +1350,10 @@ class TestCli(unittest.TestCase):
rc = afi.main([])
self.assertEqual(rc, 0)
self.assertIn("usage:", buf.getvalue())
for command in ("check", "generate-id", "normalize", "trim", "update-index",
"update-snapshot"):
for command in ("check", "generate-id", "normalize", "trim", "update-index"):
self.assertIn(command, buf.getvalue())
self.assertEqual(self.t.bytes_map(), before)
def test_another_tree_needs_its_own_snapshot(self):
# --profiles retargets the tree, but the sanctioned state of that tree
# is not the repo snapshot: checking against it is meaningless and
# re-recording into it would overwrite the tracked file.
with open(afi.SNAPSHOT_PATH, "rb") as f:
repo_snapshot = f.read()
for command in ("check", "update-snapshot"):
with self.assertRaises(SystemExit) as caught:
with contextlib.redirect_stderr(io.StringIO()):
afi.main([command, "--profiles", self.t.profiles])
self.assertEqual(caught.exception.code, 2, command)
with open(afi.SNAPSHOT_PATH, "rb") as f:
self.assertEqual(f.read(), repo_snapshot)
# Named explicitly, both commands run against that tree.
rc, out = self.t.cli("update-snapshot")
self.assertEqual(rc, 0, out)
# generate-id never reads the snapshot, so it keeps working without one.
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.main(["generate-id", "--dry-run", "--profiles", self.t.profiles])
self.assertEqual(rc, 0, buf.getvalue())
def test_filament_id_and_setting_id_together_are_rejected(self):
# Each flag's help promises it skips the other kind, so the pair cannot
# quietly mean "both".
@@ -1667,8 +1456,8 @@ class TestCli(unittest.TestCase):
"setting_id", load_json_file(self.t.preset_path("VendorA", name)))
def test_check_returns_1_on_errors(self):
# What CI keys off: check exits nonzero when the tree does not match the
# snapshot it is validated against.
# What CI keys off: check exits nonzero when the tree breaks a rule, here
# the baseline's ids that are not minted.
before = self.t.bytes_map()
rc, out = self.t.cli("check")
self.assertEqual(rc, 1)
@@ -1695,12 +1484,12 @@ class TestCli(unittest.TestCase):
["--check"], # the pre-subcommand flag
["--update-snapshot"], # the pre-subcommand flag
["nonsense"], # not a command
["update-snapshot"], # removed command
["generate-id", "--filament-id", "--setting-id"],
["generate-id", "--snapshot", "x"], # check's option
["check", "--materials"], # removed flag
["check", "--obsolete-keys"], # removed flag
["check", "--snapshot", "x"], # removed flag
["check", "--filament-id"], # generate-id's option
["normalize", "--snapshot", "x"], # not a snapshot command
["normalize", "--profile-type", "nozzle"]): # not a profile type
with self.subTest(argv=argv):
with self.assertRaises(SystemExit) as cm, \
@@ -1716,7 +1505,7 @@ class TestCli(unittest.TestCase):
@unittest.skipUnless(os.path.isdir(REAL_PROFILES), "resources/profiles not present")
class TestRealTree(unittest.TestCase):
def test_shipped_snapshot_matches_tree(self):
def test_shipped_filament_ids_pass(self):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
errors = afi.check_filament_ids(REAL_PROFILES)
@@ -1778,10 +1567,10 @@ class TestReviewFixes(OfCleanTreeCase):
self.assertIn(path, str(caught.exception))
self.assertIn("test edit", str(caught.exception))
def test_check3_skips_of_id_inherited_from_other_vendor(self):
def test_check2_accepts_an_of_id_inherited_from_another_vendor(self):
# An OFL filament carries its own minted OF id and a vendor tunes it
# correctly (same base name, non-empty printers). The new claim must
# trip only the snapshot gate, never mint conformance.
# correctly (same base name, non-empty printers): the id it inherits is
# the mint of its own triple.
fid = afi.generate_filament_id("Generic", "PLA", "Generic PLA Matte")
self.t.write_preset(OFL, preset("Generic PLA Matte @base", filament_id=fid,
instantiation=False,
@@ -1790,22 +1579,13 @@ class TestReviewFixes(OfCleanTreeCase):
self.t.write_preset(OFL, preset("Generic PLA Matte @System",
inherits="Generic PLA Matte @base",
compatible_printers=[]))
rc, _ = self.t.update_snapshot()
self.assertEqual(rc, 0)
self.t.write_preset("VendorA", preset("Generic PLA Matte @P1",
inherits="Generic PLA Matte @System",
compatible_printers=["P1 0.4 nozzle"]))
errors, out = self.t.check()
self.assertNotIn("does not match the mint", out)
self.assertIn("not sanctioned", out)
self.assertEqual(errors, 1, out)
# After sanctioning the claim the tree is fully green again.
rc, _ = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 0, out)
def test_check3c_prints_expected_mint(self):
def test_check2c_prints_expected_mint(self):
self.t.write_preset("VendorA", preset("Orphan PLA @P1",
compatible_printers=["P1 0.4 nozzle"],
filament_vendor="OV",
+8 -24
View File
@@ -527,9 +527,7 @@ class TestCheck(TreeCase):
# `check`, now that the per-vendor pass no longer skips it.
self.t.write(apt.OFL, "filament/Stray.json",
{"type": "filament", "name": "Stray"})
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn(f"{apt.OFL}/filament/Stray.json: no {apt.OFL}.json list "
f"references it", out)
@@ -598,9 +596,7 @@ class TestCheck(TreeCase):
self.t.write("V", "filament/A.json", {
"type": "filament", "name": "A", "silent_mode": "0"})
self.run_command("update-index")
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out) # normalization also rejects the obsolete key
self.assertIn("Obsolete key: 'silent_mode' found in V/filament/A.json", out)
self.assertIn("Files with warnings : 1", out)
@@ -623,9 +619,7 @@ class TestCheck(TreeCase):
"type": "machine", "name": "M 0.4 nozzle",
"default_filament_profile": ["A", "Nope"]})
self.t.index("V", "machine", "M 0.4 nozzle", "machine/M.json")
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn("Missing filament profile: 'Nope'", out)
@@ -635,9 +629,7 @@ class TestCheck(TreeCase):
self.bundle()
for sub in apt.PROFILE_SUBDIRS:
os.makedirs(os.path.join(self.t.profiles, apt.USER_DIR, "default", sub))
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
_rc, out = self.run_command("check", "--snapshot", snapshot)
_rc, out = self.run_command("check")
self.assertIn("Checked vendors : 1", out)
self.assertNotIn("user", out)
@@ -743,9 +735,7 @@ class TestCheck(TreeCase):
self.t.write("V", f"filament/Stray{n}.json",
{"type": "filament", "name": f"Stray{n}"})
self.t.write("V", "filament/NoType.json", {"name": "NoType"})
snapshot = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", snapshot)
rc, out = self.run_command("check", "--snapshot", snapshot)
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertEqual(out.count("update-index\" to add them"), 1, out)
self.assertEqual(out.count("or delete them"), 1, out)
@@ -797,11 +787,6 @@ class TestNormalized(TreeCase):
errors, gaps = apt.check_normalized(self.t.profiles, vendor)
return errors, gaps, buf.getvalue()
def snapshot(self):
path = os.path.join(self.t.dir, "snapshot.json")
self.run_command("update-snapshot", "--snapshot", path)
return path
def test_a_bundle_the_two_commands_just_wrote_reports_nothing(self):
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
self.t.write("V", "process/B.json", {"type": "process", "name": "B"})
@@ -862,7 +847,7 @@ class TestNormalized(TreeCase):
# library included.
self.t.write(apt.OFL, "filament/A.json",
{"type": "filament", "name": "A", "version": "01.00.00.00"})
rc, out = self.run_command("check", "--snapshot", self.snapshot())
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn(f"{apt.OFL}/filament/A.json: normalize would remove version", out)
@@ -872,7 +857,7 @@ class TestNormalized(TreeCase):
{"type": "filament", "name": f"A{n}",
"version": "01.00.00.00"})
self.t.write("W", "filament/B.json", {"type": "filament", "name": "B"})
rc, out = self.run_command("check", "--snapshot", self.snapshot())
rc, out = self.run_command("check")
self.assertEqual(rc, 1, out)
self.assertIn("3 profile file(s) above are not what", out)
self.assertEqual(out.count('normalize" writes: run it and commit'), 1, out)
@@ -898,8 +883,7 @@ class TestDispatch(TreeCase):
def test_an_option_belongs_to_one_command_only(self):
for argv in (["trim", "--force"],
["update-index", "--filament-id"],
["check", "--profile-type", "filament"],
["update-snapshot", "--vendor", "V"]):
["check", "--profile-type", "filament"]):
with self.subTest(argv=argv):
with self.assertRaises(SystemExit) as cm, \
contextlib.redirect_stdout(io.StringIO()), \