Plesty Documentation

Testing Experiments Without Hardware

Use unittest.mock to patch build_client and test measurement logic without physical instruments.

Experiment tests do not need Gate d1 (that applies to device modules only). Instead, mock the device clients to test measurement logic without connecting to any instruments.

Patching build_client

The standard approach is to patch build_client at the point where it is called:

# tests/test_wavelength_sweep.py
from unittest.mock import MagicMock, patch
from plesty.scan_exp.measurements import wavelength_sweep
from plesty.lib.device.composite_device import CompositeDevice


def make_mock_device(query_value: str = "1.23e-3") -> MagicMock:
    """Create a mock device that returns a fixed value for any query."""
    mock = MagicMock()
    mock.query.return_value = query_value
    mock.write.return_value = True
    mock.identity.return_value = "Mock Device v1.0"
    mock.is_operatable = True
    return mock


def test_wavelength_sweep_basic():
    laser = make_mock_device()
    powermeter = make_mock_device(query_value="0.005")

    instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})

    result = wavelength_sweep(instruments, start_nm=1020, stop_nm=1025, step_nm=1)

    assert result.wavelengths == [1020, 1021, 1022, 1023, 1024, 1025]
    assert len(result.powers) == 6
    assert all(p == 0.005 for p in result.powers)


def test_wavelength_sweep_calls_write_in_order():
    laser = make_mock_device()
    powermeter = make_mock_device()
    instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})

    wavelength_sweep(instruments, start_nm=1060, stop_nm=1062, step_nm=1, settle_ms=0)

    write_calls = [call[0][1] for call in laser.write.call_args_list]
    assert write_calls == [1060, 1061, 1062]


def test_wavelength_sweep_respects_step():
    laser = make_mock_device()
    powermeter = make_mock_device()
    instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})

    result = wavelength_sweep(instruments, start_nm=1000, stop_nm=1010, step_nm=5)

    assert result.wavelengths == [1000, 1005, 1010]

Why Gate d1 does not apply to experiments

Gate d1 checks that the device's DevicePipeline mock tests are present and passing. Experiments are not devices — they do not inherit BaseDeviceSyncModel and do not have param_schema.json. The compliance standard for experiments is the same quantum standard, but Gate d1 is skipped because module_type != "device".

Coverage requirement (Gate 7)

Gate 7 requires ≥ 80% test coverage. For experiment modules, this means covering:

  1. Measurement functions with mocked instruments
  2. Edge cases (empty ranges, step > range, minimum values)
  3. Error paths (device raises an exception mid-sweep)
def test_wavelength_sweep_handles_device_error():
    laser = make_mock_device()
    powermeter = MagicMock()
    powermeter.query.side_effect = RuntimeError("Connection lost")

    instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})

    import pytest
    with pytest.raises(RuntimeError, match="Connection lost"):
        wavelength_sweep(instruments, start_nm=1060, stop_nm=1062)

Running tests

uv run pytest tests/ -v
uv run pytest tests/ --cov=plesty/scan_exp --cov-report=term-missing

The coverage report shows which lines are not covered. Focus on branches and error paths rather than chasing 100%.