Files
ARR-2.0-0918/tests/test_ohip_archive_budget.py
T

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()