Commit 0dc6c7d0 authored by vertighel's avatar vertighel
Browse files

Merge camera tests into single file using devices.cam/cam2

parent e59594b4
Loading
Loading
Loading
Loading
Loading

tests/test_camera_atik.py

deleted100644 → 0
+0 −135
Original line number Diff line number Diff line
import ctypes
import os
import tempfile
import unittest
from unittest.mock import patch, MagicMock, ANY

from noctua.devices.atik import Camera


class TestAtikCameraMethods(unittest.TestCase):
    """Unit tests for atik.Camera (devices.cam2)."""

    def setUp(self):
        with patch('ctypes.CDLL'):
            self.camera = Camera("dummy")

        # Replace lib with a controllable mock; simulate a connected camera
        self.mock_lib = MagicMock()
        self.camera._lib = self.mock_lib
        self.camera._handle = MagicMock(name='fake_handle')  # truthy → _check_connection returns it
        self.camera._props.nPixelsX = 4096
        self.camera._props.nPixelsY = 4126
        self.camera._subframe = [0, 0, 4096, 4126]
        self.camera.error = []

    # --- abort ---

    def test_abort(self):
        self.camera.abort()
        self.mock_lib.ArtemisAbortExposure.assert_called_once_with(self.camera._handle)

    # --- start ---

    def test_start_light(self):
        self.camera.start(10.0, 1)
        self.mock_lib.ArtemisSetDarkMode.assert_called_once_with(self.camera._handle, False)
        self.mock_lib.ArtemisStartExposure.assert_called_once()

    def test_start_dark(self):
        self.camera.start(10.0, 0)
        self.mock_lib.ArtemisSetDarkMode.assert_called_once_with(self.camera._handle, True)

    def test_start_bias_is_dark(self):
        self.camera.start(0.0, 2)
        self.mock_lib.ArtemisSetDarkMode.assert_called_once_with(self.camera._handle, True)

    def test_start_accepts_datetime_kwarg(self):
        # datetime is accepted but unused by the Atik SDK path
        self.camera.start(5.0, 1, datetime="2024-06-01T00:00:00.000")
        self.mock_lib.ArtemisStartExposure.assert_called_once()

    # --- full_frame / half_frame / small_frame ---

    def test_full_frame_returns_size(self):
        result = self.camera.full_frame()
        self.assertEqual(result, [4096, 4126])
        self.mock_lib.ArtemisSubframe.assert_called_once_with(
            self.camera._handle, 0, 0, 4096, 4126)
        self.assertEqual(self.camera._subframe, [0, 0, 4096, 4126])

    def test_half_frame_returns_size(self):
        result = self.camera.half_frame()
        self.assertEqual(result, [4096, 4126 // 2])
        self.mock_lib.ArtemisSubframe.assert_called_once_with(
            self.camera._handle, 0, 4126 // 4, 4096, 4126 // 2)
        self.assertEqual(self.camera._subframe, [0, 4126 // 4, 4096, 4126 // 2])

    def test_small_frame_returns_size(self):
        result = self.camera.small_frame()
        self.assertEqual(result, [4096, 500])
        self.mock_lib.ArtemisSubframe.assert_called_once_with(
            self.camera._handle, 0, 1500, 4096, 500)
        self.assertEqual(self.camera._subframe, [0, 1500, 4096, 500])

    # --- state ---

    def test_state(self):
        self.mock_lib.ArtemisCameraState.return_value = 0
        self.assertEqual(self.camera.state, 0)

    # --- binning ---

    def test_binning_get_calls_sdk(self):
        result = self.camera.binning
        self.assertIsInstance(result, list)
        self.assertEqual(len(result), 2)
        self.mock_lib.ArtemisGetBin.assert_called_once_with(self.camera._handle, ANY, ANY)

    def test_binning_set(self):
        self.camera.binning = [2, 2]
        self.mock_lib.ArtemisBin.assert_called_once_with(self.camera._handle, 2, 2)

    # --- filter ---

    def test_filter_get_returns_zero(self):
        self.assertEqual(self.camera.filter, 0)

    def test_filter_set_is_noop(self):
        self.camera.filter = 3
        self.assertEqual(self.mock_lib.method_calls, [])  # no SDK calls

    # --- all ---

    def test_all_has_all_keys(self):
        self.mock_lib.ArtemisCameraState.return_value = 0
        result = self.camera.all
        expected_keys = {
            "ambient", "setpoint", "temperature", "cooler", "fan",
            "binning", "max_range", "xystart", "xyend",
            "xrange", "yrange", "center", "state", "description",
        }
        self.assertEqual(set(result.keys()), expected_keys)

    def test_all_xystart_reflects_subframe(self):
        self.camera._subframe = [512, 256, 4096, 500]
        self.mock_lib.ArtemisCameraState.return_value = 0
        result = self.camera.all
        # binning defaults to 0 from mock → guard gives 0; check keys exist
        self.assertIn("xystart", result)
        self.assertIn("xyend", result)

    # --- download ---

    def test_download_polls_image_ready(self):
        self.mock_lib.ArtemisImageReady.return_value = 1   # ready immediately
        self.mock_lib.ArtemisImageBuffer.return_value = 0  # null ptr → skip write
        with tempfile.TemporaryDirectory() as tmpdir:
            self.camera.download(filepath=os.path.join(tmpdir, "test.fits"))
        self.mock_lib.ArtemisImageReady.assert_called()
        self.mock_lib.ArtemisGetImageData.assert_called()
        self.mock_lib.ArtemisImageBuffer.assert_called()


if __name__ == '__main__':
    unittest.main()
+272 −0
Original line number Diff line number Diff line
import os
import tempfile
import unittest
from unittest.mock import patch, MagicMock
from unittest.mock import patch, MagicMock, ANY

from noctua.devices.stx import Camera
# Patch ctypes.CDLL so the Atik SDK is not loaded on the dev machine
with patch('ctypes.CDLL'):
    from noctua import devices


def mock_resp(text):
@@ -15,43 +17,42 @@ def mock_resp(text):
    return r


class TestSTXCameraMethods(unittest.TestCase):
    """Unit tests for stx.Camera (devices.cam)."""
# ─────────────────────────────────────────────────────────────
# STX Camera  (devices.cam)
# ─────────────────────────────────────────────────────────────

    def setUp(self):
        self.camera = Camera("http://mock-stx")
        self.camera._command_interval = 0  # skip 50ms throttle in tests
        self.camera.error = []
class TestSTXCamera(unittest.TestCase):
    """Unit tests for stx.Camera via devices.cam."""

    # --- abort ---
    def setUp(self):
        devices.cam._command_interval = 0
        devices.cam.error = []

    @patch('noctua.devices.stx.requests.get')
    def test_abort(self, mock_get):
        mock_get.return_value = mock_resp("OK\r\n")
        self.camera.abort()
        devices.cam.abort()
        args, _ = mock_get.call_args
        self.assertIn("ImagerAbortExposure", args[0])

    # --- start ---

    @patch('noctua.devices.stx.requests.get')
    def test_start_when_idle(self, mock_get):
        mock_get.side_effect = [
            mock_resp("0\r\n"),   # state → 0.0 (idle)
            mock_resp("OK\r\n"),  # StartExposure
        ]
        self.camera.start(10.0, 1)
        devices.cam.start(10.0, 1)
        self.assertEqual(mock_get.call_count, 2)
        self.assertEqual(self.camera.error, [])
        self.assertEqual(devices.cam.error, [])

    @patch('noctua.devices.stx.requests.get')
    def test_start_not_idle(self, mock_get):
        mock_get.return_value = mock_resp("2\r\n")  # state = 2 (exposing)
        self.camera.start(10.0, 1)
        # state is queried twice: once in `if self.state` and once in the log message
        devices.cam.start(10.0, 1)
        # state is queried twice: in `if self.state` and in the log message
        for call_args in mock_get.call_args_list:
            self.assertIn("ImagerState", call_args[0][0])
        self.assertIn("Camera not idle", self.camera.error)
        self.assertIn("Camera not idle", devices.cam.error)

    @patch('noctua.devices.stx.requests.get')
    def test_start_passes_datetime(self, mock_get):
@@ -59,20 +60,18 @@ class TestSTXCameraMethods(unittest.TestCase):
            mock_resp("0\r\n"),
            mock_resp("OK\r\n"),
        ]
        self.camera.start(5.0, 1, datetime="2024-06-01T00:00:00.000")
        devices.cam.start(5.0, 1, datetime="2024-06-01T00:00:00.000")
        _, kwargs = mock_get.call_args
        self.assertIn("DateTime", kwargs.get('params', ''))

    # --- full_frame / half_frame / small_frame ---

    @patch('noctua.devices.stx.requests.get')
    def test_full_frame_returns_size(self, mock_get):
        mock_get.side_effect = [
            mock_resp("4096\r\n4126\r\n"),  # GetSettings → cam_x, cam_y
            mock_resp("0\r\n"),              # state check inside set_window
            mock_resp("4096\r\n4126\r\n"),  # CameraXSize, CameraYSize
            mock_resp("0\r\n"),              # state in set_window
            mock_resp("OK\r\n"),             # SetSettings
        ]
        result = self.camera.full_frame()
        result = devices.cam.full_frame()
        self.assertEqual(result, [4096, 4126])

    @patch('noctua.devices.stx.requests.get')
@@ -82,7 +81,7 @@ class TestSTXCameraMethods(unittest.TestCase):
            mock_resp("0\r\n"),
            mock_resp("OK\r\n"),
        ]
        result = self.camera.half_frame()
        result = devices.cam.half_frame()
        self.assertEqual(result, [4096 // 2, 4126 // 2])

    @patch('noctua.devices.stx.requests.get')
@@ -92,38 +91,32 @@ class TestSTXCameraMethods(unittest.TestCase):
            mock_resp("0\r\n"),
            mock_resp("OK\r\n"),
        ]
        result = self.camera.small_frame()
        result = devices.cam.small_frame()
        self.assertEqual(result, [4096 // 10, 4126 // 10])

    # --- state ---

    @patch('noctua.devices.stx.requests.get')
    def test_state(self, mock_get):
        mock_get.return_value = mock_resp("0\r\n")
        self.assertEqual(self.camera.state, 0.0)

    # --- binning ---
        self.assertEqual(devices.cam.state, 0.0)

    @patch('noctua.devices.stx.requests.get')
    def test_binning_get(self, mock_get):
        mock_get.return_value = mock_resp("2\r\n2\r\n")
        self.assertEqual(self.camera.binning, [2, 2])
        self.assertEqual(devices.cam.binning, [2, 2])

    @patch('noctua.devices.stx.requests.get')
    def test_binning_set(self, mock_get):
        mock_get.side_effect = [
            mock_resp("0\r\n"),   # state check
            mock_resp("OK\r\n"),  # SetSettings
            mock_resp("0\r\n"),
            mock_resp("OK\r\n"),
        ]
        self.camera.binning = [2, 2]
        devices.cam.binning = [2, 2]
        self.assertEqual(mock_get.call_count, 2)

    # --- filter ---

    @patch('noctua.devices.stx.requests.get')
    def test_filter_get(self, mock_get):
        mock_get.return_value = mock_resp("3\r\n")
        self.assertEqual(self.camera.filter, 3.0)
        self.assertEqual(devices.cam.filter, 3.0)

    @patch('noctua.devices.stx.requests.get')
    def test_filter_set(self, mock_get):
@@ -131,27 +124,25 @@ class TestSTXCameraMethods(unittest.TestCase):
            mock_resp("0\r\n"),   # is_moving check
            mock_resp("OK\r\n"),  # ChangeFilter
        ]
        self.camera.filter = 2
        devices.cam.filter = 2
        self.assertEqual(mock_get.call_count, 2)

    # --- all ---

    @patch('noctua.devices.stx.requests.get')
    def test_all_has_all_keys(self, mock_get):
        # 13 values matching the ImagerGetSettings params list
        # 13 values matching ImagerGetSettings params order
        settings = (
            "20.5\r\n-10.0\r\n-5.3\r\n"   # ambient, setpoint, temp
            "1\r\n45\r\n"                   # cooler, fan
            "1\r\n1\r\n"                    # binX, binY
            "4096\r\n4126\r\n"              # camX, camY
            "0\r\n0\r\n4096\r\n4126\r\n"   # startX, startY, numX, numY
            "20.5\r\n-10.0\r\n-5.3\r\n"
            "1\r\n45\r\n"
            "1\r\n1\r\n"
            "4096\r\n4126\r\n"
            "0\r\n0\r\n4096\r\n4126\r\n"
        )
        mock_get.side_effect = [
            mock_resp(settings),
            mock_resp("0\r\n"),           # state
            mock_resp("STX-16803\r\n"),   # description
            mock_resp("0\r\n"),
            mock_resp("STX-16803\r\n"),
        ]
        result = self.camera.all
        result = devices.cam.all
        expected_keys = {
            "ambient", "setpoint", "temperature", "cooler", "fan",
            "binning", "max_range", "xystart", "xyend",
@@ -159,19 +150,123 @@ class TestSTXCameraMethods(unittest.TestCase):
        }
        self.assertEqual(set(result.keys()), expected_keys)

    # --- download ---

    @patch('noctua.devices.stx.requests.get')
    def test_download_writes_file(self, mock_get):
        fake_content = b"SIMPLE  =                    T"
        mock_get.return_value = mock_resp(fake_content)
        with tempfile.TemporaryDirectory() as tmpdir:
            filepath = os.path.join(tmpdir, "test.fits")
            self.camera.download(filepath=filepath)
            devices.cam.download(filepath=filepath)
            self.assertTrue(os.path.exists(filepath))
            with open(filepath, 'rb') as f:
                self.assertEqual(f.read(), fake_content)


# ─────────────────────────────────────────────────────────────
# Atik Camera  (devices.cam2)
# ─────────────────────────────────────────────────────────────

class TestAtikCamera(unittest.TestCase):
    """Unit tests for atik.Camera via devices.cam2."""

    def setUp(self):
        self.mock_lib = MagicMock()
        devices.cam2._lib = self.mock_lib
        devices.cam2._handle = MagicMock(name='fake_handle')  # truthy → skips _check_connection
        devices.cam2._props.nPixelsX = 4096
        devices.cam2._props.nPixelsY = 4126
        devices.cam2._subframe = [0, 0, 4096, 4126]
        devices.cam2.error = []

    def test_abort(self):
        devices.cam2.abort()
        self.mock_lib.ArtemisAbortExposure.assert_called_once_with(devices.cam2._handle)

    def test_start_light(self):
        devices.cam2.start(10.0, 1)
        self.mock_lib.ArtemisSetDarkMode.assert_called_once_with(devices.cam2._handle, False)
        self.mock_lib.ArtemisStartExposure.assert_called_once()

    def test_start_dark(self):
        devices.cam2.start(10.0, 0)
        self.mock_lib.ArtemisSetDarkMode.assert_called_once_with(devices.cam2._handle, True)

    def test_start_bias_is_dark(self):
        devices.cam2.start(0.0, 2)
        self.mock_lib.ArtemisSetDarkMode.assert_called_once_with(devices.cam2._handle, True)

    def test_start_accepts_datetime_kwarg(self):
        devices.cam2.start(5.0, 1, datetime="2024-06-01T00:00:00.000")
        self.mock_lib.ArtemisStartExposure.assert_called_once()

    def test_full_frame_returns_size(self):
        result = devices.cam2.full_frame()
        self.assertEqual(result, [4096, 4126])
        self.mock_lib.ArtemisSubframe.assert_called_once_with(
            devices.cam2._handle, 0, 0, 4096, 4126)
        self.assertEqual(devices.cam2._subframe, [0, 0, 4096, 4126])

    def test_half_frame_returns_size(self):
        result = devices.cam2.half_frame()
        self.assertEqual(result, [4096, 4126 // 2])
        self.mock_lib.ArtemisSubframe.assert_called_once_with(
            devices.cam2._handle, 0, 4126 // 4, 4096, 4126 // 2)
        self.assertEqual(devices.cam2._subframe, [0, 4126 // 4, 4096, 4126 // 2])

    def test_small_frame_returns_size(self):
        result = devices.cam2.small_frame()
        self.assertEqual(result, [4096, 500])
        self.mock_lib.ArtemisSubframe.assert_called_once_with(
            devices.cam2._handle, 0, 1500, 4096, 500)
        self.assertEqual(devices.cam2._subframe, [0, 1500, 4096, 500])

    def test_state(self):
        self.mock_lib.ArtemisCameraState.return_value = 0
        self.assertEqual(devices.cam2.state, 0)

    def test_binning_get_calls_sdk(self):
        result = devices.cam2.binning
        self.assertIsInstance(result, list)
        self.assertEqual(len(result), 2)
        self.mock_lib.ArtemisGetBin.assert_called_once_with(devices.cam2._handle, ANY, ANY)

    def test_binning_set(self):
        devices.cam2.binning = [2, 2]
        self.mock_lib.ArtemisBin.assert_called_once_with(devices.cam2._handle, 2, 2)

    def test_filter_get_returns_zero(self):
        self.assertEqual(devices.cam2.filter, 0)

    def test_filter_set_is_noop(self):
        devices.cam2.filter = 3
        self.assertEqual(self.mock_lib.method_calls, [])

    def test_all_has_all_keys(self):
        self.mock_lib.ArtemisCameraState.return_value = 0
        result = devices.cam2.all
        expected_keys = {
            "ambient", "setpoint", "temperature", "cooler", "fan",
            "binning", "max_range", "xystart", "xyend",
            "xrange", "yrange", "center", "state", "description",
        }
        self.assertEqual(set(result.keys()), expected_keys)

    def test_all_xystart_reflects_subframe(self):
        devices.cam2._subframe = [512, 256, 4096, 500]
        self.mock_lib.ArtemisCameraState.return_value = 0
        result = devices.cam2.all
        self.assertIn("xystart", result)
        self.assertIn("xyend", result)

    def test_download_polls_image_ready(self):
        self.mock_lib.ArtemisImageReady.return_value = 1
        self.mock_lib.ArtemisImageBuffer.return_value = 0  # null ptr → skip write
        with tempfile.TemporaryDirectory() as tmpdir:
            devices.cam2.download(filepath=os.path.join(tmpdir, "test.fits"))
        self.mock_lib.ArtemisImageReady.assert_called()
        self.mock_lib.ArtemisGetImageData.assert_called()
        self.mock_lib.ArtemisImageBuffer.assert_called()


if __name__ == '__main__':
    unittest.main()