Testing

This document explains the testing strategy for simind-python-connector, which handles the challenge of testing code that depends on optional external dependencies (SIRF, STIR, SIMIND, and PyTomography) that may not be available in every environment.

Test Organization

Test Markers

Tests are organized using pytest markers to indicate their dependencies:

  • @pytest.mark.unit - Pure unit tests with no external dependencies (CI-friendly)

  • @pytest.mark.requires_sirf - Tests that require SIRF to be installed

  • @pytest.mark.requires_stir - Tests that require STIR Python to be installed

  • @pytest.mark.requires_simind - Tests that require SIMIND command-line tool

  • @pytest.mark.requires_pytomography - Tests that require PyTomography to be installed

  • @pytest.mark.integration - Integration tests (may be slow)

  • @pytest.mark.ci_skip - Tests to skip in CI environments

Automatic Dependency Detection

The test suite automatically detects available dependencies:

  • SIRF detection: Attempts to import sirf

  • STIR detection: Attempts to import stir and import stirextra

  • PyTomography detection: Attempts to import pytomography

  • SIMIND detection: Checks if simind command is available

  • CI detection: Checks CI or GITHUB_ACTIONS environment variables

Tests requiring unavailable dependencies are automatically skipped with informative messages.

Running Tests

Local Development (All Tests)

# Run all available tests
pytest

# Run only unit tests (fast)
pytest -m unit

# Run only SIRF-dependent tests
pytest -m requires_sirf

# Run with coverage
pytest --cov=simind_python_connector --cov-report=html

CI Environment (GitHub Actions)

The GitHub Actions workflow runs only CI-friendly tests:

# CI command (no SIRF/SIMIND dependencies)
pytest -m "not requires_sirf and not requires_stir and not requires_simind and not requires_pytomography and not ci_skip"

Docker Backend Isolation

Use the dedicated backend containers to validate connector separation and run one example per backend in isolated environments:

bash scripts/run_container_validation.sh
bash scripts/run_container_examples.sh

Run only selected groups:

bash scripts/run_container_validation.sh --only-core
bash scripts/run_container_validation.sh --only-pytomography
bash scripts/run_container_examples.sh --only-osem

Override target architecture (or let scripts auto-detect from SIMIND binary):

bash scripts/run_container_examples.sh --only-core --docker-platform linux/amd64
bash scripts/run_container_validation.sh --with-simind --docker-platform linux/amd64

To include SIMIND-dependent integration/example checks in the validation run:

bash scripts/run_container_validation.sh --with-simind

The container scripts check for a repo-local SIMIND executable at ./simind/simind before running SIMIND-dependent checks. If missing, they skip those checks by default. Use --require-simind to fail fast instead. input.smc remains packaged in simind_python_connector/configs and is not part of the SIMIND runtime availability check.

SIMIND itself is not bundled with this package; install it separately from the official SIMIND site and manual:

The container suite includes tests/test_container_library_isolation.py, which asserts that each container can import only its target backend library. When SIMIND mode is enabled, it also runs geometry-isolation diagnostics: tests/test_geometry_isolation_forward_projection.py and tests/test_osem_geometry_diagnostics.py.

Alternative CI Configuration

You can also use the dedicated CI pytest configuration:

# Using CI-specific config
pytest -c pytest-ci.ini

Test Categories

CI-Friendly Test Suites

The default CI job runs the tests that avoid heavyweight dependencies. These focus on:

  • Conversion mathstests/test_schneider_density.py exercises the HU-to-density pipelines.

  • Configuration & builderstests/test_simulation_config.py and tests/test_acquisition_builder_unit.py validate YAML/SMC handling plus Interfile header generation without a SIRF runtime.

  • Connector behaviortests/test_python_connector.py, tests/test_connectors_base.py, and tests/test_connector_backend_separation.py cover the connector-first API without requiring reconstruction packages.

  • Utility helperstests/test_utils.py, tests/test_utils_small.py, tests/test_interfile_parser.py, and tests/test_interfile_numpy.py cover file parsing and small helper functions.

  • Backend guardstests/test_backends.py, tests/test_marker_policy.py, and tests/test_examples_backend_separation.py ensure optional backend imports remain isolated.

Backend-Dependent Suites

Tests that construct real STIR/SIRF/PyTomography objects carry dependency markers and are skipped when those packages are unavailable. These include:

  • tests/test_native_adaptors.py for STIR/SIRF adaptor behavior.

  • tests/test_pytomography_adaptor.py for PyTomography tensor and output adaptation.

  • tests/test_arithmetic_operations.py and tests/test_sirf_stir_utils.py for backend wrapper/helper behavior.

SIMIND and Container Diagnostics

SIMIND-dependent tests use requires_simind and are skipped unless the simind command is available. Container and geometry diagnostics include:

  • tests/test_integration.py for full workflow checks.

  • tests/test_container_library_isolation.py for backend container import isolation.

  • tests/test_geometry_isolation_forward_projection.py and tests/test_osem_geometry_diagnostics.py for backend geometry diagnostics.

Integration Test

tests/test_integration.py orchestrates a full example run and needs external runtime dependencies. It is marked with dependency markers so it is skipped in lightweight CI environments.

Configuration Files

pytest.ini (Local Development)

  • Runs all available tests based on detected dependencies

  • Includes verbose output and duration reporting

pytest-ci.ini (CI Environment)

  • Specifically filters out dependency-requiring tests

  • Optimized for GitHub Actions environment

tests/conftest.py

  • Configures pytest markers

  • Implements automatic dependency detection and test skipping

  • Handles CI environment detection

Adding New Tests

When adding new tests, use appropriate markers:

import pytest

@pytest.mark.unit
def test_pure_python_logic():
    """Test that doesn't need external dependencies."""
    assert True

@pytest.mark.requires_sirf
def test_sirf_functionality():
    """Test that uses SIRF objects."""
    from sirf.STIR import ImageData
    # ... test code

@pytest.mark.requires_simind
def test_simind_execution():
    """Test that calls SIMIND command."""
    # ... test code that runs simind

@pytest.mark.requires_pytomography
def test_pytomography_path():
    """Test that uses PyTomography APIs."""
    # ... test code that imports pytomography

This ensures your tests will be properly categorized and run in the appropriate environments.

Continuous Integration

GitHub Actions is used to run tests automatically. The CI workflow:

  1. Installs only basic Python dependencies (no SIRF/SIMIND)

  2. Runs code quality checks (ruff check and ruff format --check)

  3. Executes CI-friendly tests using dependency markers

  4. Generates coverage reports for the tested code

  5. Builds and validates the package

This approach ensures reliable CI while maintaining comprehensive test coverage for local development.