openflexure-microscope-server/tests/unit_tests/test_stack_examples.py

102 lines
3.1 KiB
Python

"""Unit tests for validating stack classification logic in AutofocusThing.
This module tests whether the `check_stack_result` method correctly classifies
z-stack sharpness profiles into "success", "continue", or "restart" categories
based on predefined test cases.
Sharpness profiles are loaded from a JSON file and converted into mock capture
objects to simulate real camera captures.
"""
import json
import os
import pytest
from labthings_fastapi.testing import create_thing_without_server
from openflexure_microscope_server.things.autofocus import AutofocusThing
THIS_DIR = os.path.dirname(__file__)
DATA_PATH = os.path.join(THIS_DIR, "data", "sharpness_test_cases.json")
class MockCapture:
"""Simple mock object representing a captured image.
This class mimics the minimal interface required by
`check_stack_result` by adding the `sharpness` and `buffer_id`
attributes.
:param sharpness: The sharpness value associated with the image.
:param buffer_id: A unique identifier for the image buffer.
"""
def __init__(self, sharpness, buffer_id):
"""Give each capture a sharpness and buffer_id."""
self.sharpness = sharpness
self.buffer_id = buffer_id
def make_captures(sharpness_list):
"""Convert a list of sharpness values into mock capture objects.
:param sharpness_list: A list of numeric sharpness values.
:returns: A list of MockCapture instances with sequential buffer IDs.
"""
return [MockCapture(s, i) for i, s in enumerate(sharpness_list)]
def load_cases():
"""Load the sharpness data from the json.
Includes the sharpnesses to test, manually written labels
on the required result, and whether the test is allowed to
fail. This ensures future tests will flag any regression on
tests while allowing improvements.
"""
with open(DATA_PATH) as f:
data = json.load(f)
params = []
for i, case in enumerate(data):
expected = case["label"]
# Allow multiple acceptable labels
if not isinstance(expected, list):
expected = [expected]
marks = []
if case.get("allow_failure", False):
marks.append(pytest.mark.xfail(reason="Known failing case"))
params.append(
pytest.param(
case["sharpnesses"],
expected,
marks=marks,
id=f"case_{i}",
)
)
return params
@pytest.mark.parametrize(("sharpnesses", "expected"), load_cases())
def test_stack_labelling(sharpnesses, expected):
"""Test stack classification accuracy against labelled sharpness cases.
This test loads the test from load_cases and evaluates
the classification returned by `check_stack_result`.
The test ensures that only cases marked with "allow_failure: true" can fail.
Expected labels may be a single value or a list of acceptable values.
"""
autofocus_thing = create_thing_without_server(AutofocusThing, mock_all_slots=True)
captures = make_captures(sharpnesses)
result, _ = autofocus_thing.check_stack_result(captures, check_turning_points=False)
assert result in expected