Skip to content

Commit fdd5333

Browse files
authored
Merge pull request #113 from AllenNeuralDynamics/wip/pytest
Wip/pytest
2 parents d3b933e + 5ea56be commit fdd5333

13 files changed

+463
-39
lines changed

parallax/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import os
66

7-
__version__ = "1.1.3"
7+
__version__ = "1.1.4"
88

99
# allow multiple OpenMP instances
1010
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"

parallax/axis_filter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def __init__(self, model, camera_name):
193193
self.worker = None
194194
self.name = camera_name
195195
self.thread = None
196+
self.threadDeleted = False
196197

197198
def init_thread(self):
198199
"""Initialize or reinitialize the worker and thread"""
@@ -207,7 +208,6 @@ def init_thread(self):
207208
self.thread.destroyed.connect(self.onThreadDestroyed)
208209
self.threadDeleted = False
209210

210-
#self.worker.frame_processed.connect(self.frame_processed)
211211
self.worker.frame_processed.connect(self.frame_processed.emit)
212212
self.worker.found_coords.connect(self.found_coords)
213213
self.worker.finished.connect(self.thread.quit)
@@ -266,16 +266,18 @@ def clean(self):
266266
if self.worker is not None:
267267
self.worker.stop_running() # Signal the worker to stop
268268

269-
if not self.threadDeleted and self.thread.isRunning():
269+
if self.thread and not self.threadDeleted and self.thread.isRunning():
270270
self.thread.quit() # Ask the thread to quit
271271
self.thread.wait() # Wait for the thread to finish
272272
self.thread = None # Clear the reference to the thread
273273
self.worker = None # Clear the reference to the worker
274+
self.threadDeleted = True
274275
logger.debug(f"{self.name} Cleaned the thread")
275276

276277
def onThreadDestroyed(self):
277278
"""Flag if thread is deleted"""
278279
self.threadDeleted = True
280+
self.thread = None
279281

280282
def __del__(self):
281283
"""Destructor for the filter object."""

parallax/probe_detect_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ def __init__(self, model, camera_name):
395395
self.worker = None
396396
self.name = camera_name
397397
self.thread = None
398+
self.threadDeleted = False
398399

