144 lines
5.8 KiB
Python
144 lines
5.8 KiB
Python
import json
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
import pytest
|
|
from jsonschema import Draft202012Validator, FormatChecker
|
|
from pydantic import ValidationError
|
|
|
|
from kf_oj.contracts.judge_state import (
|
|
EvaluationState,
|
|
require_current_lease,
|
|
require_transition,
|
|
)
|
|
from kf_oj.contracts.judging import (
|
|
MAX_WIRE_BYTES,
|
|
WIRE_MODELS,
|
|
AdmissionPolicy,
|
|
JobManifest,
|
|
JudgementReport,
|
|
LeaseGrant,
|
|
decode_wire,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
FIXTURES = ROOT / "tests/fixtures/contracts"
|
|
PATHS = sorted(p for name in WIRE_MODELS for p in FIXTURES.glob(f"{name}.*.json"))
|
|
|
|
|
|
@pytest.mark.parametrize("path", PATHS, ids=lambda p: p.stem)
|
|
def test_shared_judging_examples(path: Path) -> None:
|
|
name = path.name.split(".")[0]
|
|
model = WIRE_MODELS[name]
|
|
payload = json.loads(path.read_text())
|
|
schema = json.loads((ROOT / f"contracts/schemas/{name}.v1.schema.json").read_text())
|
|
validator = Draft202012Validator(schema, format_checker=FormatChecker())
|
|
if ".invalid-" in path.name:
|
|
with pytest.raises(ValueError):
|
|
decode_wire(model, path.read_bytes())
|
|
if ".invalid-semantic-" in path.name:
|
|
assert validator.is_valid(payload)
|
|
else:
|
|
assert not validator.is_valid(payload)
|
|
else:
|
|
decoded = decode_wire(model, path.read_bytes())
|
|
validator.validate(payload)
|
|
if name == "judge-admission" and "defaults" in path.name:
|
|
assert decoded.max_concurrent_jobs == decoded.prefetch == 1
|
|
assert decoded.max_retries == 2
|
|
else:
|
|
assert decoded.model_dump(mode="json") == payload
|
|
|
|
|
|
def fixture(name: str, model):
|
|
return decode_wire(model, (FIXTURES / f"{name}.valid.json").read_bytes())
|
|
|
|
|
|
def test_every_state_transition_matches_shared_table() -> None:
|
|
transitions = json.loads((FIXTURES / "judge-state.transitions.json").read_text())
|
|
assert set(transitions) == {s.value for s in EvaluationState}
|
|
for current in EvaluationState:
|
|
for target in EvaluationState:
|
|
allowed = target.value in transitions[current.value]
|
|
assert current.allows(target) == allowed
|
|
if allowed:
|
|
require_transition(current, target)
|
|
else:
|
|
with pytest.raises(ValueError):
|
|
require_transition(current, target)
|
|
|
|
|
|
def test_lease_fencing_identity_and_expiry_boundary() -> None:
|
|
grant = fixture("judge-lease", LeaseGrant)
|
|
binding = grant.binding
|
|
require_current_lease(binding, binding, grant.expires_at_unix_ms, grant.expires_at_unix_ms - 1)
|
|
for now in (grant.expires_at_unix_ms, grant.expires_at_unix_ms + 1, -1):
|
|
with pytest.raises(ValueError):
|
|
require_current_lease(binding, binding, grant.expires_at_unix_ms, now)
|
|
for field, value in (
|
|
("fencing_version", 2),
|
|
("attempt_id", UUID("aaaaaaaa-aaaa-4aaa-8aaa-000000000009")),
|
|
("worker_id", UUID("aaaaaaaa-aaaa-4aaa-8aaa-000000000009")),
|
|
("evaluation_id", UUID("aaaaaaaa-aaaa-4aaa-8aaa-000000000009")),
|
|
):
|
|
supplied = binding.model_copy(update={field: value})
|
|
with pytest.raises(ValueError):
|
|
require_current_lease(binding, supplied, grant.expires_at_unix_ms, 1)
|
|
assert grant.lease_token not in repr(grant)
|
|
|
|
|
|
def test_node_admission_never_accepts_job_or_retry_above_trusted_policy() -> None:
|
|
manifest = fixture("judge-manifest", JobManifest)
|
|
policy = fixture("judge-admission", AdmissionPolicy)
|
|
for attempt in (1, 2, 3):
|
|
policy.admit(manifest, attempt)
|
|
for attempt in (0, 4, True):
|
|
with pytest.raises(ValueError):
|
|
policy.admit(manifest, attempt)
|
|
for field, value in (("max_package_bytes", 1), ("max_job_wall_time_us", 1000)):
|
|
with pytest.raises(ValueError):
|
|
policy.model_copy(update={field: value}).admit(manifest, 1)
|
|
lower = policy.run_ceiling.model_copy(update={"memory_bytes": 32 * 1024 * 1024})
|
|
with pytest.raises(ValueError):
|
|
policy.model_copy(update={"run_ceiling": lower}).admit(manifest, 1)
|
|
other = manifest.model_copy(
|
|
update={"toolchain": manifest.toolchain.model_copy(update={"image_revision": 2})}
|
|
)
|
|
with pytest.raises(ValueError):
|
|
policy.admit(other, 1)
|
|
|
|
|
|
def test_result_requires_authorized_manifest_and_complete_case_order() -> None:
|
|
manifest = fixture("judge-manifest", JobManifest)
|
|
result = fixture("judge-result", JudgementReport)
|
|
result.validate_for_manifest(manifest, result.manifest_digest)
|
|
with pytest.raises(ValueError):
|
|
result.validate_for_manifest(manifest, "f" * 64)
|
|
short = result.model_dump(mode="json")
|
|
short.update(cases=short["cases"][:1], run_cpu_time_us=100, run_memory_peak_bytes=256 * 1024)
|
|
valid_partial = JudgementReport.model_validate(short)
|
|
with pytest.raises(ValueError):
|
|
valid_partial.validate_for_manifest(manifest, result.manifest_digest)
|
|
wrong_order = result.model_dump(mode="json")
|
|
wrong_order["cases"].reverse()
|
|
with pytest.raises(ValueError):
|
|
JudgementReport.model_validate(wrong_order).validate_for_manifest(
|
|
manifest, result.manifest_digest
|
|
)
|
|
|
|
|
|
def test_wire_decoding_bounds_ambiguity_and_private_error_output() -> None:
|
|
payload = (FIXTURES / "judge-lease.valid.json").read_bytes()
|
|
duplicate = payload.replace(b'"schema_version": 1', b'"schema_version": 1, "schema_version": 1')
|
|
for invalid in (duplicate, payload.decode().encode("utf-16"), b" " * (MAX_WIRE_BYTES + 1)):
|
|
with pytest.raises(ValueError):
|
|
decode_wire(LeaseGrant, invalid)
|
|
secret = "do-not-echo-private-token"
|
|
with pytest.raises(ValueError) as error:
|
|
decode_wire(
|
|
LeaseGrant, payload.replace(b'"0' + b"0" * 63 + b'"', json.dumps(secret).encode())
|
|
)
|
|
assert secret not in str(error.value)
|
|
with pytest.raises(ValidationError):
|
|
LeaseGrant.model_validate({})
|