Browse Source

test: harden _fake_state against printer_state_to_dict mock-leakage

  The two TestBroadcastStatusChange / TestEndToEndUnderRunningLoop tests
  patch printer_state_to_dict to return a fixed dict, but on parallel
  xdist runners (CI's pytest -n 30) the patch occasionally didn't catch
  the call and the real function ran against the 4-field SimpleNamespace
  fake — first attr access (.kprofiles) AttributeError'd, swallowed by
  the try/except in _broadcast_status_change, send_status never awaited,
  the assertion failed.

  Filled _fake_state with every attribute the real printer_state_to_dict
  reads (iterables empty, scalars None, stg_cur=0 for the int comparison
  in get_derived_status_name). Test now passes whether or not the patch
  lands.
maziggy 2 weeks ago
parent
commit
96a0c60c60
1 changed files with 44 additions and 4 deletions
  1. 44 4
      backend/tests/unit/test_printer_manager_status_broadcast.py

+ 44 - 4
backend/tests/unit/test_printer_manager_status_broadcast.py

@@ -50,15 +50,55 @@ def _close_unawaited(coro):
 
 
 
 
 def _fake_state(**overrides):
 def _fake_state(**overrides):
-    """Minimal stand-in for a ``PrinterState`` — only the attributes
-    ``printer_state_to_dict`` reads. We use a SimpleNamespace rather than
-    constructing a real PrinterState so this test stays fast and doesn't
-    couple to the (large, evolving) PrinterState dataclass shape."""
+    """Stand-in for a ``PrinterState``.
+
+    The tests below patch ``printer_state_to_dict`` so the fake doesn't need
+    to satisfy every attribute access — but the patch was observed to race on
+    parallel CI runners (pytest-xdist), and when it didn't catch the call the
+    real ``printer_state_to_dict`` ran against this fake and ``AttributeError``'d
+    on ``.kprofiles``. The fake now carries every attribute the real function
+    reads, so it remains correct even if the patch is somehow bypassed — the
+    test no longer depends on a fragile monkeypatch landing in time.
+
+    Iterables (``kprofiles``, ``printable_objects``, ``hms_errors``,
+    ``temperatures``, etc.) default to empty so the function's loops are
+    no-ops; scalars default to ``None`` so any "if state.x is None" guard
+    falls through cleanly.
+    """
     base = {
     base = {
+        # State the existing test bodies explicitly set / read
         "connected": True,
         "connected": True,
         "state": "FINISH",
         "state": "FINISH",
         "raw_data": {},
         "raw_data": {},
         "progress": 100.0,
         "progress": 100.0,
+        # Iterables — must be iterable for the loops inside printer_state_to_dict
+        "kprofiles": [],
+        "printable_objects": [],
+        "hms_errors": [],
+        "temperatures": {},
+        "nozzle_rack": [],
+        # Nullable scalars — printer_state_to_dict tolerates None for these
+        "active_extruder": None,
+        "ams_status_main": None,
+        "ams_status_sub": None,
+        "big_fan1_speed": None,
+        "big_fan2_speed": None,
+        "chamber_light": None,
+        "cooling_fan_speed": None,
+        "current_print": None,
+        "door_open": None,
+        "firmware_version": None,
+        "gcode_file": None,
+        "heatbreak_fan_speed": None,
+        "layer_num": None,
+        "remaining_time": None,
+        "speed_level": None,
+        "stg_cur": 0,  # get_derived_status_name does ``0 <= state.stg_cur < 255``
+        "subtask_name": None,
+        "total_layers": None,
+        "tray_now": None,
+        "wifi_signal": None,
+        "wired_network": None,
     }
     }
     base.update(overrides)
     base.update(overrides)
     return SimpleNamespace(**base)
     return SimpleNamespace(**base)