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
17 changes: 17 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Tests

on:
pull_request:

jobs:
tests:
runs-on: ubuntu-latest
container:
image: cubertgmbh/cuvis_pyil:3.5.0-ubuntu24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install test dependencies
run: python3 -m pip install -e ".[test]"
- name: Run tests
run: pytest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
/venv
/cuvis/__pycache__
/.venv310
/tests/__pycache__
/.claude
2 changes: 1 addition & 1 deletion cuvis/FileWriteSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ def _from_internal(cls, pa: cuvis_il.cuvis_proc_args_t):

@dataclass
class WorkerSettings(object):
input_queue_size: int = 0
input_queue_size: int = 10
mandatory_queue_size: int = 4
supplementary_queue_size: int = 4
output_queue_size: int = 10
Expand Down
5 changes: 5 additions & 0 deletions cuvis/cube_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ def __init__(self, img_buf=None, dformat=None):
self.channels = None
self.array = None
self.wavelength = None
self._img_buf = None

elif isinstance(img_buf, cuvis_il.cuvis_imbuffer_t):
# Keep a reference to the underlying buffer so the NumPy view
# remains valid for the lifetime of this ImageData instance.
self._img_buf = img_buf

if dformat is None:
raise TypeError("Missing format for reading image buffer")
Expand Down Expand Up @@ -108,4 +112,5 @@ def from_array(cls, array: np.ndarray, width: int, height: int, channels: int, w
instance.height = height
instance.channels = channels
instance.wavelength = wavelength
instance._img_buf = None
return instance
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "cuvis"
version = "3.5.0"
version = "3.5.0.2"
description = "CUVIS Python SDK."
readme = "README.md"
requires-python = ">=3.9"
Expand Down Expand Up @@ -44,3 +44,10 @@ include-package-data = true

[tool.setuptools.package-data]
cuvis = ["git-hash.txt"]

[project.optional-dependencies]
test = [
"pytest>=7.0.0",
"pytest-timeout>=2.1.0",
"pytest-xdist>=3.0.0",
]
12 changes: 12 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--tb=short
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test package for cuvis.python
108 changes: 108 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Pytest configuration and fixtures for cuvis.python integration tests.

This module provides shared fixtures for testing the CUVIS SDK Python wrapper.
All fixtures use the real SDK (integration testing, not mocked).
"""

import pytest
import tempfile
import shutil
from pathlib import Path
import os
import gc
import cuvis


@pytest.fixture(scope="session")
def sdk_initialized():
"""
Initialize SDK once per test session.

Uses CUVIS_SETTINGS environment variable if set, otherwise uses current directory.
"""
settings_path = os.environ.get("CUVIS_SETTINGS", ".")
cuvis.init(settings_path=settings_path)
yield
gc.collect()
# cuvis.shutdown()


@pytest.fixture(scope="session")
def test_data_dir():
"""Path to test data directory containing test_mesu.cu3s."""
return Path(__file__).parent / "test_data"


@pytest.fixture(scope="session")
def rgb_userplugin_path(test_data_dir):
"""
Path to RGB user plugin file (00_RGB.xml).

Skips tests if the file is not found.
"""
plugin_path = test_data_dir / "00_RGB.xml"
if not plugin_path.exists():
pytest.skip(f"RGB user plugin not found: {plugin_path}")
return str(plugin_path)


@pytest.fixture(scope="session")
def test_session_file(test_data_dir, sdk_initialized):
"""
Load test SessionFile once per session.

Skips tests if the file is not found.
"""
session_path = test_data_dir / "test_mesu.cu3s"
if not session_path.exists():
pytest.skip(f"Test data not found: {session_path}")
session = cuvis.SessionFile(str(session_path))
yield session
del session
gc.collect()


@pytest.fixture
def test_measurement(test_session_file):
"""
Get first measurement from Test session.

Function-scoped to ensure each test gets a fresh measurement reference.
"""
return test_session_file.get_measurement(0)


@pytest.fixture
def processing_context_from_session(test_session_file):
"""
Create ProcessingContext from SessionFile.

Function-scoped to ensure each test gets a fresh context.
"""
return cuvis.ProcessingContext(test_session_file)


@pytest.fixture
def temp_output_dir():
"""
Temporary directory for export outputs.

Automatically cleaned up after test completes.
"""
temp_dir = tempfile.mkdtemp()
yield Path(temp_dir)
shutil.rmtree(temp_dir, ignore_errors=True)


@pytest.fixture(scope="session")
def simulated_acquisition_context(test_session_file, sdk_initialized):
"""
Create simulated AcquisitionContext from SessionFile.

Session-scoped since acquisition context initialization can be slow.
"""
acq = cuvis.AcquisitionContext(test_session_file, simulate=True)
yield acq
del acq
gc.collect()
99 changes: 99 additions & 0 deletions tests/test_acquisition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
Tests for cuvis.AcquisitionContext module.

Mirrors functionality from Example 1 notebook (Take Snapshot) related to
simulated camera acquisition, operation modes, and snapshot capture.
"""

import pytest
import time
import cuvis


def test_simulated_acquisition_context_creation(simulated_acquisition_context):
"""Test simulated AcquisitionContext created successfully."""
assert simulated_acquisition_context is not None
assert isinstance(simulated_acquisition_context, cuvis.AcquisitionContext)


def test_acquisition_context_state(simulated_acquisition_context):
"""Test acquisition context state property."""
state = simulated_acquisition_context.state
assert isinstance(state, cuvis.HardwareState)


def test_acquisition_context_ready(simulated_acquisition_context):
"""Test acquisition context ready property."""
ready = simulated_acquisition_context.ready
assert isinstance(ready, bool)


def test_acquisition_context_operation_mode(simulated_acquisition_context):
"""Test operation mode get/set."""
# Get current mode
original_mode = simulated_acquisition_context.operation_mode
assert isinstance(original_mode, cuvis.OperationMode)

# Set to Software mode
simulated_acquisition_context.operation_mode = cuvis.OperationMode.Software
assert simulated_acquisition_context.operation_mode == cuvis.OperationMode.Software

# Restore original
simulated_acquisition_context.operation_mode = original_mode


def test_acquisition_context_integration_time(simulated_acquisition_context):
"""Test integration time get/set."""
# Get current integration time
original_time = simulated_acquisition_context.integration_time
assert isinstance(original_time, (int, float))
assert original_time > 0

# Set new integration time
new_time = 10.0
simulated_acquisition_context.integration_time = new_time
assert simulated_acquisition_context.integration_time == new_time

# Restore original
simulated_acquisition_context.integration_time = original_time


def test_acquisition_context_session_info(simulated_acquisition_context):
"""Test session info get/set."""
session_info = simulated_acquisition_context.session_info
assert isinstance(session_info, cuvis.SessionData)


@pytest.mark.slow
def test_simulated_capture_snapshot(simulated_acquisition_context, processing_context_from_session):
"""Test capturing snapshot in simulated mode."""
acq = simulated_acquisition_context

# Set operation mode to Software
acq.operation_mode = cuvis.OperationMode.Software

# Wait for ready state (with timeout)
timeout = 10 # seconds
start = time.time()
while not acq.ready and (time.time() - start) < timeout:
time.sleep(0.1)

if not acq.ready:
pytest.skip("Acquisition context not ready within timeout")

# Capture snapshot
mesu = acq.capture_at(timeout_ms=5000)
assert isinstance(mesu, cuvis.Measurement)

# Verify we can process the captured measurement
pc = processing_context_from_session
pc.processing_mode = cuvis.ProcessingMode.Raw
pc.apply(mesu)
assert 'cube' in mesu.data


def test_acquisition_context_component_count(simulated_acquisition_context):
"""Test component count property."""
count = simulated_acquisition_context.component_count
assert isinstance(count, int)
assert count >= 0
75 changes: 75 additions & 0 deletions tests/test_data/00_RGB.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" ?>
<userplugin xmlns="http://cubert-gmbh.de/user/plugin/userplugin.xsd">
<configuration name="RGB" citation="CUBERT" plugin_author="Cubert" plugin_version="1.1.0" required_engine_version="3.2.0">
<comment>
RGB View with adjustable bandwidths using manual fastmono channels:
---
Red := avg(RedWL ± Width/2)
Green := avg(GreenWL ± Width/2)
Blue := avg(BlueWL ± Width/2)
</comment>

<input id="RedWL" type="scalar" min="300" max="1200" tickFreq="1">650</input>
<input id="GreenWL" type="scalar" min="300" max="1200" tickFreq="1">550</input>
<input id="BlueWL" type="scalar" min="300" max="1200" tickFreq="1">470</input>
<input id="Width" type="scalar" min="1" max="200">20</input>
<input id="Normalize" type="scalar">0.75</input>

<!-- Calculate half width -->
<evaluate id="HalfWidth">
<operator type="divide">
<variable ref="Width"/>
<value>2</value>
</operator>
</evaluate>

<!-- Red bounds -->
<evaluate id="RedMin">
<operator type="subtract">
<variable ref="RedWL"/>
<variable ref="HalfWidth"/>
</operator>
</evaluate>
<evaluate id="RedMax">
<operator type="add">
<variable ref="RedWL"/>
<variable ref="HalfWidth"/>
</operator>
</evaluate>

<!-- Green bounds -->
<evaluate id="GreenMin">
<operator type="subtract">
<variable ref="GreenWL"/>
<variable ref="HalfWidth"/>
</operator>
</evaluate>
<evaluate id="GreenMax">
<operator type="add">
<variable ref="GreenWL"/>
<variable ref="HalfWidth"/>
</operator>
</evaluate>

<!-- Blue bounds -->
<evaluate id="BlueMin">
<operator type="subtract">
<variable ref="BlueWL"/>
<variable ref="HalfWidth"/>
</operator>
</evaluate>
<evaluate id="BlueMax">
<operator type="add">
<variable ref="BlueWL"/>
<variable ref="HalfWidth"/>
</operator>
</evaluate>

<output_image show="true" id="RGB">
<fast_rgb red_min="RedMin" red_max="RedMax" green_min="GreenMin" green_max="GreenMax" blue_min="BlueMin" blue_max="BlueMax" normalization="Normalize">
<cube/>
</fast_rgb>
</output_image>

</configuration>
</userplugin>
Binary file added tests/test_data/test_mesu.cu3s
Binary file not shown.
Loading