"""Unit-Tests für Anycubic-MQTT-Codes, Task-Hygiene und 10101-Retry.""" import sys import time import unittest from pathlib import Path from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from grotti import anycubic_lan from grotti.anycubic_lan import ( CODE_FILAMENT, CODE_TASK_ACTIVE, LanError, ensure_task_free, message_for_code, start_print, task_summary, ) class MessageForCodeTests(unittest.TestCase): def test_10101(self): text = message_for_code(CODE_TASK_ACTIVE) self.assertIn("10101", text) self.assertIn("aktiv", text.lower()) def test_10107(self): text = message_for_code(CODE_FILAMENT) self.assertIn("10107", text) self.assertIn("Filament", text) def test_unknown_with_default(self): text = message_for_code(99999, "Druckstart abgelehnt") self.assertIn("99999", text) self.assertIn("Druckstart abgelehnt", text) def test_string_code(self): self.assertEqual( message_for_code("10107"), message_for_code(10107), ) class ReportErrorTests(unittest.TestCase): def test_report_carries_code(self): text, code = anycubic_lan._report_error( {"code": 10101, "msg": "Print task already exists"}, "Druckstart abgelehnt", ) self.assertEqual(code, 10101) self.assertIn("10101", text) def test_raw_msg_unknown_code(self): text, code = anycubic_lan._report_error( {"code": 42, "msg": "irgendwas"}, "Druckstart abgelehnt", ) self.assertEqual(code, 42) self.assertIn("irgendwas", text) self.assertIn("42", text) class TaskSummaryTests(unittest.TestCase): def test_free(self): summary = task_summary({"state": "free", "project": {}}) self.assertEqual(summary["state"], "free") self.assertIsNone(summary["task_id"]) def test_busy_project(self): summary = task_summary({ "state": "printing", "project": {"task_id": 12345, "filename": "/tmp/foo.gcode"}, }) self.assertEqual(summary["task_id"], "12345") self.assertEqual(summary["filename"], "foo.gcode") def test_empty(self): self.assertIsNone(task_summary({})) self.assertIsNone(task_summary(None)) class TaskBusyTests(unittest.TestCase): def test_free_idle(self): self.assertFalse(anycubic_lan._task_busy({"state": "free"})) self.assertFalse(anycubic_lan._task_busy({"state": "idle"})) def test_s1_style_null_project(self): # S1 liefert project: null + last_project; last_project zählt nicht. self.assertFalse(anycubic_lan._task_busy({ "state": "free", "project": None, "last_project": {"task_id": "old"}, })) def test_busy_state(self): self.assertTrue(anycubic_lan._task_busy({"state": "printing"})) self.assertTrue(anycubic_lan._task_busy({"state": "auto_leveling"})) def test_busy_project(self): self.assertTrue(anycubic_lan._task_busy({ "state": "free", "project": {"task_id": "9"}, })) class PrintInfoSoftFailTests(unittest.TestCase): def tearDown(self): anycubic_lan._FAIL_CACHE.clear() anycubic_lan._INFO_CACHE.clear() @mock.patch("grotti.anycubic_lan.credentials", side_effect=LanError("weg")) def test_credentials_fail_returns_none(self, _cred): self.assertIsNone(anycubic_lan.print_info("10.0.0.9", use_cache=False)) self.assertIn("10.0.0.9", anycubic_lan._FAIL_CACHE) @mock.patch("grotti.anycubic_lan.credentials", side_effect=LanError("weg")) def test_fail_cache_skips_retry(self, cred): anycubic_lan._FAIL_CACHE["10.0.0.9"] = time.time() self.assertIsNone(anycubic_lan.print_info("10.0.0.9", use_cache=True)) cred.assert_not_called() @mock.patch("grotti.anycubic_lan.print_info", return_value=None) def test_ensure_free_when_lan_down(self, _info): self.assertIsNone(ensure_task_free("10.0.0.9")) class EnsureTaskFreeTests(unittest.TestCase): @mock.patch("grotti.anycubic_lan.time.sleep", return_value=None) @mock.patch("grotti.anycubic_lan.cancel_print") @mock.patch("grotti.anycubic_lan.print_info") def test_already_free(self, print_info, cancel_print, _sleep): print_info.return_value = {"state": "free", "project": {}} info = ensure_task_free("10.0.0.1") self.assertEqual(info["state"], "free") cancel_print.assert_not_called() @mock.patch("grotti.anycubic_lan.time.sleep", return_value=None) @mock.patch("grotti.anycubic_lan.cancel_print") @mock.patch("grotti.anycubic_lan.print_info") def test_clears_busy(self, print_info, cancel_print, _sleep): print_info.side_effect = [ {"state": "printing", "project": {"task_id": "1"}}, {"state": "free", "project": {}}, ] info = ensure_task_free("10.0.0.1", attempts=3, wait_s=0.01) self.assertEqual(info["state"], "free") cancel_print.assert_called_once() @mock.patch("grotti.anycubic_lan.time.sleep", return_value=None) @mock.patch("grotti.anycubic_lan.cancel_print") @mock.patch("grotti.anycubic_lan.print_info") def test_raises_when_stuck(self, print_info, cancel_print, _sleep): print_info.return_value = {"state": "printing", "project": {"task_id": "1"}} with self.assertRaises(LanError) as ctx: ensure_task_free("10.0.0.1", attempts=2, wait_s=0.01) self.assertEqual(cancel_print.call_count, 2) self.assertIn("freimachen", ctx.exception.message) class StartPrintRetryTests(unittest.TestCase): @mock.patch("grotti.anycubic_lan.time.sleep", return_value=None) @mock.patch("grotti.anycubic_lan.ensure_task_free") @mock.patch("grotti.anycubic_lan.cancel_print") @mock.patch("grotti.anycubic_lan._mqtt_print") def test_retry_on_10101(self, mqtt_print, cancel_print, ensure_free, _sleep): ensure_free.return_value = {"state": "free"} mqtt_print.side_effect = [ LanError(message_for_code(CODE_TASK_ACTIVE), code=CODE_TASK_ACTIVE), {"state": "printing"}, ] result = start_print("10.0.0.1", "demo.gcode", auto_leveling=False) self.assertEqual(result["filename"], "demo.gcode") self.assertEqual(mqtt_print.call_count, 2) cancel_print.assert_called_once() @mock.patch("grotti.anycubic_lan.time.sleep", return_value=None) @mock.patch("grotti.anycubic_lan.ensure_task_free") @mock.patch("grotti.anycubic_lan._mqtt_print") def test_no_retry_on_filament(self, mqtt_print, ensure_free, _sleep): ensure_free.return_value = {"state": "free"} mqtt_print.side_effect = LanError( message_for_code(CODE_FILAMENT), code=CODE_FILAMENT, ) with self.assertRaises(LanError) as ctx: start_print("10.0.0.1", "demo.gcode") self.assertEqual(ctx.exception.code, CODE_FILAMENT) self.assertEqual(mqtt_print.call_count, 1) if __name__ == "__main__": unittest.main()