"""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()