399400
def init_thread(self):
400401
"""
@@ -537,11 +538,12 @@ def clean(self):
537538
if self.worker is not None:
538539
self.worker.stop_running()
539540

540-
if not self.threadDeleted and self.thread.isRunning():
541+
if self.thread and not self.threadDeleted and self.thread.isRunning():
541542
self.thread.quit() # Ask the thread to quit
542543
self.thread.wait() # Wait for the thread to finish
543544
self.thread = None # Clear the reference to the thread
544545
self.worker = None # Clear the reference to the worker
546+
self.threadDeleted = True
545547
logger.debug(f"{self.name} Cleaned the thread")
546548

547549
def __del__(self):

parallax/recording_manager.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ def __init__(self, model):
2121
"""Initialize recording manager"""
2222
self.model = model
2323
self.recording_camera_list = []
24+
self.snapshot_camera_list = []
2425

2526
def save_last_image(self, save_path, screen_widgets):
2627
"""Saves the last captured image from all active camera feeds."""
2728
# Initialize the list to keep track of cameras from which an image has been saved
28-
snapshot_camera_list = []
29+
self.snapshot_camera_list = []
2930
# Get the directory path where the images will be saved
3031
if os.path.exists(save_path):
3132
print("\nSnapshot...")
@@ -34,7 +35,7 @@ def save_last_image(self, save_path, screen_widgets):
3435
if screen.is_camera():
3536
# Use custom name of the camera if it has one, otherwise use the camera's serial number
3637
camera_name = screen.get_camera_name()
37-
if camera_name not in snapshot_camera_list:
38+
if camera_name not in self.snapshot_camera_list:
3839
customName = screen.parent().title()
3940
customName = customName if customName else camera_name
4041

@@ -44,7 +45,7 @@ def save_last_image(self, save_path, screen_widgets):
4445
)
4546

4647
# Add the camera to the list of cameras from which an image has been saved
47-
snapshot_camera_list.append(camera_name)
48+
self.snapshot_camera_list.append(camera_name)
4849
else:
4950
logger.debug("save_last_image) camera not found")
5051
else:

parallax/reticle_detect_manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ def __init__(self, camera_name):
237237
self.worker = None
238238
self.name = camera_name
239239
self.thread = None
240+
self.threadDeleted = False
240241

241242
def init_thread(self):
242243
"""Initialize the worker thread."""
@@ -302,17 +303,19 @@ def clean(self):
302303
if self.worker is not None:
303304
self.worker.stop_running() # Signal the worker to stop
304305

305-
if not self.threadDeleted and self.thread.isRunning():
306+
if self.thread and not self.threadDeleted and self.thread.isRunning():
306307
logger.debug(f"{self.name} Stopping thread in {self.__class__.__name__}")
307308
self.thread.quit() # Ask the thread to quit
308309
self.thread.wait() # Wait for the thread to finish
309310
self.thread = None # Clear the reference to the thread
310311
self.worker = None # Clear the reference to the worker
312+
self.threadDeleted = True
311313
logger.debug(f"{self.name} Cleaned the thread")
312314

313315
def onThreadDestroyed(self):
314316
"""Flag if thread is deleted"""
315317
self.threadDeleted = True
318+
self.thread = None
316319

317320
def __del__(self):
318321
"""Destructor for the reticle detection manager."""

tests/test__setting.py

Lines changed: 0 additions & 11 deletions
This file was deleted.

tests/test_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from parallax.camera import MockCamera, PySpinCamera
66
from parallax.stage_listener import Stage, StageInfo
77

8-
@pytest.fixture
8+
@pytest.fixture(scope='function')
99
def model():
1010
"""Fixture to initialize the Model object."""
1111
return Model(version="V2", bundle_adjustment=False)

tests/test_recording_manager.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# test_recording_manager.py
2+
3+
import pytest
4+
import os
5+
from unittest.mock import Mock, MagicMock
6+
from parallax.recording_manager import RecordingManager
7+
8+
@pytest.fixture
9+
def mock_model():
10+
"""Fixture to create a mock model object."""
11+
return Mock()
12+
13+
@pytest.fixture
14+
def mock_screen_widget():
15+
"""Fixture to create a mock screen widget."""
16+
screen = Mock()
17+
screen.is_camera = Mock(return_value=True)
18+
screen.get_camera_name = Mock(return_value="MockCamera123")
19+
screen.save_image = Mock()
20+
screen.save_recording = Mock()
21+
screen.stop_recording = Mock()
22+
screen.parent = Mock()
23+
screen.parent().title = Mock(return_value="MockCamera")
24+
return screen
25+
26+
@pytest.fixture
27+
def recording_manager(mock_model):
28+
"""Fixture to create a RecordingManager instance."""
29+
return RecordingManager(mock_model)
30+
31+
def test_save_last_image(recording_manager, mock_screen_widget, tmpdir):
32+
"""Test saving the last image for a camera."""
33+
save_path = str(tmpdir)
34+
screen_widgets = [mock_screen_widget]
35+
36+
# Call the method to save the last image.
37+
recording_manager.save_last_image(save_path, screen_widgets)
38+
39+
# Assert that the save_image method was called.
40+
mock_screen_widget.save_image.assert_called_once_with(
41+
save_path, isTimestamp=True, name="MockCamera"
42+
)
43+
# Assert that the camera name was added to the snapshot_camera_list.
44+
assert "MockCamera123" in recording_manager.snapshot_camera_list, (
45+
f"{mock_screen_widget.get_camera_name()} was not tracked as a snapshot camera."
46+
)
47+
48+
def test_save_last_image_directory_not_exists(recording_manager, mock_screen_widget):
49+
"""Test saving the last image when the save path does not exist."""
50+
save_path = "non_existent_directory"
51+
screen_widgets = [mock_screen_widget]
52+
53+
# Call the method to save the last image.
54+
recording_manager.save_last_image(save_path, screen_widgets)
55+
56+
# Assert that save_image was not called since the directory does not exist.
57+
mock_screen_widget.save_image.assert_not_called()
58+
59+
def test_save_recording(recording_manager, mock_screen_widget, tmpdir):
60+
"""Test starting a recording for a camera."""
61+
save_path = str(tmpdir)
62+
screen_widgets = [mock_screen_widget]
63+
64+
# Call the method to start recording.
65+
recording_manager.save_recording(save_path, screen_widgets)
66+
67+
# Assert that the save_recording method was called.
68+
mock_screen_widget.save_recording.assert_called_once_with(
69+
save_path, isTimestamp=True, name="MockCamera"
70+
)
71+
# Assert that the camera name is in the list of currently recording cameras.
72+
assert mock_screen_widget.get_camera_name() in recording_manager.recording_camera_list
73+
74+
def test_save_recording_directory_not_exists(recording_manager, mock_screen_widget):
75+
"""Test starting a recording when the save path does not exist."""
76+
save_path = "non_existent_directory"
77+
screen_widgets = [mock_screen_widget]
78+
79+
# Call the method to start recording.
80+
recording_manager.save_recording(save_path, screen_widgets)
81+
82+
# Assert that save_recording was not called since the directory does not exist.
83+
mock_screen_widget.save_recording.assert_not_called()
84+
85+
def test_stop_recording(recording_manager, mock_screen_widget):
86+
"""Test stopping a recording for a camera."""
87+
screen_widgets = [mock_screen_widget]
88+
89+
# Add the camera to the recording list to simulate it being recorded.
90+
recording_manager.recording_camera_list.append(mock_screen_widget.get_camera_name())
91+
92+
# Call the method to stop recording.
93+
recording_manager.stop_recording(screen_widgets)
94+
95+
# Assert that the stop_recording method was called.
96+
mock_screen_widget.stop_recording.assert_called_once()
97+
98+
# Assert that the camera was removed from the recording list.
99+
assert mock_screen_widget.get_camera_name() not in recording_manager.recording_camera_list
100+
101+
def test_stop_recording_not_recording_camera(recording_manager, mock_screen_widget):
102+
"""Test stopping a recording when the camera is not in the recording list."""
103+
screen_widgets = [mock_screen_widget]
104+
105+
# Ensure the camera is not in the recording list.
106+
recording_manager.recording_camera_list = []
107+
108+
# Call the method to stop recording.
109+
recording_manager.stop_recording(screen_widgets)
110+
111+
# Assert that the stop_recording method was not called since the camera was not recording.
112+
mock_screen_widget.stop_recording.assert_not_called()

tests/test_screen_widget.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# tests/test_screen_widget.py
2+
import pytest
3+
import numpy as np
4+
from unittest.mock import Mock
5+
from PyQt5.QtWidgets import QApplication
6+
from PyQt5.QtCore import Qt, QPoint
7+
from parallax.screen_widget import ScreenWidget
8+
9+
@pytest.fixture(scope="function")
10+
def qapp():
11+
"""Fixture for creating a QApplication."""
12+
app = QApplication([])
13+
yield app
14+
app.quit()
15+
16+
@pytest.fixture(scope="function")
17+
def mock_camera():
18+
"""Fixture to create a mock camera object."""
19+
camera = Mock()
20+
camera.width = 4000
21+
camera.height = 3000
22+
camera.name.return_value = "MockCamera123"
23+
camera.get_last_image_data.return_value = np.zeros((4000, 3000), dtype=np.uint8)
24+
camera.get_last_image_data_singleFrame.return_value = np.zeros((4000, 3000), dtype=np.uint8)
25+
return camera
26+
27+
@pytest.fixture(scope="function")
28+
def mock_model():
29+
model = Mock()
30+
probe_detector = Mock()
31+
model.add_probe_detector(probe_detector)
32+
return model
33+
34+
@pytest.fixture(scope="function")
35+
def screen_widget(qapp, mock_camera, mock_model):
36+
"""Fixture to create a ScreenWidget instance with a mock camera."""
37+
return ScreenWidget(camera=mock_camera, model=mock_model)
38+
39+
def test_simple(screen_widget):
40+
assert True
41+
42+
def test_screen_widget_initialization(screen_widget):
43+
assert screen_widget.camera is not None
44+
assert screen_widget.view_box is not None
45+
assert screen_widget.image_item is not None
46+
47+
def test_set_data(screen_widget, mock_camera):
48+
# Prepare mock image data
49+
data = np.random.randint(0, 256, (4000, 3000), dtype=np.uint8)
50+
51+
# Mock the process method in NoFilter and AxisFilter
52+
screen_widget.filter.process = Mock()
53+
screen_widget.axisFilter.process = Mock()
54+
screen_widget.reticleDetector.process = Mock()
55+
screen_widget.probeDetector.process = Mock()
56+
57+
# Call set_data to process the image
58+
screen_widget.set_data(data)
59+
60+
# Assert that each filter's process method was called with the data
61+
screen_widget.filter.process.assert_called_once_with(data)
62+
screen_widget.axisFilter.process.assert_called_once_with(data)
63+
screen_widget.reticleDetector.process.assert_called_once_with(data)
64+
screen_widget.probeDetector.process.assert_called_once_with(
65+
data, screen_widget.camera.get_last_capture_time.return_value
66+
)
67+
68+
def test_start_and_stop_acquisition_camera(screen_widget, mock_camera):
69+
# Mock the methods for camera acquisition
70+
mock_camera.begin_continuous_acquisition = Mock()
71+
mock_camera.stop = Mock()
72+
73+
# Start acquisition and verify the method is called
74+
screen_widget.start_acquisition_camera()
75+
mock_camera.begin_continuous_acquisition.assert_called_once()
76+
77+
# Stop acquisition and verify the method is called
78+
screen_widget.stop_acquisition_camera()
79+
mock_camera.stop.assert_called_once_with(clean=False)
80+
81+
def test_image_clicked_signal(screen_widget, qtbot):
82+
with qtbot.waitSignal(screen_widget.selected, timeout=1000) as signal_waiter:
83+
mock_event = Mock()
84+
mock_event.pos = Mock(return_value=QPoint(100, 100)) # Use QPoint for pos()
85+
mock_event.button = Mock(return_value=Qt.LeftButton)
86+
87+
# Simulate a mouse click on the image using the mock event.
88+
screen_widget.image_clicked(mock_event)
89+
90+
emitted_args = signal_waiter.args
91+
assert emitted_args[0] == screen_widget.camera_name, "The camera name is incorrect."
92+
assert emitted_args[1] == (100, 100), "The clicked position is incorrect."
93+
94+
def test_zoom_out(screen_widget):
95+
# Mock the autoRange method of the view_box
96+
screen_widget.view_box.autoRange = Mock()
97+
98+
# Call zoom_out
99+
screen_widget.zoom_out()
100+
101+
# Verify that autoRange was called
102+
screen_widget.view_box.autoRange.assert_called_once()

0 commit comments

Comments
 (0)