test_vp_wire_dump.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. """Tests for the env-flagged VP wire-payload dump helper used to triage
  2. shape-of-payload bugs like #1622."""
  3. import json
  4. import os
  5. from unittest.mock import patch
  6. import pytest
  7. from backend.app.core.config import settings as app_settings
  8. from backend.app.services.virtual_printer import _debug
  9. @pytest.fixture
  10. def _isolated_log_dir(tmp_path, monkeypatch):
  11. with patch.object(app_settings, "log_dir", tmp_path):
  12. yield tmp_path
  13. def test_disabled_by_default_writes_nothing(_isolated_log_dir, monkeypatch):
  14. monkeypatch.delenv("BAMBUDDY_VP_DUMP_WIRE", raising=False)
  15. _debug.dump_wire("VP1", "out", {"hello": "world"})
  16. assert not (_isolated_log_dir / "vp_wire").exists()
  17. def test_enabled_writes_dict_as_pretty_json(_isolated_log_dir, monkeypatch):
  18. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  19. payload = {"print": {"ams": {"ams": [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA"}]}]}}}
  20. _debug.dump_wire("Bambuddy P1S", "out", payload)
  21. out = _isolated_log_dir / "vp_wire" / "Bambuddy_P1S_out.json"
  22. assert out.is_file()
  23. assert json.loads(out.read_text()) == payload
  24. def test_overwrites_on_repeat_call(_isolated_log_dir, monkeypatch):
  25. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  26. _debug.dump_wire("VP1", "out", {"v": 1})
  27. _debug.dump_wire("VP1", "out", {"v": 2})
  28. out = _isolated_log_dir / "vp_wire" / "VP1_out.json"
  29. assert json.loads(out.read_text()) == {"v": 2}
  30. def test_separate_files_per_direction(_isolated_log_dir, monkeypatch):
  31. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  32. _debug.dump_wire("VP1", "in", {"src": "printer"})
  33. _debug.dump_wire("VP1", "out", {"src": "slicer"})
  34. assert (_isolated_log_dir / "vp_wire" / "VP1_in.json").is_file()
  35. assert (_isolated_log_dir / "vp_wire" / "VP1_out.json").is_file()
  36. def test_sanitizes_path_traversal_in_vp_name(_isolated_log_dir, monkeypatch):
  37. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  38. _debug.dump_wire("../../etc/passwd", "out", {"x": 1})
  39. vp_wire = _isolated_log_dir / "vp_wire"
  40. files = list(vp_wire.glob("*"))
  41. assert len(files) == 1
  42. # The actual safety property: the written file is inside vp_wire/.
  43. # `..` as a substring of a single filename component is harmless because
  44. # the path separator (/) is collapsed to _ before construction.
  45. assert files[0].resolve().parent == vp_wire.resolve()
  46. assert "/" not in files[0].name
  47. def test_empty_vp_name_falls_back_to_default(_isolated_log_dir, monkeypatch):
  48. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  49. _debug.dump_wire("", "out", {"x": 1})
  50. assert (_isolated_log_dir / "vp_wire" / "vp_out.json").is_file()
  51. def test_bytes_payload_decoded(_isolated_log_dir, monkeypatch):
  52. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  53. _debug.dump_wire("VP1", "in", b'{"raw": true}')
  54. out = _isolated_log_dir / "vp_wire" / "VP1_in.json"
  55. assert out.read_text() == '{"raw": true}'
  56. def test_unwritable_dir_is_swallowed(_isolated_log_dir, monkeypatch):
  57. """A debug-instrumentation failure must not crash the bridge or 1Hz loop."""
  58. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  59. # Point log_dir at a location that mkdir refuses (a regular file occupying
  60. # the path). Failure must be swallowed.
  61. blocker = _isolated_log_dir / "blocker"
  62. blocker.write_text("not a dir")
  63. with patch.object(app_settings, "log_dir", blocker):
  64. _debug.dump_wire("VP1", "out", {"x": 1}) # must not raise
  65. @pytest.mark.parametrize("flag_value", ["0", "false", "off", "", "no"])
  66. def test_falsy_flag_values_disable(_isolated_log_dir, monkeypatch, flag_value):
  67. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", flag_value)
  68. _debug.dump_wire("VP1", "out", {"x": 1})
  69. assert not (_isolated_log_dir / "vp_wire").exists()
  70. @pytest.mark.parametrize("flag_value", ["1", "true", "TRUE", "yes", "on", "On"])
  71. def test_truthy_flag_values_enable(_isolated_log_dir, monkeypatch, flag_value):
  72. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", flag_value)
  73. _debug.dump_wire("VP1", "out", {"x": 1})
  74. assert (_isolated_log_dir / "vp_wire" / "VP1_out.json").is_file()
  75. def test_idempotent_atomic_no_partial_file_visible(_isolated_log_dir, monkeypatch):
  76. """tmp+rename pattern means a reader never sees a half-written .json file."""
  77. monkeypatch.setenv("BAMBUDDY_VP_DUMP_WIRE", "1")
  78. _debug.dump_wire("VP1", "out", {"x": 1})
  79. files = sorted(p.name for p in (_isolated_log_dir / "vp_wire").iterdir())
  80. # No leftover .tmp file after a successful write.
  81. assert files == ["VP1_out.json"]
  82. def test_env_check_is_per_call_not_module_load(_isolated_log_dir, monkeypatch):
  83. """Flag toggle must take effect on the next call without restarting; we
  84. re-read the env var inside ``dump_wire`` rather than caching at import."""
  85. monkeypatch.delenv("BAMBUDDY_VP_DUMP_WIRE", raising=False)
  86. _debug.dump_wire("VP1", "out", {"v": 1})
  87. assert not (_isolated_log_dir / "vp_wire").exists()
  88. os.environ["BAMBUDDY_VP_DUMP_WIRE"] = "1"
  89. try:
  90. _debug.dump_wire("VP1", "out", {"v": 2})
  91. assert (_isolated_log_dir / "vp_wire" / "VP1_out.json").is_file()
  92. finally:
  93. os.environ.pop("BAMBUDDY_VP_DUMP_WIRE", None)