Grotti 3.3.0: Projekte mit Ordner, Datei-Löschen/Verschieben, Material-Archiv, UI-Polish.
This commit is contained in:
1 parent
4ddffbbeb1
commit
67a89d1794
45 files changed
+2856
-320
No files matched your search
@@ -28,6 +28,47 @@ class ChooseSpoolTests(unittest.TestCase):
|
||||
with patch("grotti.jobs.store.spools_for_printer", return_value=[old]):
|
||||
self.assertEqual(jobs.choose_spool(conn, "h", "PLA", None, ended_at=end)["id"], 1)
|
||||
|
||||
def test_prefers_host_spool_over_stale_hint(self):
|
||||
"""Rollenwechsel: Hint im Regal, neue Rolle am Drucker → Abzug auf die neue."""
|
||||
end = datetime(2026, 10, 18, 14, 58, tzinfo=timezone.utc)
|
||||
hinted = {
|
||||
"id": 4,
|
||||
"material_type": "PLA",
|
||||
"printer_host": "",
|
||||
"created_at": end - timedelta(days=5),
|
||||
}
|
||||
current = {
|
||||
"id": 14,
|
||||
"material_type": "PLA",
|
||||
"printer_host": "h",
|
||||
"created_at": end - timedelta(minutes=10),
|
||||
}
|
||||
conn = MagicMock()
|
||||
with patch("grotti.jobs.store.spools_for_printer", return_value=[current]):
|
||||
with patch("grotti.jobs.store.get_spool_row", return_value=hinted):
|
||||
self.assertEqual(jobs.choose_spool(conn, "h", "PLA", 4, ended_at=end)["id"], 14)
|
||||
|
||||
def test_keeps_hint_when_no_host_spool(self):
|
||||
end = datetime(2026, 10, 18, 14, 58, tzinfo=timezone.utc)
|
||||
hinted = {
|
||||
"id": 4,
|
||||
"material_type": "PLA",
|
||||
"printer_host": "",
|
||||
"created_at": end - timedelta(days=5),
|
||||
}
|
||||
conn = MagicMock()
|
||||
with patch("grotti.jobs.store.spools_for_printer", return_value=[]):
|
||||
with patch("grotti.jobs.store.get_spool_row", return_value=hinted):
|
||||
self.assertEqual(jobs.choose_spool(conn, "h", "PLA", 4, ended_at=end)["id"], 4)
|
||||
|
||||
def test_resolve_tracking_spool_prefers_host(self):
|
||||
conn = MagicMock()
|
||||
with patch(
|
||||
"grotti.jobs.resolve_spools_at_host",
|
||||
return_value=[{"id": 14, "printer_host": "h"}],
|
||||
):
|
||||
self.assertEqual(jobs.resolve_tracking_spool(conn, "h", 4), 14)
|
||||
|
||||
def test_spool_valid_for_end_naive(self):
|
||||
end = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc)
|
||||
spool = {"created_at": datetime(2026, 9, 1, 12, 0)}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Sorten-Routing: Profil (Meta/Matte) und Auto-ACE-Map."""
|
||||
|
||||
import unittest
|
||||
|
||||
from grotti import store
|
||||
|
||||
|
||||
class ProfileMatchTests(unittest.TestCase):
|
||||
def test_meta_not_matte(self):
|
||||
self.assertTrue(store._profile_matches("Sunlu PLA Meta", "Sunlu PLA Meta"))
|
||||
self.assertTrue(store._profile_matches("Sunlu PLA Meta", "Sunlu PLA Meta schwarz"))
|
||||
self.assertFalse(store._profile_matches("Sunlu PLA Meta", "Sunlu PLA Matte"))
|
||||
self.assertFalse(store._profile_matches("Sunlu PLA Matte", "Sunlu PLA"))
|
||||
self.assertFalse(store._profile_matches("Sunlu PLA", "Sunlu PLA Matte"))
|
||||
self.assertFalse(store._profile_matches("Sunlu PLA", "Sunlu PLA Meta"))
|
||||
|
||||
def test_empty_profile_always_ok(self):
|
||||
self.assertTrue(store._profile_matches("", "Sunlu PLA Meta"))
|
||||
|
||||
|
||||
class MatchSpoolsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.have = [
|
||||
{
|
||||
"spool_id": 1,
|
||||
"material_type": "pla",
|
||||
"color": "#ff0000",
|
||||
"remaining_g": 800,
|
||||
"material_name": "sunlu pla meta",
|
||||
"ace_slot": 1,
|
||||
},
|
||||
{
|
||||
"spool_id": 2,
|
||||
"material_type": "pla",
|
||||
"color": "#000000",
|
||||
"remaining_g": 500,
|
||||
"material_name": "sunlu pla matte",
|
||||
"ace_slot": 2,
|
||||
},
|
||||
]
|
||||
|
||||
def test_picks_meta_not_matte(self):
|
||||
need = {"profile": "Sunlu PLA Meta", "material_type": "pla", "color": "", "paint_index": 0}
|
||||
matched = store._match_spools_for_need(need, self.have)
|
||||
self.assertEqual(len(matched), 1)
|
||||
self.assertEqual(matched[0]["spool_id"], 1)
|
||||
|
||||
def test_color_soft_among_profile(self):
|
||||
need = {
|
||||
"profile": "Sunlu PLA Meta",
|
||||
"material_type": "pla",
|
||||
"color": "#00ff00",
|
||||
"paint_index": 0,
|
||||
}
|
||||
# Nur Meta vorhanden, Farbe anders → trotzdem Meta (Farbe soft)
|
||||
matched = store._match_spools_for_need(need, self.have)
|
||||
self.assertEqual(matched[0]["spool_id"], 1)
|
||||
|
||||
def test_auto_ace_map_assigns_slots(self):
|
||||
# Ohne DB: Logik über _match + manuell wie auto_ace_map
|
||||
need = [
|
||||
{"profile": "Sunlu PLA Meta", "material_type": "pla", "color": "#ff0000", "paint_index": 1},
|
||||
{"profile": "Sunlu PLA Matte", "material_type": "pla", "color": "#000000", "paint_index": 10},
|
||||
]
|
||||
used = set()
|
||||
mapping = []
|
||||
for n in need:
|
||||
matched = store._match_spools_for_need(n, self.have, used)
|
||||
self.assertTrue(matched)
|
||||
chosen = matched[0]
|
||||
used.add(chosen["spool_id"])
|
||||
mapping.append({"paint_index": n["paint_index"], "ams_index": int(chosen["ace_slot"]) - 1})
|
||||
self.assertEqual(mapping, [
|
||||
{"paint_index": 1, "ams_index": 0},
|
||||
{"paint_index": 10, "ams_index": 1},
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,6 +23,90 @@ class OrcaSendenHelpersTest(unittest.TestCase):
|
||||
"http://127.0.0.1:8770/dateien?file=42",
|
||||
)
|
||||
|
||||
def test_app_page_url(self):
|
||||
self.assertEqual(
|
||||
orca.app_page_url("http://127.0.0.1:8770/"),
|
||||
"http://127.0.0.1:8770/dateien?orca=1",
|
||||
)
|
||||
|
||||
def test_browser_key_collapses_duplicates(self):
|
||||
self.assertEqual(
|
||||
orca.browser_key("1018-1648-Round Clip PLA + PETG(01)_PLA_0.2_30m34s.gcode"),
|
||||
orca.browser_key("1018-1648-Round_Clip_PLA_+_PETG(01)_PLA_0.2_30m34s-2.gcode"),
|
||||
)
|
||||
|
||||
def test_browser_key_strips_time_prefix(self):
|
||||
self.assertEqual(
|
||||
orca.browser_key("1018-1706-Round Clip PLA + PETG(01)_PLA_0.2_30m34s.gcode"),
|
||||
orca.browser_key("1018-1709-Round_Clip_PLA_+_PETG(01)_PLA_0.2_30m34s.gcode"),
|
||||
)
|
||||
self.assertEqual(
|
||||
orca.browser_key("1018-1709-Round Clip PLA + PETG(01)_PLA_0.2_30m34s.gcode"),
|
||||
"roundclipplapetg01pla0230m34s",
|
||||
)
|
||||
|
||||
def test_open_browser_debounces(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lock = Path(tmp) / "browser.lock"
|
||||
focus = Path(tmp) / "focus.json"
|
||||
with patch.object(orca, "BROWSER_LOCK", lock):
|
||||
with patch.object(orca, "FOCUS_PATH", focus):
|
||||
with patch.object(orca, "open_as_app", return_value=False):
|
||||
with patch.object(orca, "webbrowser") as browser:
|
||||
orca.open_browser(
|
||||
"http://127.0.0.1:8770/dateien?file=1",
|
||||
key="roundclip",
|
||||
file_id=1,
|
||||
mode="tab",
|
||||
)
|
||||
orca.open_browser(
|
||||
"http://127.0.0.1:8770/dateien?file=2",
|
||||
key="roundclip",
|
||||
file_id=2,
|
||||
mode="tab",
|
||||
)
|
||||
self.assertEqual(browser.open.call_count, 1)
|
||||
data = json.loads(focus.read_text(encoding="utf-8"))
|
||||
self.assertEqual(data["file_id"], 2)
|
||||
|
||||
def test_open_browser_app_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lock = Path(tmp) / "browser.lock"
|
||||
focus = Path(tmp) / "focus.json"
|
||||
with patch.object(orca, "BROWSER_LOCK", lock):
|
||||
with patch.object(orca, "FOCUS_PATH", focus):
|
||||
with patch.object(orca, "app_already_running", return_value=False):
|
||||
with patch.object(orca, "open_as_app", return_value=True) as app:
|
||||
with patch.object(orca, "webbrowser") as browser:
|
||||
orca.open_browser(
|
||||
"http://127.0.0.1:8770/dateien?orca=1",
|
||||
key="roundclip",
|
||||
file_id=9,
|
||||
mode="app",
|
||||
)
|
||||
app.assert_called_once()
|
||||
browser.open.assert_not_called()
|
||||
|
||||
def test_open_browser_reuses_running_app(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lock = Path(tmp) / "browser.lock"
|
||||
focus = Path(tmp) / "focus.json"
|
||||
with patch.object(orca, "BROWSER_LOCK", lock):
|
||||
with patch.object(orca, "FOCUS_PATH", focus):
|
||||
with patch.object(orca, "app_already_running", return_value=True):
|
||||
with patch.object(orca, "open_as_app", return_value=True) as app:
|
||||
with patch.object(orca, "webbrowser") as browser:
|
||||
orca.open_browser(
|
||||
"http://127.0.0.1:8770/dateien?orca=1",
|
||||
key="andere-datei",
|
||||
file_id=11,
|
||||
mode="app",
|
||||
)
|
||||
app.assert_not_called()
|
||||
browser.open.assert_not_called()
|
||||
data = json.loads(focus.read_text(encoding="utf-8"))
|
||||
self.assertEqual(data["file_id"], 11)
|
||||
|
||||
def test_find_gcode_arg_last_matching(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
folder = Path(tmp)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Sparse paint_index / Bambu-T-Nummern für ACE-Map."""
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from grotti import acm, parse, usage
|
||||
|
||||
|
||||
PAINT_SPARSE = json.dumps(
|
||||
[
|
||||
{"material_type": "PLA", "paint_color": [255, 128, 0], "paint_index": 1},
|
||||
{"material_type": "PLA", "paint_color": [0, 0, 0], "paint_index": 10},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class PaintIndexTests(unittest.TestCase):
|
||||
def test_filaments_keep_slicer_paint_index(self):
|
||||
rows = parse.filaments_from_paint_info(PAINT_SPARSE)
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(rows[0]["paint_index"], 1)
|
||||
self.assertEqual(rows[0]["type"], "PLA")
|
||||
self.assertEqual(rows[0]["color"], "#ff8000")
|
||||
self.assertEqual(rows[1]["paint_index"], 10)
|
||||
self.assertEqual(rows[1]["color"], "#000000")
|
||||
|
||||
def test_build_acm_uses_paint_index_not_array_slot(self):
|
||||
filaments = parse.filaments_from_paint_info(PAINT_SPARSE)
|
||||
mapping = [
|
||||
{"paint_index": 1, "ams_index": 3},
|
||||
{"paint_index": 10, "ams_index": 2},
|
||||
]
|
||||
slots = [
|
||||
{"index": 2, "type": "PLA", "color": "#f4f4f4"},
|
||||
{"index": 3, "type": "PLA", "color": "#c23b3b"},
|
||||
]
|
||||
payload = acm.build_acm(filaments, mapping, slots)
|
||||
paints = [item["paint_index"] for item in payload["ams_box_mapping"]]
|
||||
self.assertEqual(paints, [1, 10])
|
||||
by_paint = {item["paint_index"]: item for item in payload["ams_box_mapping"]}
|
||||
self.assertEqual(by_paint[1]["ams_index"], 3)
|
||||
self.assertEqual(by_paint[10]["ams_index"], 2)
|
||||
self.assertEqual(by_paint[1]["paint_color"], [255, 128, 0])
|
||||
self.assertEqual(by_paint[10]["paint_color"], [0, 0, 0])
|
||||
|
||||
def test_densify_mapping_and_rewrite_avoids_invalid_filament_id(self):
|
||||
mapping = [
|
||||
{"paint_index": 1, "ams_index": 3},
|
||||
{"paint_index": 10, "ams_index": 2},
|
||||
]
|
||||
remap, dense = acm.densify_mapping(mapping)
|
||||
self.assertEqual(remap, {1: 0, 10: 1})
|
||||
self.assertEqual(
|
||||
dense,
|
||||
[
|
||||
{"paint_index": 0, "ams_index": 3},
|
||||
{"paint_index": 1, "ams_index": 2},
|
||||
],
|
||||
)
|
||||
gcode = b"; hdr\nT10\nG1 X0\nT1 ; change\n; referenced_tools = 1,10\n"
|
||||
out = acm.rewrite_gcode_tools(gcode, remap)
|
||||
self.assertIn(b"T1\n", out)
|
||||
self.assertIn(b"T0 ; change", out)
|
||||
self.assertNotIn(b"T10", out)
|
||||
self.assertIn(b"; referenced_tools = 0,1", out)
|
||||
filaments = acm.remap_filaments(
|
||||
parse.filaments_from_paint_info(PAINT_SPARSE), remap
|
||||
)
|
||||
slots = [
|
||||
{"index": 2, "type": "PLA", "color": "#f4f4f4"},
|
||||
{"index": 3, "type": "PLA", "color": "#c23b3b"},
|
||||
]
|
||||
payload = acm.build_acm(filaments, dense, slots)
|
||||
paints = [item["paint_index"] for item in payload["ams_box_mapping"]]
|
||||
self.assertEqual(paints, [0, 1])
|
||||
self.assertEqual(payload["ams_box_mapping"][0]["paint_color"], [255, 128, 0])
|
||||
|
||||
def test_densify_identity_when_already_dense(self):
|
||||
mapping = [{"paint_index": 0, "ams_index": 1}, {"paint_index": 1, "ams_index": 2}]
|
||||
remap, dense = acm.densify_mapping(mapping)
|
||||
self.assertEqual(remap, {})
|
||||
self.assertEqual(dense, mapping)
|
||||
|
||||
def test_align_rewrites_when_ui_map_is_already_dense(self):
|
||||
"""Regression 10011700: UI schickt 0/1, G-code hat noch T1/T10."""
|
||||
gcode = b"; hdr\nT10\nG1 X0\nT1 ; change\n; referenced_tools = 1,10\n"
|
||||
mapping = [
|
||||
{"paint_index": 0, "ams_index": 3},
|
||||
{"paint_index": 1, "ams_index": 2},
|
||||
]
|
||||
filaments = [
|
||||
{"type": "PLA", "color": "#ff8000", "paint_index": 0},
|
||||
{"type": "PLA", "color": "#000000", "paint_index": 1},
|
||||
]
|
||||
out, dense, fils, remap = acm.align_tools_for_ace(gcode, mapping, filaments)
|
||||
self.assertEqual(remap, {1: 0, 10: 1})
|
||||
self.assertEqual(
|
||||
dense,
|
||||
[
|
||||
{"paint_index": 0, "ams_index": 3},
|
||||
{"paint_index": 1, "ams_index": 2},
|
||||
],
|
||||
)
|
||||
self.assertIn(b"T1\n", out)
|
||||
self.assertIn(b"T0 ; change", out)
|
||||
self.assertNotIn(b"T10", out)
|
||||
self.assertIn(b"; referenced_tools = 0,1", out)
|
||||
self.assertEqual([row["paint_index"] for row in fils], [0, 1])
|
||||
self.assertEqual(fils[0]["color"], "#ff8000")
|
||||
self.assertEqual(fils[1]["color"], "#000000")
|
||||
|
||||
def test_align_with_sparse_ui_map(self):
|
||||
gcode = b"T10\nT1\n"
|
||||
mapping = [
|
||||
{"paint_index": 1, "ams_index": 3},
|
||||
{"paint_index": 10, "ams_index": 2},
|
||||
]
|
||||
out, dense, fils, remap = acm.align_tools_for_ace(
|
||||
gcode, mapping, parse.filaments_from_paint_info(PAINT_SPARSE)
|
||||
)
|
||||
self.assertEqual(remap, {1: 0, 10: 1})
|
||||
self.assertEqual(dense[0]["ams_index"], 3)
|
||||
self.assertEqual(dense[1]["ams_index"], 2)
|
||||
self.assertNotIn(b"T10", out)
|
||||
self.assertEqual([row["paint_index"] for row in fils], [0, 1])
|
||||
|
||||
def test_tools_used_finds_t10(self):
|
||||
gcode = b"; header\nT10\nG1 X0\nT1 ; change\n"
|
||||
self.assertEqual(parse.tools_used_in_gcode(gcode), {1, 10})
|
||||
|
||||
def test_planned_shares_sparse_paint_falls_back_to_order(self):
|
||||
meta = {"filament_weight_total": [12.0, 8.0]}
|
||||
mapping = [
|
||||
{"paint_index": 1, "ams_index": 0},
|
||||
{"paint_index": 10, "ams_index": 1},
|
||||
]
|
||||
self.assertEqual(usage.planned_shares(meta, mapping, 2), [12.0, 8.0])
|
||||
|
||||
def test_profile_name_strips_machine(self):
|
||||
self.assertEqual(parse.profile_name('Sunlu PLA Meta @Kobra 3'), "Sunlu PLA Meta")
|
||||
self.assertEqual(parse.profile_name('"Sunlu PLA Matte @Kobra S1"'), "Sunlu PLA Matte")
|
||||
|
||||
def test_filaments_of_attaches_settings_id_profiles(self):
|
||||
# Slot 1 = PLA, Slot 10 = Matte — wie sparse paint_info
|
||||
profiles = ";".join(
|
||||
['"Sunlu PLA @Kobra 3"' if i not in (1, 10) else (
|
||||
'"Sunlu PLA @Kobra 3"' if i == 1 else '"Sunlu PLA Matte @Kobra 3"'
|
||||
) for i in range(15)]
|
||||
)
|
||||
comments = {
|
||||
"paint_info": PAINT_SPARSE,
|
||||
"filament_settings_id": profiles,
|
||||
}
|
||||
rows = parse.filaments_of(comments)
|
||||
self.assertEqual(rows[0]["profile"], "Sunlu PLA")
|
||||
self.assertEqual(rows[1]["profile"], "Sunlu PLA Matte")
|
||||
|
||||
def test_filaments_of_single_without_paint(self):
|
||||
comments = {
|
||||
"filament_type": "PLA",
|
||||
"filament_colour": "#00FF00",
|
||||
"filament_settings_id": '"Sunlu PLA Meta @Kobra 3"',
|
||||
}
|
||||
rows = parse.filaments_of(comments)
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["profile"], "Sunlu PLA Meta")
|
||||
self.assertEqual(rows[0]["type"], "PLA")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user