Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.
This commit is contained in:
commit
b2e6443c68
69 files changed
+18719
No files matched your search
@@ -0,0 +1,198 @@
|
||||
"""Tests für Abräum-Bahn und Normalisierung."""
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from grotti import eject
|
||||
from grotti.errors import GrottiError
|
||||
|
||||
|
||||
class EjectTests(unittest.TestCase):
|
||||
def test_default_steps(self):
|
||||
steps = eject.normalize_steps([
|
||||
{"type": "print", "file_id": 1, "bed_leveling": True},
|
||||
{"type": "cool"},
|
||||
{"type": "sweep"},
|
||||
{"type": "check"},
|
||||
])
|
||||
self.assertEqual([s["type"] for s in steps], ["print", "cool", "sweep", "check"])
|
||||
self.assertEqual(steps[0]["file_id"], 1)
|
||||
self.assertTrue(steps[0]["bed_leveling"])
|
||||
|
||||
def test_needs_print(self):
|
||||
with self.assertRaises(GrottiError):
|
||||
eject.normalize_steps([{"type": "cool"}])
|
||||
|
||||
def test_x_positions_inclusive(self):
|
||||
sweep = eject.normalize_sweep({"x_from": 25, "x_to": 45, "x_step": 10})
|
||||
self.assertEqual(eject.x_positions(sweep), [25.0, 35.0, 45.0])
|
||||
|
||||
def test_sweep_y_order(self):
|
||||
with self.assertRaises(GrottiError):
|
||||
eject.normalize_sweep({"y_from": 5, "y_to": 220})
|
||||
|
||||
def test_cool_ready(self):
|
||||
cool = eject.normalize_cool({"bed_max": 30, "nozzle_max": 45})
|
||||
self.assertFalse(eject.cool_ready({"bed": 40, "nozzle": 40}, cool))
|
||||
self.assertTrue(eject.cool_ready({"bed": 28, "nozzle": 40}, cool))
|
||||
|
||||
def test_cool_park_center(self):
|
||||
# Relativ +40, gegen z_max gekappt
|
||||
low = eject.cool_park_script({"position": [250, 220, 8.0], "z_max": 262}).replace(" ", "")
|
||||
self.assertIn("G91", low)
|
||||
self.assertIn("Z40.00", low)
|
||||
self.assertIn("X125.00", low)
|
||||
# Nah an Z-max → nur Restweg
|
||||
near = eject.cool_park_lift_mm({"position": [125, 125, 245.0], "z_max": 250})
|
||||
self.assertAlmostEqual(near, 5.0)
|
||||
capped = eject.cool_park_script({"position": [125, 125, 245.0], "z_max": 250}).replace(" ", "")
|
||||
self.assertIn("Z5.00", capped)
|
||||
self.assertTrue(
|
||||
eject.cool_parked({"position": [125, 125, 48]}, before_z=8.0, lift_mm=40.0)
|
||||
)
|
||||
self.assertFalse(
|
||||
eject.cool_parked({"position": [125, 125, 20]}, before_z=8.0, lift_mm=40.0)
|
||||
)
|
||||
self.assertFalse(eject.cool_parked({"position": [125, 125, 50]})) # ohne Hub-Nachweis
|
||||
self.assertFalse(eject.cool_parked({"position": [250, 220, 50]}, before_z=10, lift_mm=40))
|
||||
|
||||
def test_sweep_does_not_lower_before_back(self):
|
||||
script = eject.sweep_probe_height_script(eject.normalize_sweep({}))
|
||||
# Y hinten (Default 250) vor dem Senken auf z_push
|
||||
y_idx = script.find("Y250")
|
||||
z_push_idx = script.find("Z2.00")
|
||||
self.assertGreater(y_idx, 0)
|
||||
self.assertGreater(z_push_idx, y_idx)
|
||||
# Senken langsam — nicht F3000 ins Bett
|
||||
self.assertIn("F%d" % eject.SWEEP_FEED_DOWN, script)
|
||||
down = script[z_push_idx : z_push_idx + 40]
|
||||
self.assertNotIn("F3000", down)
|
||||
|
||||
def test_assert_clear_before_lower(self):
|
||||
sweep = eject.normalize_sweep({})
|
||||
eject.assert_clear_before_lower({"position": [125, 250, 40.0]}, sweep)
|
||||
with self.assertRaises(GrottiError):
|
||||
eject.assert_clear_before_lower({"position": [125, 250, 8.0]}, sweep)
|
||||
|
||||
def test_check_delta_migrates_retract_workaround(self):
|
||||
check = eject.normalize_check({"max_delta_mm": 2.5})
|
||||
self.assertAlmostEqual(check["max_delta_mm"], 3.0)
|
||||
self.assertAlmostEqual(eject.normalize_check({"max_delta_mm": 1.0}).get("max_delta_mm"), 3.0)
|
||||
self.assertAlmostEqual(eject.normalize_check({}).get("max_delta_mm"), 3.0)
|
||||
err = eject.explain_printer_error("No trigger on probe after full movement")
|
||||
self.assertIn("Wägezelle", err)
|
||||
self.assertNotIn("Induktiv", err)
|
||||
|
||||
def test_check_z_ignores_toolhead(self):
|
||||
z, src = eject.check_z_from_live({"probe_z": None, "position": [125, 125, 1.7]})
|
||||
self.assertIsNone(z)
|
||||
z, src = eject.check_z_from_live({"probe_z": 0.37})
|
||||
self.assertAlmostEqual(z, 0.37)
|
||||
self.assertEqual(src, "probe")
|
||||
|
||||
def test_sweep_respects_single_x(self):
|
||||
sweep = eject.normalize_sweep({"x_from": 125, "x_to": 125, "x_step": 0})
|
||||
script = eject.sweep_script(sweep)
|
||||
# eine Schiebe-Spur: genau ein Absenken auf z_push, ein Mal nach vorn
|
||||
self.assertEqual(script.count("Z2.00"), 1)
|
||||
self.assertEqual(script.count("Y5.00"), 1)
|
||||
self.assertLess(script.find("Y250"), script.find("Z2.00"))
|
||||
|
||||
def test_assert_at_push_height(self):
|
||||
sweep = eject.normalize_sweep({})
|
||||
eject.assert_at_push_height({"position": [125, 220, 2.0]}, sweep)
|
||||
with self.assertRaises(GrottiError):
|
||||
eject.assert_at_push_height({"position": [125, 220, 18.0]}, sweep)
|
||||
|
||||
def test_step_labels(self):
|
||||
self.assertEqual(eject.step_label({"type": "check"}), "Tasten")
|
||||
self.assertEqual(eject.step_label({"type": "sweep"}), "Schieben")
|
||||
|
||||
def test_check_script_probe(self):
|
||||
script = eject.check_script({"x": 125, "y": 125}, current_z=50.0)
|
||||
self.assertEqual(script.count("PROBE"), 1)
|
||||
self.assertNotIn("SET_KINEMATIC_POSITION", script)
|
||||
self.assertNotIn("BED_MESH_CLEAR", script)
|
||||
self.assertIn("G0 Z50.00", script)
|
||||
self.assertIn("X125", script.replace(" ", ""))
|
||||
# Auch bei schlechter Z: kein Budget-Fake, nur z_clear
|
||||
low = eject.check_script({"x": 125, "y": 125}, current_z=-4.0)
|
||||
self.assertEqual(low.count("PROBE"), 1)
|
||||
self.assertNotIn("SET_KINEMATIC_POSITION", low)
|
||||
self.assertIn("G0 Z25.00", low)
|
||||
|
||||
def test_restore_uses_live_xyz(self):
|
||||
script, z = eject.restore_position_script(
|
||||
{"position": [250.0, 220.0, 48.0]}, {"z_push": 2}
|
||||
)
|
||||
self.assertAlmostEqual(z, 48.0)
|
||||
self.assertIn("SET_KINEMATIC_POSITION", script)
|
||||
self.assertIn("X=250.000", script)
|
||||
self.assertIn("Y=220.000", script)
|
||||
self.assertIn("Z=48.000", script)
|
||||
with self.assertRaises(GrottiError):
|
||||
eject.restore_position_script({"position": [125, 125, 1.0]}, {"z_push": 2})
|
||||
|
||||
def test_strip_end_motor_disable(self):
|
||||
raw = "G1 X250 Y220 F12000\nM84; disable motors\nM140 S0\n"
|
||||
out = eject.strip_end_motor_disable(raw)
|
||||
self.assertNotIn("M84", out)
|
||||
self.assertIn("G1 X250 Y220", out)
|
||||
self.assertIn("M140 S0", out)
|
||||
# Kommentarzeile mit M84 bleibt
|
||||
commented = "; M84; disable motors\nG1 X1\n"
|
||||
self.assertIn("; M84", eject.strip_end_motor_disable(commented))
|
||||
|
||||
def test_probe_result_regex(self):
|
||||
from grotti import moonraker
|
||||
|
||||
match = moonraker._PROBE_RESULT_RE.search("Result is z=0.374500\n")
|
||||
self.assertIsNotNone(match)
|
||||
self.assertAlmostEqual(float(match.group(1)), 0.3745)
|
||||
|
||||
|
||||
class StoreQueueOutTests(unittest.TestCase):
|
||||
def test_legacy_mesh_hidden(self):
|
||||
from grotti import store
|
||||
|
||||
row = {
|
||||
"id": 1,
|
||||
"file_id": 1,
|
||||
"host": "10.0.0.1",
|
||||
"family": "kobra_3",
|
||||
"priority": 100,
|
||||
"status": "running",
|
||||
"spool_id": None,
|
||||
"ace_map": [],
|
||||
"bed_leveling": True,
|
||||
"override_machine": False,
|
||||
"message": "ok",
|
||||
"steps": [
|
||||
{"type": "print", "file_id": 1},
|
||||
{"type": "cool"},
|
||||
{"type": "sweep"},
|
||||
{"type": "check", "probe_count": 3, "mesh_min_x": 0},
|
||||
{"type": "check", "max_delta_mm": 1},
|
||||
],
|
||||
"step_index": 4,
|
||||
"repeat_total": 1,
|
||||
"repeat_done": 0,
|
||||
"phase": "",
|
||||
"phase_started_at": None,
|
||||
"created_at": None,
|
||||
"updated_at": None,
|
||||
"printer_name": "A",
|
||||
"filename": "x.gcode",
|
||||
}
|
||||
out = store.print_queue_out(row, "x.gcode")
|
||||
self.assertEqual(out["step_labels"], ["Drucken", "Abkühlen", "Schieben", "Tasten"])
|
||||
self.assertEqual(out["step_label_current"], "Tasten")
|
||||
self.assertTrue(out["is_ablauf"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Zustandsmaschine der Druckwarteschlange mit Fake-Moonraker."""
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from grotti.print_queue import PRINT_START_TIMEOUT_S, PrintQueueDispatcher, _cool_wait_text
|
||||
|
||||
|
||||
class CoolTextTests(unittest.TestCase):
|
||||
def test_wait_text(self):
|
||||
cool = {"bed_max": 30, "nozzle_max": 45}
|
||||
text = _cool_wait_text({"bed": 40, "nozzle": 50}, cool)
|
||||
self.assertIn("Düse", text)
|
||||
self.assertIn("Bett", text)
|
||||
self.assertNotIn(" · ", text)
|
||||
|
||||
|
||||
class DispatcherHelpers(unittest.TestCase):
|
||||
def test_timeout_constant(self):
|
||||
self.assertGreaterEqual(PRINT_START_TIMEOUT_S, 300)
|
||||
|
||||
def test_worker_busy_empty(self):
|
||||
d = PrintQueueDispatcher("postgresql://x", {}, watcher=None)
|
||||
self.assertFalse(d._worker_busy(1))
|
||||
|
||||
def test_auto_enabled_default(self):
|
||||
d = PrintQueueDispatcher("postgresql://x", {}, watcher=None)
|
||||
self.assertTrue(d._auto_enabled())
|
||||
d.cfg = {"auto_queue": False}
|
||||
self.assertFalse(d._auto_enabled())
|
||||
|
||||
|
||||
class ResolveSpoolTests(unittest.TestCase):
|
||||
def test_material_match(self):
|
||||
from grotti.jobs import assert_material_match
|
||||
from grotti.errors import GrottiError
|
||||
|
||||
file_row = {"filaments": [{"type": "PLA"}]}
|
||||
assert_material_match(file_row, [{"material_type": "PLA"}])
|
||||
with self.assertRaises(GrottiError):
|
||||
assert_material_match(file_row, [{"material_type": "PETG"}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""store.print_queue_out und Hilfen ohne Datenbank."""
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from grotti.store import print_queue_out
|
||||
|
||||
|
||||
class PrintQueueOutTests(unittest.TestCase):
|
||||
def test_basic_fields(self):
|
||||
now = datetime(2026, 10, 14, 12, 0, tzinfo=timezone.utc)
|
||||
out = print_queue_out(
|
||||
{
|
||||
"id": 7,
|
||||
"file_id": 3,
|
||||
"filename": "teil.gcode",
|
||||
"host": "10.10.1.10",
|
||||
"printer_name": "A2",
|
||||
"family": "kobra_3",
|
||||
"priority": 50,
|
||||
"status": "waiting",
|
||||
"spool_id": None,
|
||||
"ace_map": [{"paint_index": 0, "ams_index": 1}],
|
||||
"bed_leveling": True,
|
||||
"override_machine": False,
|
||||
"message": "wartet",
|
||||
"steps": [],
|
||||
"step_index": 0,
|
||||
"repeat_total": 1,
|
||||
"repeat_done": 0,
|
||||
"phase": "",
|
||||
"phase_started_at": None,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
)
|
||||
self.assertEqual(out["id"], 7)
|
||||
self.assertEqual(out["host"], "10.10.1.10")
|
||||
self.assertEqual(out["family_label"], "Kobra 3")
|
||||
self.assertTrue(out["bed_leveling"])
|
||||
self.assertEqual(out["ace_map"][0]["ams_index"], 1)
|
||||
self.assertFalse(out["is_ablauf"])
|
||||
self.assertEqual(out["filename"], "teil.gcode")
|
||||
|
||||
def test_ablauf_labels_skip_legacy_mesh(self):
|
||||
steps = [
|
||||
{"type": "print"},
|
||||
{"type": "cool"},
|
||||
{"type": "sweep"},
|
||||
{"type": "check", "probe_count": 3, "mesh_min_x": 0},
|
||||
{"type": "check", "max_delta_mm": 1},
|
||||
]
|
||||
out = print_queue_out(
|
||||
{
|
||||
"id": 1,
|
||||
"file_id": 1,
|
||||
"filename": "x.gcode",
|
||||
"host": "h",
|
||||
"printer_name": "",
|
||||
"family": "kobra_3",
|
||||
"priority": 100,
|
||||
"status": "running",
|
||||
"spool_id": None,
|
||||
"ace_map": [],
|
||||
"bed_leveling": False,
|
||||
"override_machine": False,
|
||||
"message": "",
|
||||
"steps": steps,
|
||||
"step_index": 2,
|
||||
"repeat_total": 2,
|
||||
"repeat_done": 0,
|
||||
"phase": "cooling",
|
||||
"phase_started_at": None,
|
||||
"created_at": None,
|
||||
"updated_at": None,
|
||||
}
|
||||
)
|
||||
self.assertTrue(out["is_ablauf"])
|
||||
self.assertNotIn("mesh", " ".join(out["step_labels"]).lower())
|
||||
self.assertEqual(out["step_labels"][0], "Drucken")
|
||||
self.assertIn(out["step_label_current"], out["step_labels"])
|
||||
self.assertEqual(out["repeat_total"], 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user