| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- """Tests for expected print promotion when auto_archive is disabled (#839).
- When auto_archive=False but a print was dispatched by BamBuddy (queue/reprint),
- the on_print_start callback must still promote the expected print to _active_prints
- so that at print completion the archive_id and ams_mapping are available for
- filament usage tracking.
- These are pure unit tests that verify the module-level dict manipulation logic
- directly, NOT by calling the full on_print_start callback.
- """
- import time
- import pytest
- from backend.app.main import (
- _active_prints,
- _expected_print_creators,
- _expected_print_registered_at,
- _expected_prints,
- _print_ams_mappings,
- register_expected_print,
- )
- @pytest.fixture(autouse=True)
- def _clear_dicts():
- """Clear module-level tracking dicts before and after each test."""
- _expected_prints.clear()
- _expected_print_registered_at.clear()
- _expected_print_creators.clear()
- _print_ams_mappings.clear()
- _active_prints.clear()
- yield
- _expected_prints.clear()
- _expected_print_registered_at.clear()
- _expected_print_creators.clear()
- _print_ams_mappings.clear()
- _active_prints.clear()
- class TestRegisterExpectedPrint:
- """Verify register_expected_print populates all tracking dicts."""
- def test_registers_filename_and_variants(self):
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- assert _expected_prints[(1, "Box.3mf")] == 54
- assert _expected_prints[(1, "Box")] == 54
- assert _expected_prints[(1, "Box.gcode")] == 54
- def test_stores_ams_mapping(self):
- register_expected_print(1, "test.3mf", archive_id=10, ams_mapping=[2, -1, 3])
- assert _print_ams_mappings[10] == [2, -1, 3]
- def test_no_ams_mapping_when_none(self):
- register_expected_print(1, "test.3mf", archive_id=10, ams_mapping=None)
- assert 10 not in _print_ams_mappings
- def test_stores_creator(self):
- register_expected_print(1, "test.3mf", archive_id=10, created_by_id=5)
- assert _expected_print_creators[(1, "test.3mf")] == 5
- def test_stores_registered_at(self):
- before = time.monotonic()
- register_expected_print(1, "test.3mf", archive_id=10)
- after = time.monotonic()
- ts = _expected_print_registered_at[(1, "test.3mf")]
- assert before <= ts <= after
- class TestExpectedPrintDetection:
- """Verify the expected-print detection logic used in on_print_start.
- Reproduces the key-building and lookup logic from the auto_archive=False
- block in on_print_start to verify that expected prints are correctly
- detected across all filename variations.
- """
- @staticmethod
- def _build_check_keys(printer_id: int, filename: str, subtask_name: str):
- """Reproduce the key-building logic from on_print_start."""
- check_keys = []
- if subtask_name:
- check_keys += [
- (printer_id, subtask_name),
- (printer_id, f"{subtask_name}.3mf"),
- (printer_id, f"{subtask_name}.gcode.3mf"),
- ]
- if filename:
- base_fn = filename.split("/")[-1] if "/" in filename else filename
- check_keys.append((printer_id, base_fn))
- no_archive_base = base_fn.replace(".gcode", "").replace(".3mf", "")
- check_keys += [
- (printer_id, no_archive_base),
- (printer_id, f"{no_archive_base}.3mf"),
- ]
- return check_keys
- def test_detects_expected_print_by_subtask(self):
- """Expected print is found when subtask_name matches."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- keys = self._build_check_keys(1, filename="", subtask_name="Box")
- assert any(k in _expected_prints for k in keys)
- def test_detects_expected_print_by_filename(self):
- """Expected print is found when filename matches."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- keys = self._build_check_keys(1, filename="Box.3mf", subtask_name="")
- assert any(k in _expected_prints for k in keys)
- def test_detects_expected_print_by_gcode_filename(self):
- """Expected print is found when MQTT reports .gcode filename."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- # MQTT sometimes reports gcode filename
- keys = self._build_check_keys(1, filename="Box.gcode", subtask_name="Box")
- assert any(k in _expected_prints for k in keys)
- def test_no_false_positive_for_different_file(self):
- """Expected print NOT found for a different filename."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- keys = self._build_check_keys(1, filename="Benchy.3mf", subtask_name="Benchy")
- assert not any(k in _expected_prints for k in keys)
- def test_no_false_positive_for_different_printer(self):
- """Expected print NOT found when printer_id doesn't match."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- keys = self._build_check_keys(2, filename="Box.3mf", subtask_name="Box")
- assert not any(k in _expected_prints for k in keys)
- def test_empty_expected_prints_returns_false(self):
- """No detection when _expected_prints is empty."""
- keys = self._build_check_keys(1, filename="test.3mf", subtask_name="test")
- assert not any(k in _expected_prints for k in keys)
- def test_filename_with_spaces_and_parens(self):
- """Handles filenames with spaces and parentheses (e.g. 'Box3.0_(2)_plate_5.3mf')."""
- register_expected_print(1, "Box3.0_(2)_plate_5.3mf", archive_id=54, ams_mapping=[1])
- keys = self._build_check_keys(
- 1,
- filename="Box3.0_(2)_plate_5.gcode",
- subtask_name="Box3.0_(2)_plate_5",
- )
- assert any(k in _expected_prints for k in keys)
- class TestExpectedPrintPromotion:
- """Verify that expected prints are correctly promoted to _active_prints.
- Reproduces the expected-print pop + promotion logic from on_print_start
- (lines 1468-1496) to verify that _active_prints is populated and
- _expected_prints is cleaned up.
- """
- @staticmethod
- def _simulate_expected_print_promotion(printer_id: int, subtask_name: str, filename: str, archive_filename: str):
- """Simulate the expected-print lookup and promotion from on_print_start."""
- expected_keys = []
- if subtask_name:
- expected_keys.append((printer_id, subtask_name))
- expected_keys.append((printer_id, f"{subtask_name}.3mf"))
- expected_keys.append((printer_id, f"{subtask_name}.gcode.3mf"))
- if filename:
- fname = filename.split("/")[-1] if "/" in filename else filename
- expected_keys.append((printer_id, fname))
- base = fname.replace(".gcode", "").replace(".3mf", "")
- expected_keys.append((printer_id, base))
- expected_keys.append((printer_id, f"{base}.3mf"))
- expected_archive_id = None
- for key in expected_keys:
- expected_archive_id = _expected_prints.pop(key, None)
- _expected_print_registered_at.pop(key, None)
- if expected_archive_id:
- for other_key in expected_keys:
- _expected_prints.pop(other_key, None)
- _expected_print_registered_at.pop(other_key, None)
- break
- if expected_archive_id:
- _active_prints[(printer_id, archive_filename)] = expected_archive_id
- if subtask_name:
- _active_prints[(printer_id, f"{subtask_name}.3mf")] = expected_archive_id
- return expected_archive_id
- def test_promotion_populates_active_prints(self):
- """After promotion, archive is in _active_prints."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- archive_id = self._simulate_expected_print_promotion(
- printer_id=1,
- subtask_name="Box",
- filename="Box.gcode",
- archive_filename="Box.3mf",
- )
- assert archive_id == 54
- assert _active_prints[(1, "Box.3mf")] == 54
- def test_promotion_cleans_up_expected_prints(self):
- """After promotion, _expected_prints is empty for this print."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- self._simulate_expected_print_promotion(
- printer_id=1,
- subtask_name="Box",
- filename="Box.gcode",
- archive_filename="Box.3mf",
- )
- # All variants should be cleaned up
- assert (1, "Box.3mf") not in _expected_prints
- assert (1, "Box") not in _expected_prints
- assert (1, "Box.gcode") not in _expected_prints
- def test_ams_mapping_survives_promotion(self):
- """_print_ams_mappings is NOT consumed during promotion — it's needed at completion."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- self._simulate_expected_print_promotion(
- printer_id=1,
- subtask_name="Box",
- filename="Box.gcode",
- archive_filename="Box.3mf",
- )
- # ams_mapping should still be available for on_print_complete
- assert _print_ams_mappings[54] == [1]
- def test_completion_lookup_finds_promoted_archive(self):
- """Simulate on_print_complete finding the archive in _active_prints."""
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- self._simulate_expected_print_promotion(
- printer_id=1,
- subtask_name="Box",
- filename="Box.gcode",
- archive_filename="Box.3mf",
- )
- # Simulate on_print_complete key building
- completion_keys = [
- (1, "Box.3mf"),
- (1, "Box.gcode.3mf"),
- (1, "Box"),
- ]
- found_id = None
- for key in completion_keys:
- found_id = _active_prints.pop(key, None)
- if found_id:
- break
- assert found_id == 54
- # And ams_mapping is retrievable
- assert _print_ams_mappings.pop(54, None) == [1]
- def test_no_promotion_for_external_print(self):
- """When no expected print exists, nothing is promoted."""
- archive_id = self._simulate_expected_print_promotion(
- printer_id=1,
- subtask_name="Benchy",
- filename="Benchy.gcode",
- archive_filename="Benchy.3mf",
- )
- assert archive_id is None
- assert len(_active_prints) == 0
- class TestAMSMappingInjection:
- """Verify ams_mapping injection into usage tracker session."""
- def test_injection_into_session(self):
- """ams_mapping from _print_ams_mappings is injectable into a session."""
- from datetime import datetime, timezone
- from backend.app.services.usage_tracker import PrintSession, _active_sessions
- _active_sessions.clear()
- # Create a session without ams_mapping (simulates MQTT not providing it)
- session = PrintSession(
- printer_id=1,
- print_name="Box",
- started_at=datetime.now(timezone.utc),
- tray_remain_start={},
- tray_now_at_start=-1,
- spool_assignments={},
- ams_mapping=None,
- )
- _active_sessions[1] = session
- # Register expected print with ams_mapping
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- # Simulate the injection logic from on_print_start
- _stored_map = _print_ams_mappings.get(54)
- assert _stored_map == [1]
- ut_session = _active_sessions.get(1)
- assert ut_session is not None
- assert ut_session.ams_mapping is None # before injection
- ut_session.ams_mapping = _stored_map # injection
- assert ut_session.ams_mapping == [1]
- _active_sessions.clear()
- def test_no_injection_when_session_already_has_mapping(self):
- """Don't overwrite existing ams_mapping in session."""
- from datetime import datetime, timezone
- from backend.app.services.usage_tracker import PrintSession, _active_sessions
- _active_sessions.clear()
- session = PrintSession(
- printer_id=1,
- print_name="Box",
- started_at=datetime.now(timezone.utc),
- tray_remain_start={},
- tray_now_at_start=-1,
- spool_assignments={},
- ams_mapping=[5, 6], # already has mapping from MQTT
- )
- _active_sessions[1] = session
- register_expected_print(1, "Box.3mf", archive_id=54, ams_mapping=[1])
- _stored_map = _print_ams_mappings.get(54)
- ut_session = _active_sessions.get(1)
- # Guard: don't overwrite if session already has a mapping
- if ut_session and not ut_session.ams_mapping:
- ut_session.ams_mapping = _stored_map
- assert ut_session.ams_mapping == [5, 6] # unchanged
- _active_sessions.clear()
|