| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- """Where the print file went, read off the report topic (#1820).
- Until this landed, ``current_project_url`` was assigned in exactly one place --
- ``_handle_request_message`` -- and ``_on_message`` calls that only for the
- request topic. A print started from the printer's own touchscreen publishes
- nothing there, so the field stayed None for the one case the storage verdict in
- ``print_storage`` was written for. The verdict then fell through to the
- ``sdcard`` fallback, and #1820's H2S reports ``sdcard: true`` (its "card" is the
- internal eMMC), so every such print swept ~110 doomed FTPS connections and
- archived blank with no stated reason.
- The printer does announce it, as an unsolicited ``project_file`` *response* on
- the report topic ~2 s before ``gcode_state`` reaches PREPARE. The frames below
- are from that reporter's sanitised capture, taken on an H2S + AMS 2 Pro.
- """
- import pytest
- from backend.app.services.bambu_mqtt import BambuMQTTClient
- from backend.app.services.print_storage import (
- REASON_INTERNAL_HISTORY,
- print_file_reachable_over_ftp,
- )
- pytestmark = pytest.mark.unit
- @pytest.fixture
- def client():
- return BambuMQTTClient(ip_address="10.0.0.7", serial_number="H2S1820", access_code="12345678", model="H2S")
- def screen_start(**overrides) -> dict:
- """The frame an H2S publishes for a print started from its own screen.
- Trimmed of the fields nothing here reads (``ams``, ``vt_tray``, the
- calibration flags); everything kept is verbatim from the capture, including
- the empty ``subtask_name`` and the printer's own ``sequence_id`` counter --
- the two things that distinguish it from a slicer's dispatch.
- """
- print_data = {
- "command": "project_file",
- "result": "SUCCESS",
- "reason": "SUCCESS",
- "err_code": 0,
- "sequence_id": "3338",
- "subtask_name": "",
- "task_type": 1,
- "param": "Metadata/plate_1.gcode",
- "plate": 1,
- "ams_mapping": [0],
- "mapping": [1],
- "url": "file:///userdata/model/history/JOB_A.gcode.3mf",
- }
- print_data.update(overrides)
- return {"print": print_data}
- class TestAScreenStartedPrintNamesItsFile:
- def test_the_url_is_captured(self, client):
- client._process_message(screen_start())
- assert client.state.current_project_url == "file:///userdata/model/history/JOB_A.gcode.3mf"
- def test_the_sticky_copy_is_captured_too(self, client):
- """The connection diagnostic runs after the print, by which point the
- per-print field has been cleared."""
- client._process_message(screen_start())
- assert client.state.last_project_url == "file:///userdata/model/history/JOB_A.gcode.3mf"
- def test_the_verdict_now_skips_the_sweep(self, client):
- """The whole point, in one assertion: with the URL in hand the H2S's
- ``sdcard: true`` no longer decides the outcome."""
- client.state.sdcard = True
- client.state.sdcard_reported = True
- client._process_message(screen_start())
- verdict = print_file_reachable_over_ftp(client.state)
- assert verdict.reachable is False
- assert verdict.reason == REASON_INTERNAL_HISTORY
- # Still probed, because this printer keeps screen-started jobs under
- # /cache for a while and that copy archives in full when it is there.
- assert verdict.probe_filename == "JOB_A.gcode.3mf"
- def test_the_mapping_is_captured_when_no_slicer_sent_one(self, client):
- """On a screen start this frame is the only place it appears."""
- client._process_message(screen_start())
- assert client._captured_ams_mapping == [0]
- def test_a_mapping_from_the_request_topic_wins(self, client):
- """The slicer's own mapping describes the same print and arrived first;
- the echo can carry a different shape or none at all."""
- client._captured_ams_mapping = [0, -1, -1, -1]
- client._process_message(screen_start(ams_mapping=[3]))
- assert client._captured_ams_mapping == [0, -1, -1, -1]
- class TestWhatMustNotBeCaptured:
- def test_a_refused_dispatch_is_ignored(self, client):
- """It names a file that was never written. Acting on it would pin the
- next print's archive on a destination nothing went to."""
- client._process_message(screen_start(result="FAIL", reason="STORAGE_FULL"))
- assert client.state.current_project_url is None
- assert client._captured_ams_mapping is None
- @pytest.mark.parametrize("url", ["", None, 12345, [], {}])
- def test_a_missing_or_non_string_url_is_ignored(self, client, url):
- """The value is whatever the sender put on the wire."""
- client._process_message(screen_start(url=url))
- assert client.state.current_project_url is None
- def test_another_command_on_the_same_topic_is_ignored(self, client):
- """push_status carries a `url` field of its own on some firmwares."""
- client._process_message({"print": {"command": "push_status", "url": "file:///userdata/model/history/x.3mf"}})
- assert client.state.current_project_url is None
- def test_our_own_dispatch_is_not_logged_as_someone_elses(self, client, caplog):
- """Our publish is echoed on *both* topics. The request-topic echo lands
- first and clears ``_own_project_file_key``, so a report-topic branch
- that reused ``_handle_request_message``'s diagnostic would report every
- Bambuddy-started print as an external one.
- """
- dispatch = {
- "command": "project_file",
- "sequence_id": "20002",
- "file": "JOB_NORMAL.gcode.3mf",
- "url": "brtc://emmc/JOB_NORMAL.gcode.3mf",
- "subtask_name": "JOB_NORMAL",
- }
- client._own_project_file_key = client._project_file_key(dispatch)
- client._handle_request_message({"print": dispatch})
- assert client._own_project_file_key is None
- caplog.clear()
- client._process_message({"print": {**dispatch, "result": "SUCCESS", "is_from_mqtt": True}})
- assert "External project_file payload" not in caplog.text
- # ...and the capture still happened, which is what makes it worth having
- # on this topic at all: some brokers refuse the request subscription.
- assert client.state.current_project_url == "brtc://emmc/JOB_NORMAL.gcode.3mf"
- class TestTheSlicerPathIsUnchanged:
- def test_an_external_dispatch_still_sweeps(self, client):
- """The regression to fear: routing more URLs into the matcher must not
- turn an ordinary Studio print into a blank archive."""
- client.state.sdcard = True
- client.state.sdcard_reported = True
- client._process_message(
- {"print": {"command": "project_file", "result": "SUCCESS", "url": "ftp://JOB_NORMAL.gcode.3mf"}}
- )
- assert print_file_reachable_over_ftp(client.state).reachable is True
|