Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/labthings_fastapi/testing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Test harnesses to help with writitng tests for things.."""

from __future__ import annotations
from collections.abc import Iterator
from collections.abc import Iterator, Sequence
from concurrent.futures import Future
from contextlib import contextmanager
from typing import (
Expand Down Expand Up @@ -137,7 +137,7 @@

:return: None
"""
return None

Check warning on line 140 in src/labthings_fastapi/testing.py

View workflow job for this annotation

GitHub Actions / coverage

140 line is not covered with tests

@property
def global_lock(self) -> GlobalLock | None:
Expand Down Expand Up @@ -254,7 +254,7 @@
if isinstance(interface, MockThingServerInterface):
interface._mocks.append(mock)
else:
raise TypeError(

Check warning on line 257 in src/labthings_fastapi/testing.py

View workflow job for this annotation

GitHub Actions / coverage

257 line is not covered with tests
"Slots may not be mocked when a Thing is attached to a real "
"server."
)
Expand All @@ -270,3 +270,37 @@
"""Use the dummy URL for function in the context variable."""
with set_url_for_context(dummy_url_for):
yield


def manually_connect_thing_slot(
Comment thread
julianstirling marked this conversation as resolved.
host: Thing,
slot_name: str,
target: Thing | Sequence[Thing],
) -> None:
"""Manually connect a thing_slot.

This will accept either a single `Thing` instance or a sequence
of `Thing` instances. If `Mock` instances are used, note that they
must pass an `isinstance` test, so should use the ``spec`` argument
to specify the correct class for the `~lt.thing_slot` being mocked.
Mock instances must also provide a unique ``name`` attribute.

:param host: the `~lt.Thing` on which the slot is defined.
:param slot_name: the name of the `~lt.thing_slot`.
:param target: the `~lt.Thing` or sequence of Things it should be connected to.
If a sequence of multiple Thing are passed, their names are used to create a
mapping.
:raises KeyError: if multiple targets are specified, but they do not
have unique names.
"""
if not isinstance(target, Sequence):
names: str | Sequence[str] = target.name
things = {target.name: target}
else:
names = [t.name for t in target]
if len(set(names)) != len(names):
msg = f"Thing slot targets {names} are not uniquely named."
raise KeyError(msg)
things = {t.name: t for t in target}
slot = getattr(host.__class__, slot_name)
slot.connect(host, target=names, things=things)
57 changes: 57 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Test the `testing` module."""

from collections.abc import Mapping

import pytest

import labthings_fastapi as lt
from labthings_fastapi import testing


class ThingA(lt.Thing):
"""A Thing subclass that connects to ThingB and ThingC."""

friend: "ThingB" = lt.thing_slot()
friends: "Mapping[str, ThingC]" = lt.thing_slot()


class ThingB(lt.Thing):
"""A Thing subclass that connects to a ThingA,"""

friend: "ThingA" = lt.thing_slot()


class ThingC(lt.Thing):
"""A dummy Thing subclass."""


@pytest.mark.parametrize("mock_slots", [True, False])
def test_manual_connect(mock_slots, mocker):
"""Make sure we can create and connect Things without a server."""
a = testing.create_thing_without_server(ThingA, mock_all_slots=mock_slots)
b = testing.create_thing_without_server(ThingB, mock_all_slots=mock_slots)
c = testing.create_thing_without_server(ThingC, mock_all_slots=mock_slots)

testing.manually_connect_thing_slot(a, "friend", b)
testing.manually_connect_thing_slot(a, "friends", [c])
testing.manually_connect_thing_slot(b, "friend", a)

assert a.friend is b
assert len(a.friends) == 1
assert a.friends["thingc"] is c
assert b.friend is a

mc1 = mocker.Mock(spec=ThingC)
mc1.name = "mock_c_1"
mc2 = mocker.Mock(spec=ThingC)
mc2.name = "mock_c_2"

testing.manually_connect_thing_slot(a, "friends", [mc1, mc2])
assert a.friends["mock_c_1"] is mc1
assert a.friends["mock_c_2"] is mc2

with pytest.raises(TypeError):
testing.manually_connect_thing_slot(a, "friend", mocker.Mock())

with pytest.raises(KeyError, match="are not uniquely named"):
testing.manually_connect_thing_slot(a, "friend", [mc1, mc1])
Loading