158 lines
5.9 KiB
Python
158 lines
5.9 KiB
Python
"""Unit tests for the ImageHub segmentation-linking domain service.
|
|
|
|
The service was built with INJECTED infrastructure (put_blob / sniff_meta / safe_name)
|
|
precisely so the domain rules can be exercised with fakes — no Postgres, no MinIO.
|
|
Covers: parent validation (bad uuid / not-found / not-an-image), the mask path
|
|
namespacing, organ-label fallback, and the empty-payload guard.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
import uuid
|
|
|
|
from src.imagehub_segmentation import MaskUpload, SegmentationError, SegmentationService
|
|
from src.initiative_db.models import ImagehubDataset, ImagehubDatasetFile
|
|
|
|
|
|
class _FakeResult:
|
|
def __init__(self, value):
|
|
self._value = value
|
|
|
|
def scalar_one_or_none(self):
|
|
return self._value
|
|
|
|
|
|
class _FakeSession:
|
|
"""Minimal AsyncSession stand-in. The 1st execute() resolves the parent file;
|
|
every later execute() resolves the 'existing mask at this path' lookup (None =
|
|
create new). Records added rows."""
|
|
|
|
def __init__(self, parent=None, existing=None):
|
|
self._parent = parent
|
|
self._existing = existing
|
|
self.added: list = []
|
|
self.exec_calls = 0
|
|
self.flushes = 0
|
|
|
|
async def execute(self, _stmt):
|
|
self.exec_calls += 1
|
|
return _FakeResult(self._parent if self.exec_calls == 1 else self._existing)
|
|
|
|
async def get(self, _model, _key):
|
|
return None # blob absent → service will add it
|
|
|
|
def add(self, obj):
|
|
self.added.append(obj)
|
|
|
|
async def flush(self):
|
|
self.flushes += 1
|
|
|
|
|
|
def _safe_name(name):
|
|
base = (name or "").strip().replace("\\", "/").rsplit("/", 1)[-1]
|
|
return base or "file"
|
|
|
|
|
|
async def _put_blob(data, media_type):
|
|
return {
|
|
"sha256": "deadbeef" + str(len(data)),
|
|
"size": len(data),
|
|
"bucket": "imagehub-blobs",
|
|
"key": "blobs/de/ad/deadbeef",
|
|
"media_type": media_type or "application/octet-stream",
|
|
"deduped": False,
|
|
}
|
|
|
|
|
|
def _sniff(_filename, _data, _media):
|
|
return {"format": "nifti", "shape": [4, 4, 4]}
|
|
|
|
|
|
def _service(session):
|
|
return SegmentationService(session, put_blob=_put_blob, sniff_meta=_sniff, safe_name=_safe_name)
|
|
|
|
|
|
def _image_parent(dataset_id, logical_path="ct.nii.gz"):
|
|
return ImagehubDatasetFile(
|
|
id=uuid.uuid4(), dataset_id=dataset_id, logical_path=logical_path, file_kind="image"
|
|
)
|
|
|
|
|
|
def _mask(filename="liver.nii.gz", organ="Gan", data=b"xyz"):
|
|
return MaskUpload(filename=filename, data=data, media_type="application/gzip", organ_label=organ)
|
|
|
|
|
|
class TestMaskLogicalPath(unittest.TestCase):
|
|
def test_namespaces_under_parent_stem(self):
|
|
svc = _service(_FakeSession())
|
|
self.assertEqual(svc._mask_logical_path("ct.nii.gz", "liver.nii.gz"), "ct.seg/liver.nii.gz")
|
|
self.assertEqual(svc._mask_logical_path("scan.nii", "a.nii.gz"), "scan.seg/a.nii.gz")
|
|
self.assertEqual(svc._mask_logical_path("study.dcm", "k.nii.gz"), "study.seg/k.nii.gz")
|
|
|
|
def test_mask_path_cannot_collide_with_a_real_image_path(self):
|
|
# A real file's logical_path never contains '/', a mask path always does.
|
|
svc = _service(_FakeSession())
|
|
self.assertIn("/", svc._mask_logical_path("ct.nii.gz", "ct.nii.gz"))
|
|
|
|
|
|
class TestLinkMasks(unittest.IsolatedAsyncioTestCase):
|
|
def setUp(self):
|
|
self.ds = ImagehubDataset(id=uuid.uuid4(), owner_user_id=uuid.uuid4())
|
|
self.uid = uuid.uuid4()
|
|
|
|
async def test_happy_path_links_mask_to_image(self):
|
|
parent = _image_parent(self.ds.id)
|
|
sess = _FakeSession(parent=parent, existing=None)
|
|
rows = await _service(sess).link_masks(self.ds, str(parent.id), [_mask()], self.uid)
|
|
self.assertEqual(len(rows), 1)
|
|
r = rows[0]
|
|
self.assertEqual(r.file_kind, "segmentation")
|
|
self.assertEqual(r.parent_file_id, parent.id)
|
|
self.assertEqual(r.organ_label, "Gan")
|
|
self.assertEqual(r.logical_path, "ct.seg/liver.nii.gz")
|
|
self.assertEqual(r.dataset_id, self.ds.id)
|
|
|
|
async def test_organ_label_falls_back_to_filename(self):
|
|
parent = _image_parent(self.ds.id)
|
|
sess = _FakeSession(parent=parent)
|
|
rows = await _service(sess).link_masks(self.ds, str(parent.id), [_mask(organ=" ")], self.uid)
|
|
self.assertEqual(rows[0].organ_label, "liver.nii.gz")
|
|
|
|
async def test_bad_parent_uuid_is_404(self):
|
|
with self.assertRaises(SegmentationError) as ctx:
|
|
await _service(_FakeSession()).link_masks(self.ds, "not-a-uuid", [_mask()], self.uid)
|
|
self.assertEqual(ctx.exception.status, 404)
|
|
|
|
async def test_parent_not_found_is_404(self):
|
|
sess = _FakeSession(parent=None)
|
|
with self.assertRaises(SegmentationError) as ctx:
|
|
await _service(sess).link_masks(self.ds, str(uuid.uuid4()), [_mask()], self.uid)
|
|
self.assertEqual(ctx.exception.status, 404)
|
|
|
|
async def test_attaching_to_a_mask_is_422(self):
|
|
mask_parent = ImagehubDatasetFile(
|
|
id=uuid.uuid4(), dataset_id=self.ds.id, logical_path="ct.seg/x.nii.gz", file_kind="segmentation"
|
|
)
|
|
sess = _FakeSession(parent=mask_parent)
|
|
with self.assertRaises(SegmentationError) as ctx:
|
|
await _service(sess).link_masks(self.ds, str(mask_parent.id), [_mask()], self.uid)
|
|
self.assertEqual(ctx.exception.status, 422)
|
|
|
|
async def test_empty_payload_is_422(self):
|
|
parent = _image_parent(self.ds.id)
|
|
sess = _FakeSession(parent=parent)
|
|
with self.assertRaises(SegmentationError) as ctx:
|
|
await _service(sess).link_masks(self.ds, str(parent.id), [], self.uid)
|
|
self.assertEqual(ctx.exception.status, 422)
|
|
|
|
async def test_all_empty_byte_masks_is_422(self):
|
|
parent = _image_parent(self.ds.id)
|
|
sess = _FakeSession(parent=parent)
|
|
with self.assertRaises(SegmentationError) as ctx:
|
|
await _service(sess).link_masks(self.ds, str(parent.id), [_mask(data=b"")], self.uid)
|
|
self.assertEqual(ctx.exception.status, 422)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|