test_camera_profiles.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. """Unit tests for camera profile registry.
  2. The registry decouples per-model camera tuning (probesize, analyzeduration,
  3. reconnect cadence) from the hard-coded constants that lived in
  4. ``camera.py`` until #1395 follow-up. Adding a new model's quirk should
  5. be a config edit, not a code change.
  6. """
  7. from dataclasses import FrozenInstanceError
  8. import pytest
  9. from backend.app.services.camera_profiles import (
  10. DEFAULT_PROFILE,
  11. CameraProfile,
  12. get_camera_profile,
  13. )
  14. class TestGetCameraProfile:
  15. def test_unknown_model_returns_default(self):
  16. """Models with no override fall through to DEFAULT_PROFILE so the
  17. camera path is never blocked on a missing entry."""
  18. assert get_camera_profile("UNKNOWN_MODEL") is DEFAULT_PROFILE
  19. assert get_camera_profile("Future_Bambu_Model_X42") is DEFAULT_PROFILE
  20. def test_none_model_returns_default(self):
  21. """`None` / empty model (very early in connect handshake) must not
  22. crash; the default profile is safe for any RTSP-capable printer."""
  23. assert get_camera_profile(None) is DEFAULT_PROFILE
  24. assert get_camera_profile("") is DEFAULT_PROFILE
  25. def test_default_profile_preserves_historical_fast_startup(self):
  26. """X1/H2 fast-startup tuning is the historical baseline. The first
  27. refactor must not regress it for the printers that already worked.
  28. """
  29. assert DEFAULT_PROFILE.probesize == 32
  30. assert DEFAULT_PROFILE.analyzeduration == 0
  31. assert DEFAULT_PROFILE.rtsp_reconnect_max == 30
  32. assert DEFAULT_PROFILE.rtsp_reconnect_delay == 0.2
  33. def test_p2s_has_relaxed_probe(self):
  34. """P2S firmware 01.02.00.00 needs more probe room — ffmpeg's own
  35. diagnostic says so. This is the first per-model override and the
  36. regression to guard."""
  37. profile = get_camera_profile("P2S")
  38. assert profile is not DEFAULT_PROFILE
  39. # Order of magnitude up from the default — enough to lock onto a
  40. # slow-keyframe stream without adding multi-second startup.
  41. assert profile.probesize >= 1_000_000
  42. assert profile.analyzeduration >= 500_000
  43. def test_p2s_regenerates_timestamps_from_wallclock(self):
  44. """P2S firmware 01.02.00.00 sends non-advancing RTP timestamps;
  45. ffmpeg's default CFR conversion (`-r 15`) then freezes the output
  46. clock after frame 1 and drops everything else (#1395). The profile
  47. must splice `-use_wallclock_as_timestamps 1` into the input args so
  48. ffmpeg rebuilds PTS from arrival time. Order matters — the flag and
  49. its value must be adjacent so they reach ffmpeg as a pair."""
  50. args = get_camera_profile("P2S").extra_ffmpeg_input_args
  51. assert "-use_wallclock_as_timestamps" in args
  52. idx = args.index("-use_wallclock_as_timestamps")
  53. assert args[idx + 1] == "1"
  54. def test_default_profile_has_no_timestamp_override(self):
  55. """X1/H2 send correct, advancing RTP timestamps — they must NOT get
  56. the wallclock override, which would needlessly re-stamp a healthy
  57. stream. Guards against the P2S fix leaking into the default."""
  58. assert DEFAULT_PROFILE.extra_ffmpeg_input_args == ()
  59. def test_p2s_internal_code_resolves_to_p2s_profile(self):
  60. """SSDP internal codes (e.g. `N7` for P2S) must resolve to the
  61. same profile as their display name. Otherwise printers freshly
  62. connected (before display-name lookup completes) would use the
  63. default profile and hit the same #1395 bug."""
  64. assert get_camera_profile("N7") is get_camera_profile("P2S")
  65. def test_lookup_is_case_insensitive(self):
  66. """Display-name capitalisation should not matter — callers may
  67. carry lowercase or mixed-case values straight from MQTT."""
  68. assert get_camera_profile("p2s") is get_camera_profile("P2S")
  69. assert get_camera_profile("P2s") is get_camera_profile("P2S")
  70. def test_known_rtsp_models_keep_default_unchanged(self):
  71. """X1, X1C, X1E, H2D, H2S, X2D — every other RTSP-capable model
  72. must use the default profile until proven otherwise. Anything
  73. else means we silently changed behaviour for a model the user
  74. hasn't reported a problem on."""
  75. for model in ("X1", "X1C", "X1E", "X2D", "H2C", "H2D", "H2D PRO", "H2S"):
  76. assert get_camera_profile(model) is DEFAULT_PROFILE, (
  77. f"{model} unexpectedly has a non-default profile — review "
  78. "whether the change is intentional before shipping."
  79. )
  80. class TestCameraProfileShape:
  81. def test_profile_is_frozen(self):
  82. """Profiles are immutable; mutating them at runtime would
  83. introduce action-at-a-distance for the camera generator."""
  84. with pytest.raises(FrozenInstanceError):
  85. DEFAULT_PROFILE.probesize = 999 # type: ignore[misc]
  86. def test_extra_ffmpeg_input_args_defaults_to_empty_tuple(self):
  87. """Profiles can declare extra `-flag value` pairs to splice into
  88. the ffmpeg input args without changing the dataclass shape.
  89. Default is empty so the historical command is unchanged."""
  90. p = CameraProfile()
  91. assert p.extra_ffmpeg_input_args == ()