test_support_bundle_rotated_logs.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. """The support bundle must span the rotated logs, not just the live file (#2555).
  2. ``bambuddy.log`` is capped at 5 MB by the RotatingFileHandler, and the bundle
  3. used to ship only that one file — the three rotated backups sat on disk unread.
  4. That cap is invisible on a single printer and brutal on a farm. The 19-printer
  5. fleet in #2555 emits ~100 lines/s of MQTT frame dumps with debug logging on,
  6. which fills 5 MB in under five minutes: we asked the reporter for debug logs to
  7. diagnose a *queue* problem and the bundle came back holding 4m49s of history,
  8. almost none of it about the queue. The byte budget for the bundle was 10 MB all
  9. along; it just never looked past the newest file.
  10. """
  11. from unittest.mock import patch
  12. from backend.app.api.routes.support import _get_log_content
  13. def _write_rotation(log_dir, live: str, backups: list[str]):
  14. """Lay out a RotatingFileHandler set: bambuddy.log plus .log.1 .. .log.N.
  15. ``backups[0]`` becomes ``.log.1``, which the handler defines as the *newest*
  16. backup — i.e. the one immediately preceding the live file.
  17. """
  18. (log_dir / "bambuddy.log").write_text(live, encoding="utf-8")
  19. for index, text in enumerate(backups, start=1):
  20. (log_dir / f"bambuddy.log.{index}").write_text(text, encoding="utf-8")
  21. def test_reads_rotated_backups_as_well_as_the_live_file(tmp_path):
  22. _write_rotation(tmp_path, "live line\n", ["newest backup\n", "middle backup\n", "oldest backup\n"])
  23. with (
  24. patch("backend.app.api.routes.support.settings.log_dir", tmp_path),
  25. patch("backend.app.api.routes.support.settings.log_backup_count", 3),
  26. ):
  27. content = _get_log_content().decode()
  28. for expected in ("oldest backup", "middle backup", "newest backup", "live line"):
  29. assert expected in content, f"{expected!r} missing — the bundle dropped rotated history"
  30. def test_output_is_chronological_oldest_first(tmp_path):
  31. """A log you have to read backwards is not a log. .log.3 is the oldest."""
  32. _write_rotation(tmp_path, "D\n", ["C\n", "B\n", "A\n"])
  33. with (
  34. patch("backend.app.api.routes.support.settings.log_dir", tmp_path),
  35. patch("backend.app.api.routes.support.settings.log_backup_count", 3),
  36. ):
  37. content = _get_log_content().decode()
  38. assert content.split() == ["A", "B", "C", "D"]
  39. def test_byte_budget_is_spent_on_the_newest_history(tmp_path):
  40. """When the rotation exceeds max_bytes, drop the OLD end, keep the recent.
  41. Truncating from the wrong end would hand us a bundle full of history that
  42. predates the problem being reported.
  43. """
  44. _write_rotation(tmp_path, "live\n", ["recent\n", "ancient\n"])
  45. with (
  46. patch("backend.app.api.routes.support.settings.log_dir", tmp_path),
  47. patch("backend.app.api.routes.support.settings.log_backup_count", 2),
  48. ):
  49. # Enough for "live\n" + "recent\n" but not for "ancient\n" as well.
  50. content = _get_log_content(max_bytes=12).decode()
  51. assert "live" in content
  52. assert "recent" in content
  53. assert "ancient" not in content
  54. def test_partial_line_at_the_truncation_point_is_discarded(tmp_path):
  55. """Seeking into the middle of a line must not emit a mangled fragment."""
  56. (tmp_path / "bambuddy.log").write_text("aaaaaaaaaa\nbbbbbbbbbb\n", encoding="utf-8")
  57. with (
  58. patch("backend.app.api.routes.support.settings.log_dir", tmp_path),
  59. patch("backend.app.api.routes.support.settings.log_backup_count", 3),
  60. ):
  61. content = _get_log_content(max_bytes=15).decode()
  62. assert content == "bbbbbbbbbb\n", "a half-line leaked through the seek"
  63. def test_missing_backups_are_skipped_not_fatal(tmp_path):
  64. """A fresh install has no .log.N yet; a gap must not abort the bundle."""
  65. (tmp_path / "bambuddy.log").write_text("live only\n", encoding="utf-8")
  66. (tmp_path / "bambuddy.log.2").write_text("older\n", encoding="utf-8") # .log.1 absent
  67. with (
  68. patch("backend.app.api.routes.support.settings.log_dir", tmp_path),
  69. patch("backend.app.api.routes.support.settings.log_backup_count", 3),
  70. ):
  71. content = _get_log_content().decode()
  72. assert content == "older\nlive only\n"
  73. def test_absent_log_file_still_reports_cleanly(tmp_path):
  74. with (
  75. patch("backend.app.api.routes.support.settings.log_dir", tmp_path),
  76. patch("backend.app.api.routes.support.settings.log_backup_count", 3),
  77. ):
  78. assert _get_log_content() == b"Log file not found"