mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Run every profile maintenance job from one tool (#15726)
* Run every profile maintenance job from one tool orca_id_tool.py becomes orca_profile_tool.py, and orca_extra_profile_check.py and orca_filament_lib.py fold into it as subcommands: check, generate-id, fix, trim, update-index and update-snapshot. The three scripts already overlapped -- the checker imported half of its rules from the id tool, which in turn kept a copy-pasted set of output helpers to avoid the resulting import cycle -- while disagreeing on how a vendor is enumerated, how a JSON file is read and what the exit code means. One file settles all three. check, normalize, trim and update-index reproduce their predecessors exactly; normalize and update-index were diffed byte-for-byte against the old scripts over a copy of the whole tree. Deliberate changes: the compatible-printers check no longer switches itself off when --check-materials is passed, an error exits 1 rather than -1, update-index honours --profile-type and reports a profile it cannot place instead of dropping it from the index, fix and update-index gained --dry-run, trim keeps an unindexed file some surviving profile still inherits from, and vendors are enumerated as directories with an index -- which is why blacklist.json, a data file that an unscoped index rebuild once wrote four empty list sections into, loses them here and will not collect them again. The dead rename_filament_system() helper is gone. The suite under scripts/tests now covers the maintenance commands too, and CI runs it; nothing there ran in CI before. No shipped profile data changes apart from those four keys. * update vendor index files with "python3 ./scripts/orca_profile_tool.py update-index" and "python3 ./scripts/orca_profile_tool.py normalize"
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for scripts/orca_id_tool.py (stdlib unittest, no external deps).
|
||||
"""Tests for scripts/orca_profile_tool.py (stdlib unittest, no external deps).
|
||||
|
||||
Run from the repo root: python -m unittest discover -s scripts/tests -v
|
||||
"""
|
||||
@@ -17,7 +17,7 @@ import uuid
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
import orca_id_tool as afi # noqa: E402
|
||||
import orca_profile_tool as afi # noqa: E402
|
||||
import update_bambu_filament_ids as ubfi # noqa: E402
|
||||
|
||||
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
@@ -164,12 +164,14 @@ class SyntheticTree:
|
||||
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
|
||||
return changed, errors, buf.getvalue()
|
||||
|
||||
def cli(self, *flags):
|
||||
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, "--profiles", self.profiles,
|
||||
"--snapshot", self.snapshot])
|
||||
rc = afi.main(flags)
|
||||
return rc, buf.getvalue()
|
||||
|
||||
|
||||
@@ -426,26 +428,6 @@ class TestTripleResolution(unittest.TestCase):
|
||||
("MyVendor", "PLA", "MyPLA"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reserved namespaces
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReservedSpaces(unittest.TestCase):
|
||||
def test_owners(self):
|
||||
# Bambu AMS/RFID catalog: reserved, but no vendor (not even BBL) may declare it
|
||||
self.assertEqual(afi.reserved_space_owner("GFL99"), (True, None))
|
||||
# Qidi device protocol: reserved, but no vendor may declare it
|
||||
self.assertEqual(afi.reserved_space_owner("QD_X4_PLA"), (True, None))
|
||||
self.assertEqual(afi.reserved_space_owner("P1234abc"), (True, None))
|
||||
self.assertEqual(afi.reserved_space_owner("pAbCdEf1"), (True, None)) # case-insensitive
|
||||
self.assertEqual(afi.reserved_space_owner("null"), (True, None))
|
||||
self.assertEqual(afi.reserved_space_owner("OF5CgdDq"), (False, None))
|
||||
self.assertEqual(afi.reserved_space_owner("P1234abcd"), (False, None)) # 8 hex chars: not the user space
|
||||
|
||||
def test_gf_is_reserved_and_ownerless(self):
|
||||
self.assertEqual(afi.reserved_space_owner("GFA00"), (True, None))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# checks on synthetic trees
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -474,7 +456,7 @@ class TestChecks(OfCleanTreeCase):
|
||||
errors, out = self.t.check()
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('claim "VendorA/ANEW" is not sanctioned', out)
|
||||
self.assertIn("--update-snapshot", out)
|
||||
self.assertIn("update-snapshot", out)
|
||||
|
||||
def test_check2_vanished_claim_is_stability_error(self):
|
||||
self.t.remove_preset("VendorA", "APLA @P1")
|
||||
@@ -669,11 +651,12 @@ class TestChecks(OfCleanTreeCase):
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn("does not match the mint of its triple", out)
|
||||
|
||||
def test_check4_reserved_namespace_claims(self):
|
||||
for fid, marker in [("GFX99", "Bambu AMS/RFID catalog"),
|
||||
("QD_X_PLA", "composed by the device"),
|
||||
("P1a2b3c4", "user-custom"),
|
||||
("null", "user-custom")]:
|
||||
def test_check1_an_id_another_system_composed_is_not_a_mint(self):
|
||||
# Nothing is reserved because nothing is exempt: an id some other system
|
||||
# composes for its own purposes - Bambu's catalog, a Qidi box, the dialog
|
||||
# that creates a user filament - is simply not the mint of a triple, and
|
||||
# check 1 rejects it for that and nothing else.
|
||||
for fid in ("GFX99", "QD_X_PLA", "P1a2b3c4", "null"):
|
||||
with self.subTest(fid=fid):
|
||||
name = f"R{fid} @base"
|
||||
self.t.write_preset("VendorA", preset(name, filament_id=fid,
|
||||
@@ -684,8 +667,8 @@ class TestChecks(OfCleanTreeCase):
|
||||
compatible_printers=["P1"]))
|
||||
errors, out = self.t.check()
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn("reserved id space", out)
|
||||
self.assertIn(marker, out)
|
||||
self.assertIn(f'filament_id "{fid}"', out)
|
||||
self.assertIn('is not a minted "OF" id', out)
|
||||
|
||||
def test_check3c_unresolvable_instantiated_filament(self):
|
||||
self.t.write_preset("VendorA", preset("DNEW @P1", compatible_printers=["P1"]))
|
||||
@@ -713,7 +696,7 @@ class TestCheck5(OfCleanTreeCase):
|
||||
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 5a.
|
||||
# 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()
|
||||
@@ -736,7 +719,7 @@ class TestCheck5(OfCleanTreeCase):
|
||||
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 5b.
|
||||
# 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()
|
||||
@@ -922,20 +905,23 @@ class TestUpdateSnapshot(SyntheticTreeCase):
|
||||
with open(self.t.snapshot, "rb") as f:
|
||||
self.assertEqual(f.read(), before) # nothing written on refusal
|
||||
|
||||
def test_refuses_reserved_namespace_ids(self):
|
||||
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"]))
|
||||
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", out)
|
||||
with open(self.t.snapshot, "rb") as f:
|
||||
self.assertEqual(f.read(), before) # nothing written on refusal
|
||||
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",
|
||||
@@ -1536,7 +1522,7 @@ class TestRemint(SyntheticTreeCase):
|
||||
|
||||
class TestCli(unittest.TestCase):
|
||||
"""main(argv) over a synthetic tree. The clean tree's baseline ids are
|
||||
deliberately non-conformant ("AX01"/"OGFL99"), so a --generate run always
|
||||
deliberately non-conformant ("AX01"/"OGFL99"), so a generate-id run always
|
||||
has both a filament_id rewrite and setting_id inserts to do."""
|
||||
|
||||
def setUp(self):
|
||||
@@ -1544,17 +1530,17 @@ class TestCli(unittest.TestCase):
|
||||
self.addCleanup(self.t.cleanup)
|
||||
|
||||
def test_bare_invocation_prints_help(self):
|
||||
# Naming no command is not an error: it is how you find out what the
|
||||
# commands are, and it must never be mistaken for a run that did work.
|
||||
before = self.t.bytes_map()
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = afi.main([])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("usage:", buf.getvalue())
|
||||
self.assertIn("--generate", buf.getvalue())
|
||||
# ... and so does any invocation naming no mode: help, and no work
|
||||
before = self.t.bytes_map()
|
||||
rc, out = self.t.cli()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("usage:", out)
|
||||
for command in ("check", "generate-id", "normalize", "trim", "update-index",
|
||||
"update-snapshot"):
|
||||
self.assertIn(command, buf.getvalue())
|
||||
self.assertEqual(self.t.bytes_map(), before)
|
||||
|
||||
def test_another_tree_needs_its_own_snapshot(self):
|
||||
@@ -1563,20 +1549,20 @@ class TestCli(unittest.TestCase):
|
||||
# re-recording into it would overwrite the tracked file.
|
||||
with open(afi.SNAPSHOT_PATH, "rb") as f:
|
||||
repo_snapshot = f.read()
|
||||
for mode in ("--check", "--update-snapshot"):
|
||||
for command in ("check", "update-snapshot"):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
afi.main([mode, "--profiles", self.t.profiles])
|
||||
self.assertEqual(caught.exception.code, 2, mode)
|
||||
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 modes run against that tree.
|
||||
rc, out = self.t.cli("--update-snapshot")
|
||||
# Named explicitly, both commands run against that tree.
|
||||
rc, out = self.t.cli("update-snapshot")
|
||||
self.assertEqual(rc, 0, out)
|
||||
# --generate never reads the snapshot, so it keeps working without one.
|
||||
# generate-id never reads the snapshot, so it keeps working without one.
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = afi.main(["--dry-run", "--profiles", self.t.profiles])
|
||||
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):
|
||||
@@ -1584,7 +1570,7 @@ class TestCli(unittest.TestCase):
|
||||
# quietly mean "both".
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
afi.main(["--generate", "--filament-id", "--setting-id",
|
||||
afi.main(["generate-id", "--filament-id", "--setting-id",
|
||||
"--profiles", self.t.profiles])
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
|
||||
@@ -1593,14 +1579,14 @@ class TestCli(unittest.TestCase):
|
||||
path = self.t.preset_path("VendorA", "APLA @base")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("{ not json")
|
||||
rc, out = self.t.cli("--generate")
|
||||
rc, out = self.t.cli("generate-id")
|
||||
self.assertEqual(rc, 1, out)
|
||||
self.assertIn("error(s)", out)
|
||||
self.assertNotIn("SUCCESS", out)
|
||||
|
||||
def test_dry_run_alone_previews_generate(self):
|
||||
def test_dry_run_previews_generate_id(self):
|
||||
before = self.t.bytes_map()
|
||||
rc, out = self.t.cli("--dry-run")
|
||||
rc, out = self.t.cli("generate-id", "--dry-run")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("would ", out)
|
||||
self.assertIn("nothing written", out)
|
||||
@@ -1615,7 +1601,7 @@ class TestCli(unittest.TestCase):
|
||||
filament_type="PLA", compatible_printers=["P1"]))
|
||||
before = self.t.bytes_map()
|
||||
|
||||
rc, out = self.t.cli("--generate")
|
||||
rc, out = self.t.cli("generate-id")
|
||||
|
||||
self.assertEqual(rc, 0, out)
|
||||
after = self.t.bytes_map()
|
||||
@@ -1629,7 +1615,7 @@ class TestCli(unittest.TestCase):
|
||||
|
||||
def test_dryrun_is_the_same_flag(self):
|
||||
before = self.t.bytes_map()
|
||||
rc, out = self.t.cli("--dryrun")
|
||||
rc, out = self.t.cli("generate-id", "--dryrun")
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertIn("would ", out) # the same preview, not a silent no-op
|
||||
self.assertIn("nothing written", out)
|
||||
@@ -1637,7 +1623,7 @@ class TestCli(unittest.TestCase):
|
||||
|
||||
def test_generate_vendor_writes_only_in_that_bundle(self):
|
||||
before = self.t.bytes_map()
|
||||
rc, out = self.t.cli("--generate", "--vendor", "VendorA")
|
||||
rc, out = self.t.cli("generate-id", "--vendor", "VendorA")
|
||||
self.assertEqual(rc, 0, out)
|
||||
after = self.t.bytes_map()
|
||||
changed = sorted(rel for rel in before if after[rel] != before[rel])
|
||||
@@ -1646,7 +1632,7 @@ class TestCli(unittest.TestCase):
|
||||
self.assertTrue(rel.startswith("VendorA" + os.sep), rel)
|
||||
# The bundles it spared were not simply already conformant: the
|
||||
# un-narrowed run goes on to write in them too.
|
||||
rc, out = self.t.cli("--generate")
|
||||
rc, out = self.t.cli("generate-id")
|
||||
self.assertEqual(rc, 0, out)
|
||||
final = self.t.bytes_map()
|
||||
self.assertTrue(any(final[rel] != after[rel] for rel in after
|
||||
@@ -1654,13 +1640,13 @@ class TestCli(unittest.TestCase):
|
||||
|
||||
def test_generate_unknown_vendor_returns_1(self):
|
||||
before = self.t.bytes_map()
|
||||
rc, out = self.t.cli("--generate", "--vendor", "Nope")
|
||||
rc, out = self.t.cli("generate-id", "--vendor", "Nope")
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("unknown vendor", out)
|
||||
self.assertEqual(self.t.bytes_map(), before)
|
||||
|
||||
def test_setting_id_only_leaves_filament_ids_alone(self):
|
||||
rc, out = self.t.cli("--generate", "--setting-id")
|
||||
rc, out = self.t.cli("generate-id", "--setting-id")
|
||||
self.assertEqual(rc, 0, out)
|
||||
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
||||
self.assertEqual(root["filament_id"], "AX01") # not re-minted
|
||||
@@ -1671,7 +1657,7 @@ class TestCli(unittest.TestCase):
|
||||
"APLA @P1"))
|
||||
|
||||
def test_filament_id_only_inserts_no_setting_id(self):
|
||||
rc, out = self.t.cli("--generate", "--filament-id")
|
||||
rc, out = self.t.cli("generate-id", "--filament-id")
|
||||
self.assertEqual(rc, 0, out)
|
||||
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
|
||||
self.assertEqual(root["filament_id"],
|
||||
@@ -1680,24 +1666,40 @@ class TestCli(unittest.TestCase):
|
||||
self.assertNotIn(
|
||||
"setting_id", load_json_file(self.t.preset_path("VendorA", name)))
|
||||
|
||||
def test_check_mode_returns_1_on_errors(self):
|
||||
# What CI keys off: --check exits nonzero when the tree does not match
|
||||
# the snapshot it is validated against.
|
||||
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.
|
||||
before = self.t.bytes_map()
|
||||
rc, out = self.t.cli("--check")
|
||||
rc, out = self.t.cli("check")
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("error(s)", out)
|
||||
self.assertEqual(self.t.bytes_map(), before) # --check never writes
|
||||
self.assertIn("Files with errors", out)
|
||||
self.assertEqual(self.t.bytes_map(), before) # check never writes
|
||||
|
||||
def test_check_vendor_narrows_the_per_vendor_pass(self):
|
||||
# check_profile.sh passes --vendor to this command, so it has to be
|
||||
# accepted -- and it must narrow only the per-vendor half.
|
||||
rc, out = self.t.cli("check", "--vendor", "VendorA")
|
||||
self.assertEqual(rc, 1, out) # the tree-wide checks still ran
|
||||
self.assertIn("Checked vendors : 1", out)
|
||||
|
||||
def test_an_empty_vendor_means_every_vendor(self):
|
||||
# check_profile.sh cannot expand an empty array under set -u, so it
|
||||
# passes --vendor "" to mean "all of them".
|
||||
_rc, scoped = self.t.cli("check", "--vendor", "")
|
||||
_rc, unscoped = self.t.cli("check")
|
||||
self.assertEqual(scoped, unscoped)
|
||||
|
||||
def test_removed_and_conflicting_flags_are_rejected(self):
|
||||
for argv in (["--remint", "VendorA"], # removed mode
|
||||
["--mint", "A/B/C"], # removed mode
|
||||
["--drop-redundant-ids", "VendorA"], # removed mode
|
||||
["--assign"], # removed mode
|
||||
["--generate", "--check"], # two modes
|
||||
["--vendor", "VendorA"], # narrowing without a mode
|
||||
["--filament-id"], # narrowing without a mode
|
||||
["--check", "--vendor", "VendorA"]): # narrowing on --check
|
||||
for argv in (["--remint", "VendorA"], # removed mode
|
||||
["--generate"], # the pre-subcommand flag
|
||||
["--check"], # the pre-subcommand flag
|
||||
["--update-snapshot"], # the pre-subcommand flag
|
||||
["nonsense"], # not a command
|
||||
["generate-id", "--filament-id", "--setting-id"],
|
||||
["generate-id", "--materials"], # check's option
|
||||
["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, \
|
||||
contextlib.redirect_stdout(io.StringIO()), \
|
||||
@@ -1722,7 +1724,7 @@ class TestRealTree(unittest.TestCase):
|
||||
# The exact CI invocation, return code included.
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = afi.main(["--check"])
|
||||
rc = afi.main(["check"])
|
||||
self.assertEqual(rc, 0, buf.getvalue())
|
||||
|
||||
def test_every_instantiated_filament_resolves_an_id(self):
|
||||
|
||||
Reference in New Issue
Block a user