CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
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
|