import json from uuid import UUID, uuid4 import pytest from aare.queue.models import ( QueueInstructionBase, QueueItemKind, RasterInstruction, RotationInstruction, ScreeningInstruction, UnknownInstruction, dump_queue_item, parse_queue_item, ) KNOWN_INSTRUCTIONS = [ (RasterInstruction, QueueItemKind.RASTER), (ScreeningInstruction, QueueItemKind.SCREENING), (RotationInstruction, QueueItemKind.ROTATION), ] # A plausible instruction from a producer newer than this reader: a kind we have # never heard of, plus fields we know nothing about. FUTURE_INSTRUCTION = { "id": "11111111-2222-3333-4444-555555555555", "kind": "xrf_map", "sample_id": 9, "grid_steps": [4, 4], "dwell_ms": 50, } @pytest.mark.parametrize("cls,kind", KNOWN_INSTRUCTIONS, ids=lambda v: getattr(v, "value", None)) def test_known_instruction_defaults_its_own_kind(cls, kind): assert cls(sample_id=1).kind == kind def test_ids_are_generated_and_distinct(): ids = {RasterInstruction(sample_id=1).id for _ in range(5)} assert len(ids) == 5 @pytest.mark.parametrize("cls,kind", KNOWN_INSTRUCTIONS, ids=lambda v: getattr(v, "value", None)) def test_known_instruction_round_trips_to_its_own_class(cls, kind): original = cls(sample_id=3) restored = parse_queue_item(dump_queue_item(original)) assert type(restored) is cls assert restored.sample_id == 3 @pytest.mark.parametrize("cls,kind", KNOWN_INSTRUCTIONS, ids=lambda v: getattr(v, "value", None)) def test_round_trip_preserves_the_id(cls, kind): """Identity ops target the id, so a read must never regenerate it.""" original = cls(sample_id=3) assert parse_queue_item(dump_queue_item(original)).id == original.id def test_unrecognised_kind_becomes_an_unknown_instruction(): item = parse_queue_item(json.dumps(FUTURE_INSTRUCTION)) assert isinstance(item, UnknownInstruction) assert item.kind == "xrf_map" assert item.id == UUID(FUTURE_INSTRUCTION["id"]) def test_unknown_instruction_survives_a_read_write_cycle_verbatim(): """Reordering rewrites every entry. A reader that cannot interpret an item must not drop the fields it did not understand.""" item = parse_queue_item(json.dumps(FUTURE_INSTRUCTION)) assert json.loads(dump_queue_item(item)) == FUTURE_INSTRUCTION def test_unknown_instruction_is_a_queue_instruction(): """It shares the base, so consumers reach its common fields the same way they would for a kind they understand.""" item = parse_queue_item(json.dumps(FUTURE_INSTRUCTION)) assert isinstance(item, QueueInstructionBase) assert item.id == UUID(FUTURE_INSTRUCTION["id"]) assert item.sample_id == FUTURE_INSTRUCTION["sample_id"] def test_unknown_fields_are_kept_as_extras(): """Held as pydantic extras rather than a raw blob, which is what lets them serialize back out without a custom serializer.""" item = parse_queue_item(json.dumps(FUTURE_INSTRUCTION)) assert item.model_extra == { "grid_steps": FUTURE_INSTRUCTION["grid_steps"], "dwell_ms": FUTURE_INSTRUCTION["dwell_ms"], } def test_unknown_instruction_does_not_regenerate_a_missing_id(): """The base defaults id, but a stored entry always has one already -- minting a fresh one on read would silently break remove/reorder.""" with pytest.raises(ValueError): parse_queue_item(json.dumps({"kind": "xrf_map", "sample_id": 9})) @pytest.mark.parametrize( "payload", [ {"kind": "xrf_map", "sample_id": 9}, {"id": "not-a-uuid", "kind": "xrf_map", "sample_id": 9}, {"id": str(uuid4()), "sample_id": 9}, {"id": str(uuid4()), "kind": "xrf_map"}, ], ids=["no-id", "non-uuid-id", "no-kind", "no-sample-id"], ) def test_entries_missing_a_base_field_are_rejected(payload): """Every instruction inherits id and sample_id and declares a kind, so an entry missing any of them is corrupt rather than merely unrecognised -- it must not parse as Unknown.""" with pytest.raises(ValueError): parse_queue_item(json.dumps(payload)) def test_malformed_json_is_rejected(): with pytest.raises(ValueError): parse_queue_item("NOT JSON AT ALL") def test_non_object_json_is_rejected(): with pytest.raises(TypeError): parse_queue_item("[1, 2, 3]") def test_a_known_kind_with_a_bad_payload_is_not_smuggled_in_as_unknown(): """Falling back on any ValidationError would turn a malformed raster item into an opaque Unknown that silently round-trips broken data.""" with pytest.raises(ValueError): parse_queue_item(json.dumps({"id": str(uuid4()), "kind": "raster"})) # no sample_id