112 lines
6.2 KiB
Python
112 lines
6.2 KiB
Python
"""Acquisition stops before exceeding the same inventory bound used by audit."""
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from integrations.ohip import collect_arr_source as source, collect_arr_day as day, capture_day_job as jobs
|
|
from integrations.ohip.rate_info import RateInfoReader
|
|
from tests.test_ohip_arr_collection import DAY, HOTEL, FakeService
|
|
from tests.test_ohip_day_capture import DayService
|
|
|
|
|
|
class ArchiveBudgetTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.root = Path(temporary.name)
|
|
self.enterContext(patch.object(source, "MAX_ARCHIVE_BYTES", 16 * 1024))
|
|
self.enterContext(patch.object(source, "MAX_FINAL_METADATA_BYTES", 2 * 1024))
|
|
|
|
def test_budget_checked_before_file_creation_and_footer_space_reserved(self):
|
|
archive = source.Archive(self.root / "capture")
|
|
accepted = b"a" * (source.MAX_ARCHIVE_BYTES - source.MAX_FINAL_METADATA_BYTES)
|
|
archive.write("response.bin", accepted)
|
|
with self.assertRaisesRegex(source.CollectionError, "archive_byte_budget_exceeded"):
|
|
archive.write("overflow.bin", b"x")
|
|
self.assertFalse((archive.path / "overflow.bin").exists())
|
|
self.assertEqual((archive.path / "response.bin").read_bytes(), accepted)
|
|
archive.write("rate-assessments.json", b" " * source.MAX_FINAL_METADATA_BYTES)
|
|
self.assertEqual(archive.inventory_bytes, source.MAX_ARCHIVE_BYTES)
|
|
archive.write("result.json", {"status": "failed", "files": archive.files.copy()})
|
|
self.assertEqual(sum(f["bytes"] for f in archive.files if f["name"] != "result.json"), source.MAX_ARCHIVE_BYTES)
|
|
|
|
def test_manifest_and_footer_have_separate_finite_limits(self):
|
|
archive = source.Archive(self.root / "capture")
|
|
with patch.object(source, "MAX_MANIFEST_BYTES", 128):
|
|
for name, payload, error in [("result.json", b"x" * 129, "archive_manifest_too_large"),
|
|
("rate-assessments.json", b"x" * (source.MAX_FINAL_METADATA_BYTES + 1), "archive_footer_too_large")]:
|
|
with self.subTest(name=name), self.assertRaisesRegex(source.CollectionError, error):
|
|
archive.write(name, payload)
|
|
self.assertFalse((archive.path / name).exists())
|
|
self.assertEqual(archive.inventory_bytes, 0)
|
|
|
|
def run_capture(self, v2):
|
|
archive = source.Archive(self.root / "capture")
|
|
service = DayService() if v2 else FakeService()
|
|
def large_response(method, path, body):
|
|
status, headers, raw = service(method, path, body)
|
|
return status, headers, raw + b" " * source.MAX_ARCHIVE_BYTES
|
|
reader = source.Reader(archive, HOTEL, large_response, sleep=lambda _: self.fail("must not retry"))
|
|
if v2:
|
|
result = day.collect(day.Options(DAY, DAY, DAY, HOTEL), archive, reader,
|
|
RateInfoReader(archive, HOTEL, large_response, sleep=lambda _: self.fail("must not retry")))
|
|
else:
|
|
result = source.collect(source.Options(DAY, HOTEL), archive, reader)
|
|
saved = json.loads((archive.path / "result.json").read_bytes())
|
|
self.assertEqual(result["error"], "archive_byte_budget_exceeded")
|
|
self.assertEqual(saved["error"], result["error"])
|
|
self.assertFalse(saved["candidate_capture_complete"])
|
|
self.assertFalse(saved["finance_ready"])
|
|
self.assertEqual(len(service.calls), 1)
|
|
self.assertFalse(list(archive.path.glob("*.response.bin")))
|
|
self.assertLessEqual(sum(f["bytes"] for f in saved["files"]), source.MAX_ARCHIVE_BYTES)
|
|
self.assertEqual(result["manifest_sha256"], hashlib.sha256((archive.path / "result.json").read_bytes()).hexdigest())
|
|
return archive
|
|
|
|
def test_v1_overflow_stops_after_first_response_and_keeps_failure_manifest(self):
|
|
self.run_capture(False)
|
|
|
|
def test_v2_overflow_preserves_assessment_footer_and_failure_manifest(self):
|
|
archive = self.run_capture(True)
|
|
self.assertEqual(json.loads((archive.path / "rate-assessments.json").read_bytes()), {"records": []})
|
|
|
|
def test_oversize_assessment_footer_still_writes_failed_manifest(self):
|
|
archive = source.Archive(self.root / "capture")
|
|
service = DayService()
|
|
with patch.object(source, "MAX_ARCHIVE_BYTES", 64 * 1024), \
|
|
patch.object(source, "MAX_FINAL_METADATA_BYTES", 32):
|
|
result = day.collect(day.Options(DAY, DAY, DAY, HOTEL), archive,
|
|
source.Reader(archive, HOTEL, service, sleep=lambda _: None),
|
|
RateInfoReader(archive, HOTEL, service, sleep=lambda _: None))
|
|
self.assertFalse(result["candidate_capture_complete"])
|
|
self.assertFalse(result["all_rates_valid"])
|
|
self.assertEqual(result["error"], "archive_footer_too_large")
|
|
self.assertEqual(result["rate_responses"], 3)
|
|
self.assertFalse((archive.path / "rate-assessments.json").exists())
|
|
saved = json.loads((archive.path / "result.json").read_bytes())
|
|
self.assertEqual((saved["status"], saved["error"]), ("failed", "archive_footer_too_large"))
|
|
self.assertNotIn("rate-assessments.json", [item["name"] for item in saved["files"]])
|
|
|
|
def test_day_job_records_bounded_failure_without_automatic_new_attempt(self):
|
|
service = DayService()
|
|
def transport(method, path, body):
|
|
status, headers, raw = service(method, path, body)
|
|
return status, headers, raw + b" " * source.MAX_ARCHIVE_BYTES
|
|
def factory(archive, hotel):
|
|
return (source.Reader(archive, hotel, transport, sleep=lambda _: self.fail("retry")),
|
|
RateInfoReader(archive, hotel, transport, sleep=lambda _: self.fail("retry")))
|
|
result = jobs.run_batch(self.root / "jobs", "budget", day.Options(DAY, DAY, DAY, HOTEL), factory)
|
|
self.assertEqual((result["status"], result["error"]), ("capture_failed", "archive_byte_budget_exceeded"))
|
|
self.assertEqual(len(service.calls), 1)
|
|
state = json.loads((self.root / "jobs/budget/state.json").read_bytes())
|
|
self.assertEqual(len(state["attempts"]), 1)
|
|
self.assertEqual(state["attempts"][0]["status"], "failed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|