from __future__ import annotations
|
|
from pathlib import Path
|
from datetime import datetime, timezone
|
import tempfile
|
import unittest
|
|
from hibor_fast_collection.budget import Budget, BudgetExpired
|
from hibor_fast_collection.models import (
|
ContractError, ErrorCode, EvidenceState, PackageState, TaskSpec, TerminalReceipt,
|
)
|
from helpers import task_spec
|
|
|
class FakeClock:
|
def __init__(self): self.value = 0.0
|
def __call__(self): return self.value
|
|
|
class ModelsBudgetTests(unittest.TestCase):
|
def test_task_spec_exact_and_batch_budget(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
spec = TaskSpec.from_mapping(task_spec(Path(tmp), quantity=3))
|
self.assertEqual(spec.total_budget_ms, 1_080_000)
|
bad = task_spec(Path(tmp))
|
bad["package_name"] = "other"
|
with self.assertRaises(ContractError): TaskSpec.from_mapping(bad)
|
|
def test_key_order_rejected(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
value = task_spec(Path(tmp))
|
first = value.pop("schema_version")
|
value["schema_version"] = first
|
with self.assertRaises(ContractError): TaskSpec.from_mapping(value)
|
|
def test_budget_work_and_close_deadline(self):
|
clock = FakeClock()
|
budget = Budget(10_000, 2_000, clock=clock)
|
self.assertEqual(budget.require("x", "y", 9_000), 8_000)
|
clock.value = 8.001
|
with self.assertRaises(BudgetExpired): budget.require("x", "y", 1_000)
|
self.assertGreater(budget.require("close", "z", 1_000, close=True), 0)
|
clock.value = 10.1
|
with self.assertRaises(BudgetExpired): budget.require("close", "z", 1_000, close=True)
|
|
def test_observed_deadline_and_batch_increment(self):
|
fresh_clock = FakeClock()
|
fresh = Budget(840_000, 30_000, batch_increment_ms=240_000,
|
clock=fresh_clock)
|
fresh.begin_item(1)
|
self.assertEqual((fresh.remaining_ms(), fresh.remaining_ms(close=True),
|
fresh.remaining_ms(batch_close=True)),
|
(570_000, 600_000, 840_000))
|
|
clock = FakeClock()
|
wall = lambda: datetime(2026, 7, 29, 0, 5, tzinfo=timezone.utc)
|
budget = Budget(840_000, 30_000, observed_at_utc="2026-07-29T00:00:00Z",
|
batch_increment_ms=240_000, clock=clock, utc_now=wall)
|
budget.begin_item(1)
|
self.assertEqual(budget.remaining_ms(), 270_000)
|
self.assertEqual(budget.remaining_ms(close=True), 300_000)
|
self.assertEqual(budget.remaining_ms(batch_close=True), 540_000)
|
with self.assertRaises(ValueError):
|
budget.begin_item(2)
|
clock.value = 299.0
|
self.assertTrue(budget.complete_item(1))
|
budget.begin_item(2)
|
self.assertEqual(budget.remaining_ms(), 210_000)
|
self.assertEqual(budget.remaining_ms(close=True), 240_000)
|
clock.value = 509.1
|
with self.assertRaises(BudgetExpired): budget.checkpoint("item_2", "terminal")
|
self.assertGreater(budget.remaining_ms(close=True), 0)
|
clock.value = 539.1
|
self.assertFalse(budget.complete_item(2))
|
|
def test_terminal_receipt_cross_fields(self):
|
target = Path("terminal.json")
|
valid = TerminalReceipt(target, target, True, True, EvidenceState.V, True, False, True,
|
10, "a"*64, None, PackageState.P09_TIMING)
|
valid.validate()
|
file_u = TerminalReceipt(target, None, True, None, EvidenceState.U, True, False, True,
|
None, None, ErrorCode.RECOVERY_UNKNOWN, PackageState.P09_TIMING)
|
file_u.validate()
|
live_u = TerminalReceipt(target, None, True, None, EvidenceState.U, None, True, True,
|
None, None, ErrorCode.PROCESS_LIVENESS_UNKNOWN, PackageState.P09_TIMING)
|
live_u.validate()
|
with self.assertRaises(ContractError):
|
TerminalReceipt(target, None, False, False, EvidenceState.U, False, False, True,
|
None, None, ErrorCode.RECOVERY_UNKNOWN, PackageState.P09_TIMING).validate()
|
|
|
if __name__ == "__main__": unittest.main()
|