test_report_release.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """The Report & Release pipeline — the highest-risk behaviour.
  2. Asserts on exact PPS call kwargs, not just counts: the localguid-stability rule (release
  3. must NOT pass deletedfolder) and the lazy guid-refind are both invisible to a call-count
  4. check but critical to correctness.
  5. """
  6. import pytest
  7. from conftest import FakePPS
  8. from pipeline import (
  9. ALLOWED_STEPS,
  10. describe_pipeline,
  11. pipeline_destination,
  12. run_pipeline,
  13. )
  14. from pps_client import PPSError
  15. def _cfg(steps, move_target="Rep", deleted="Deleted", step_delay=0):
  16. return {
  17. "deleted_folder": deleted,
  18. "list_query": "from=*",
  19. "default_days_back": 7,
  20. "report_release": {
  21. "steps": steps,
  22. "move_target": move_target,
  23. "step_delay_seconds": step_delay,
  24. },
  25. }
  26. def _chunk():
  27. return [
  28. {"localguid": "6:6:1", "guid": "g1"},
  29. {"localguid": "6:6:2", "guid": "g2"},
  30. ]
  31. def test_release_only_has_no_deletedfolder():
  32. pps = FakePPS()
  33. run_pipeline(pps, _cfg(["release"]), "Quarantine", _chunk())
  34. acts = pps.acts()
  35. assert len(acts) == 1
  36. assert acts[0]["action"] == "release"
  37. assert acts[0]["deletedfolder"] is None # THE localguid-stability rule
  38. assert acts[0]["folder"] == "Quarantine"
  39. def test_release_move_is_two_calls_zero_searches():
  40. pps = FakePPS()
  41. run_pipeline(pps, _cfg(["release", "move"]), "Quarantine", _chunk())
  42. assert [a["action"] for a in pps.acts()] == ["release", "move"]
  43. assert pps.acts()[0]["deletedfolder"] is None
  44. assert pps.acts()[1]["targetfolder"] == "Rep"
  45. assert pps.acts()[1]["folder"] == "Quarantine"
  46. assert pps.searches() == [] # lazy refind: no search needed
  47. def test_move_fallback_refinds_in_deleted_folder():
  48. class MovingPPS(FakePPS):
  49. def act(self, action, folder, localguids, *, targetfolder=None, deletedfolder=None, scan=False):
  50. if action == "move" and folder == "Quarantine":
  51. self.calls.append({"m": "act", "action": action, "folder": folder,
  52. "localguids": list(localguids), "targetfolder": targetfolder,
  53. "deletedfolder": deletedfolder, "scan": scan})
  54. raise PPSError("release relocated it")
  55. return super().act(action, folder, localguids, targetfolder=targetfolder,
  56. deletedfolder=deletedfolder, scan=scan)
  57. pps = MovingPPS(records=[{"guid": "g1", "localguid": "D:1"}, {"guid": "g2", "localguid": "D:2"}])
  58. run_pipeline(pps, _cfg(["release", "move"]), "Quarantine", _chunk())
  59. # release, failed move on Quarantine, search Deleted, move from Deleted
  60. assert [s["folder"] for s in pps.searches()] == ["Deleted"]
  61. moves = [a for a in pps.acts() if a["action"] == "move"]
  62. assert moves[-1]["folder"] == "Deleted"
  63. assert sorted(moves[-1]["localguids"]) == ["D:1", "D:2"]
  64. def test_release_move_delete_refinds_between_move_and_delete():
  65. pps = FakePPS(records=[{"guid": "g1", "localguid": "R:1"}, {"guid": "g2", "localguid": "R:2"}])
  66. run_pipeline(pps, _cfg(["release", "move", "delete"]), "Quarantine", _chunk())
  67. actions = [a["action"] for a in pps.acts()]
  68. assert actions == ["release", "move", "delete"]
  69. # delete happens in the move_target after a refind there
  70. assert len(pps.searches()) == 1 and pps.searches()[0]["folder"] == "Rep"
  71. delete = [a for a in pps.acts() if a["action"] == "delete"][0]
  72. assert delete["folder"] == "Rep"
  73. assert delete["deletedfolder"] == "Deleted"
  74. def test_refind_no_guids_raises():
  75. pps = FakePPS()
  76. chunk = [{"localguid": "6:6:1"}] # no guid
  77. with pytest.raises(PPSError):
  78. # force a refind by making move fail
  79. class P(FakePPS):
  80. def act(self, *a, **k):
  81. if a[0] == "move":
  82. raise PPSError("relocated")
  83. return super().act(*a, **k)
  84. run_pipeline(P(), _cfg(["release", "move"]), "Quarantine", chunk)
  85. def test_pipeline_destination():
  86. assert pipeline_destination(_cfg(["release"])) is None
  87. assert pipeline_destination(_cfg(["release", "move"])) == "Rep"
  88. assert pipeline_destination(_cfg(["release", "delete"])) == "Deleted"
  89. assert pipeline_destination(_cfg(["release", "move", "delete"])) == "Deleted"
  90. def test_describe_pipeline():
  91. text = describe_pipeline(_cfg(["release", "move", "delete"]))
  92. assert "released in place" in text and "Rep" in text and "Deleted" in text
  93. assert "→" in text
  94. def test_allowed_steps_is_canonical_order():
  95. assert ALLOWED_STEPS == ("release", "move", "delete")
  96. def test_step_delay_waits_between_steps():
  97. pps = FakePPS()
  98. waits = []
  99. run_pipeline(pps, _cfg(["release", "move"], step_delay=60), "Quarantine", _chunk(),
  100. sleep=waits.append)
  101. # One wait, of the configured length, between the two steps.
  102. assert waits == [60]
  103. def test_no_delay_before_first_or_when_zero():
  104. pps = FakePPS()
  105. waits = []
  106. run_pipeline(pps, _cfg(["release"], step_delay=60), "Quarantine", _chunk(), sleep=waits.append)
  107. assert waits == [] # single step -> no wait
  108. waits.clear()
  109. run_pipeline(pps, _cfg(["release", "move"], step_delay=0), "Quarantine", _chunk(),
  110. sleep=waits.append)
  111. assert waits == [] # delay 0 -> disabled
  112. def test_delay_scales_with_step_count():
  113. pps = FakePPS(records=[{"guid": "g1", "localguid": "R:1"}, {"guid": "g2", "localguid": "R:2"}])
  114. waits = []
  115. run_pipeline(pps, _cfg(["release", "move", "delete"], step_delay=30), "Quarantine",
  116. _chunk(), sleep=waits.append)
  117. assert waits == [30, 30] # before move, before delete