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
533 changes: 496 additions & 37 deletions poetry.lock

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ optional = true
[tool.poetry.group.docs.dependencies]
# The latest Sphinx requires a recent Python version.
Sphinx = { version = ">=8.2", python = "^3.11" }
sphinx-rtd-theme = ">=1.0.0"
sphinx-autoapi = ">=1.8.4"
m2r2 = ">=0.3.2"
toml = ">=0.10.2"
sphinx-rtd-theme = { version = ">=1.0.0", python = "^3.11" }
sphinx-autoapi = { version = ">=1.8.4", python = "^3.11" }
m2r2 = { version = ">=0.3.2", python = "^3.11" }
toml = { version = ">=0.10.2", python = "^3.11" }


[tool.poetry.group.lint.dependencies]
Expand All @@ -61,6 +61,12 @@ ni-python-styleguide = ">=0.4.6"
bandit = ">=1.8.6"


[tool.poetry.group.test.dependencies]
pytest = ">=8.3.4"
pytest-mock = ">=3.14"
pytest-cov = ">=6"


[tool.ni-python-styleguide]
extend_exclude = "generated,src/codegen/metadata,src/codegen/templates,src/handwritten"

Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the nislsc package."""
1 change: 1 addition & 0 deletions tests/unit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Unit tests for the nislsc package."""
40 changes: 40 additions & 0 deletions tests/unit/_session_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from unittest.mock import Mock


def expect_initialize_library(
interpreter: Mock,
library_handle: int = 1,
) -> None:
"""Expect a call to interpreter.initialize_library."""
interpreter.initialize_library.return_value = library_handle


def expect_initialize_session_with_devices(
interpreter: Mock,
session_handle: int = 100,
) -> None:
"""Expect a call to interpreter.initialize_session_with_devices."""
interpreter.initialize_session_with_devices.return_value = session_handle


def expect_initialize_session_with_nvmem_areas(
interpreter: Mock, session_handle: int = 200
) -> None:
"""Expect a call to interpreter.initialize_session_with_nvmem_areas."""
interpreter.initialize_session_with_nvmem_areas.return_value = session_handle


def expect_initialize_session_with_physical_channels(
interpreter: Mock, session_handle: int = 300
) -> None:
"""Expect a call to interpreter.initialize_session_with_physical_channels."""
interpreter.initialize_session_with_physical_channels.return_value = session_handle


def expect_initialize_session_without_resources(
interpreter: Mock, session_handle: int = 400
) -> None:
"""Expect a call to interpreter.initialize_session_without_resources."""
interpreter.initialize_session_without_resources.return_value = session_handle
62 changes: 62 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Shared pytest fixtures for unit tests."""

from __future__ import annotations

from collections.abc import Generator
from unittest.mock import Mock

import pytest
from pytest_mock import MockerFixture

from nislsc import Library, Session
from nislsc._base_interpreter import BaseInterpreter
from nislsc.constants import Language, ReservationAccess
from tests.unit._session_utils import (
expect_initialize_library,
expect_initialize_session_with_devices,
)


@pytest.fixture
def interpreter(mocker: MockerFixture) -> Mock:
"""Create a mock interpreter.

Patches _select_interpreter in all modules that call it, and patches
get_library_version so Library.__init__ does not need a real driver.
"""
mock_interpreter = mocker.create_autospec(BaseInterpreter)
mock_interpreter._library_handle = 0
mock_interpreter._language = Language.CURRENT_THREAD_LOCALE

for target in (
"nislsc.library._select_interpreter",
"nislsc.utils._select_interpreter",
):
stub = mocker.patch(target, autospec=True)
stub.return_value = mock_interpreter

mocker.patch("nislsc.library.get_library_version", return_value=1)

return mock_interpreter


@pytest.fixture
def library(interpreter: Mock) -> Generator[Library, None, None]:
"""Create a Library instance backed by the mock interpreter."""
expect_initialize_library(interpreter)
with Library() as lib:
yield lib


@pytest.fixture
def session(library: Library, interpreter: Mock) -> Generator[Session, None, None]:
"""Create a Session with devices, backed by the mock interpreter.

This fixture owns the session. Do not use it in tests that destroy the
session, or you may get double-close warnings.
"""
expect_initialize_session_with_devices(interpreter)
with Session.initialize_session_with_devices(
library, "Dev1", -1.0, ReservationAccess.NONE, "", -1.0
) as sess:
yield sess
110 changes: 110 additions & 0 deletions tests/unit/test_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import annotations

from unittest.mock import Mock

from nislsc import Command, Session


def test___session_with_device___open_device_command___command_handle_set(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500

with Command.open_device_command(session, "Dev1", "Reset") as cmd:
assert cmd._command_handle == 500
interpreter.open_device_command.assert_called_once_with(
session._session_handle, "Dev1", "Reset"
)


def test___session_with_device___open_device_command___session_reference_stored(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500

with Command.open_device_command(session, "Dev1", "Reset") as cmd:
assert cmd._session is session
assert cmd._interpreter is session._interpreter


def test___session_with_physical_channel___open_physical_channel_command___command_handle_set(
interpreter: Mock, session: Session
) -> None:
interpreter.open_physical_channel_command.return_value = 501

with Command.open_physical_channel_command(session, "Dev1/phys0", "SomeCmd") as cmd:
assert cmd._command_handle == 501
interpreter.open_physical_channel_command.assert_called_once_with(
session._session_handle, "Dev1/phys0", "SomeCmd"
)


def test___session_with_generic_command___open_generic_command___command_handle_set(
interpreter: Mock, session: Session
) -> None:
interpreter.open_generic_command.return_value = 502

with Command.open_generic_command(session, "$DefaultDevices", "SomeCmd") as cmd:
assert cmd._command_handle == 502
interpreter.open_generic_command.assert_called_once_with(
session._session_handle, "$DefaultDevices", "SomeCmd"
)


def test___open_device_command_open___close___close_command_called_with_handle(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500
cmd = Command.open_device_command(session, "Dev1", "Reset")

cmd.close()

interpreter.close_command.assert_called_once_with(500)
assert cmd._command_handle == 0


def test___open_device_command_open___close_twice___close_command_called_once(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500
cmd = Command.open_device_command(session, "Dev1", "Reset")

cmd.close()
cmd.close()

interpreter.close_command.assert_called_once_with(500)


def test___open_device_command_open___close_command___interpreter_close_command_called(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500
cmd = Command.open_device_command(session, "Dev1", "Reset")

cmd.close_command()

interpreter.close_command.assert_called_once_with(500)


def test___open_device_command_open___context_manager___close_command_called_on_exit(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500

with Command.open_device_command(session, "Dev1", "Reset"):
interpreter.close_command.assert_not_called()

interpreter.close_command.assert_called_once_with(500)


def test___open_device_command_open___get_command_property_string___returns_value(
interpreter: Mock, session: Session
) -> None:
interpreter.open_device_command.return_value = 500
interpreter.get_command_property_string.return_value = "Reset the device."

with Command.open_device_command(session, "Dev1", "Reset") as cmd:
result = cmd.get_command_property_string("Cmd.Descr")

interpreter.get_command_property_string.assert_called_once_with(500, "Cmd.Descr")
assert result == "Reset the device."
Loading