Commit d91d1eb3 authored by vertighel's avatar vertighel
Browse files

dev

parent c0375db2
Loading
Loading
Loading
Loading

tests/__init__.py

deleted100644 → 0
+0 −3
Original line number Diff line number Diff line
'''
Unit tests for the package
'''

tests/test_devices_access.py

deleted100644 → 0
+0 −159
Original line number Diff line number Diff line
      
# tests/test_device_access.py
import unittest
from unittest.mock import patch, MagicMock, mock_open
import configparser # To mock config loading

# We need to ensure 'noctua.devices' can be imported.
# If 'noctua' is not yet installed, and tests are run from project root:
# Make sure the project root is in PYTHONPATH or use `python -m unittest ...`

class TestDevicePropertyAccess(unittest.TestCase):

    @patch('noctua.devices.configparser.ConfigParser')
    @patch('noctua.devices.importlib.import_module') # Patch import_module used in devices/__init__.py
    def setUp(self, mock_import_module, mock_config_parser_constructor):
        """
        Set up mock configurations to allow 'noctua.devices' to "load"
        dummy devices without real hardware or full config files.
        """
        # Mock ConfigParser instances
        self.mock_nodes_config = MagicMock(spec=configparser.ConfigParser)
        self.mock_devs_config = MagicMock(spec=configparser.ConfigParser)

        # Configure the constructor to return our mocks
        mock_config_parser_constructor.side_effect = [
            self.mock_nodes_config,  # First call to ConfigParser() in devices/__init__
            self.mock_devs_config    # Second call
        ]

        # --- Mock nodes.ini content ---
        # Simulating [DUMMY_NODE] ip=localhost port=1234
        self.mock_nodes_config.items.return_value = [('ip', 'localhost'), ('port', '1234')]

        # --- Mock devices.ini content ---
        # Simulate one dome and one camera
        self.mock_devs_config.sections.return_value = ['dom', 'cam', 'tel']
        
        # Values for test_dom
        def devs_get_dom(section, option):
            if section == 'dom':
                if option == 'module': return 'alpaca' # Or whatever module 'dom' usually is
                if option == 'class': return 'Dome'
                if option == 'node': return 'DUMMY_NODE'
            raise configparser.NoOptionError(option, section)

        # Values for test_cam
        def devs_get_cam(section, option):
            if section == 'cam':
                if option == 'module': return 'stx' # Or your camera module
                if option == 'class': return 'Camera'
                if option == 'node': return 'DUMMY_NODE'
                if option == 'outlet': return '0' # if it's a switch-like camera power
            raise configparser.NoOptionError(option, section)

        # Values for test_cam
        def devs_get_tel(section, option):
            if section == 'tel':
                if option == 'module': return 'astelco' # Or your camera module
                if option == 'class': return 'Telescope'
                if option == 'node': return 'DUMMY_NODE'
            raise configparser.NoOptionError(option, section)

        # Side effect for devs.get based on section
        def devs_get_side_effect(section, option):
            if section == 'dom':
                return devs_get_dom(section, option)
            elif section == 'cam':
                return devs_get_cam(section, option)
            elif section == 'tel':
                return devs_get_tel(section, option)
            raise configparser.NoSectionError(section)

        self.mock_devs_config.get.side_effect = devs_get_side_effect

        # --- Mock the dynamically imported modules and classes ---
        self.mock_dome_module = MagicMock()
        self.mock_dome_class = MagicMock()
        self.mock_dome_instance = MagicMock(spec_set=['azimuth', 'error']) # spec_set for strictness
        self.mock_dome_instance.azimuth = 123.45
        self.mock_dome_instance.error = []
        self.mock_dome_class.return_value = self.mock_dome_instance
        self.mock_dome_module.Dome = self.mock_dome_class # Class name must match devices.ini

        self.mock_camera_module = MagicMock()
        self.mock_camera_class = MagicMock()
        self.mock_camera_instance = MagicMock(spec_set=['binning', 'error'])
        self.mock_camera_instance.binning = [1,1]
        self.mock_camera_instance.error = []
        self.mock_camera_class.return_value = self.mock_camera_instance
        self.mock_camera_module.Camera = self.mock_camera_class

        self.mock_telescope_module = MagicMock()
        self.mock_telescope_class = MagicMock()
        self.mock_telescope_instance = MagicMock(spec_set=['altaz', 'error'])
        self.mock_telescope_instance.altaz = [1,1]
        self.mock_telescope_instance.error = []
        self.mock_telescope_class.return_value = self.mock_telescope_instance
        self.mock_telescope_module.Telescope = self.mock_telescope_class

        def import_module_side_effect(name, package=None):
            # The name will be like 'noctua.devices.alpaca'
            if 'alpaca' in name: # Or your actual dome module name
                return self.mock_dome_module
            elif 'stx' in name: # Or your actual camera module name
                return self.mock_camera_module
            elif 'astelco' in name: # Or your actual telescope module name
                return self.mock_camera_module
            raise ImportError(f"Mocked import_module cannot find {name}")
        
        mock_import_module.side_effect = import_module_side_effect
        
        # Now, import noctua.devices. This will trigger its __init__.py
        # We need to ensure that if devices is already imported, it's reloaded with mocks.
        # This can be tricky. It's often better to ensure tests run in a clean environment
        # or explicitly reload. For simplicity, we assume it's the first import or
        # the mocks effectively override.
        
        # If 'noctua.devices' might already be loaded by another test:
        import sys
        if 'noctua.devices' in sys.modules:
            del sys.modules['noctua.devices'] # Force a reload
            if 'noctua.devices.alpaca' in sys.modules: del sys.modules['noctua.devices.alpaca']
            if 'noctua.devices.stx' in sys.modules: del sys.modules['noctua.devices.stx']
            if 'noctua.devices.astelco' in sys.modules: del sys.modules['noctua.devices.astelco']
            # Add other device modules if necessary

        from noctua import devices
        self.devices = devices


    def test_dome_azimuth_access(self):
        """Test accessing devices.dom.azimuth."""
        # devices.test_dom is our mock_dome_instance
        self.assertEqual(self.devices.dom.azimuth, 123.45)
        self.assertEqual(self.devices.dom.error, [])

    def test_telescope_altaz_access(self):
        """Test accessing devices.test_dom.altaz."""
        self.assertEqual(self.devices.tel.altaz, [45.0, 180.0])
        self.assertEqual(self.devices.tel.error, [])

    def test_camera_binning_access(self):
        """Test accessing devices.test_cam.binning."""
        self.assertEqual(self.devices.cam.binning, [1,1])
        self.assertEqual(self.devices.cam.error, [])

    def test_non_existent_device_attribute_error(self):
        """Test accessing a non-existent device raises AttributeError."""
        with self.assertRaises(AttributeError):
            _ = self.devices.non_existent_device

# Note: This setUp is quite involved because it mocks the dynamic
# loading.  An alternative for simpler unit tests is to *not* rely on
# `from noctua import devices` and instead directly instantiate and
# test the device classes (e.g., `alpaca.Dome`) by mocking their
# specific dependencies (e.g., `requests.get`).  The test above is
# more of an "integration unit test" for the devices package loading.

    

tests/test_devices_alpaca.py

deleted100644 → 0
+0 −30
Original line number Diff line number Diff line
import unittest
from unittest.mock import patch, MagicMock

# Assuming your project structure allows this import when tests are run
# from the project root (e.g., python -m unittest discover)
from noctua.devices import alpaca # Or directly import specific classes like Dome, Switch
from noctua.devices.__init__ import dynamic_import # To load devices based on config for integration-like unit tests

# You'll need a way to configure device instances for testing,
# or use the dynamic import mechanism if your test environment
# has access to dummy config files.

# For pure unit tests, directly instantiate:
# MOCK_URL = "http://mock-alpaca-server"
# test_switch = alpaca.Switch(MOCK_URL, switch_id=1, device_name="TestAlpacaSwitch")

class TestAlpacaSwitch(unittest.TestCase):
    def setUp(self):
        # This method is run before each test
        self.mock_url = "http://mock-alpaca-server.test"
        # device_name is added if you adopted the BaseDevice change. If not, remove it.
        self.switch = alpaca.Switch(self.mock_url, switch_id=0, device_name="TestAlpacaSwitch0")
        # Clear any errors from previous test runs if the instance is reused (not typical in setUp)
        if hasattr(self.switch, 'error'):
            self.switch.error = []

    # ... test methods ...

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

tests/test_devices_astelco.py

deleted100644 → 0
+0 −127
Original line number Diff line number Diff line
import unittest
from unittest.mock import patch, MagicMock
import socket # For socket.timeout

from noctua.devices.astelco import Telescope # Assuming direct import

class TestAstelcoTelescope(unittest.TestCase):
    def setUp(self):
        self.mock_url = "mock-astelco-server:65432"
        # If you updated BaseDevice to take device_name:
        self.telescope = Telescope(self.mock_url)
        # If not:
        # self.telescope = Telescope(self.mock_url)
        self.telescope.error = []

    # Patch 'telnetlib.Telnet' within the 'noctua.devices.astelco' module
    @patch('noctua.devices.astelco.telnetlib.Telnet')
    def test_telescope_tracking_connected_tracking_on(self, mock_telnet_constructor):
        """Test reading tracking state when connected and tracking is ON."""
        # Mock the Telnet instance and its methods
        mock_tn_instance = MagicMock()
        mock_telnet_constructor.return_value = mock_tn_instance

        # Simulate the sequence of reads for a successful "get POINTING.TRACK"
        # 1. Initial prompt, 2. Auth OK, 3. Command OK, 4. DATA INLINE, 5. Command Complete
        mock_tn_instance.read_until.side_effect = [
            b"[ruder]\n",                            # Initial prompt
            b"AUTH OK 0 0\n",                        # Auth successful
            b"1 COMMAND OK\n",                       # Command 1 (get) OK
            b"DATA INLINE 1 POINTING.TRACK=1\n",     # Data for command 1
            b"1 COMMAND COMPLETE\n",                 # Command 1 complete
        ]

        is_tracking = self.telescope.tracking

        self.assertTrue(is_tracking)
        self.assertEqual(self.telescope.error, [])
        mock_telnet_constructor.assert_called_once_with(
            self.telescope.url.split(':')[0], # host
            int(self.telescope.url.split(':')[1]), # port
            timeout=self.telescope.timeout
        )
        # Check writes: auth and then the get command
        self.assertEqual(mock_tn_instance.write.call_count, 2)
        self.assertIn(b'auth plain "admin" "admin"\n', mock_tn_instance.write.call_args_list[0][0][0])
        self.assertIn(b'1 get POINTING.TRACK\n', mock_tn_instance.write.call_args_list[1][0][0])
        mock_tn_instance.close.assert_called_once()


    @patch('noctua.devices.astelco.telnetlib.Telnet')
    def test_telescope_tracking_connected_tracking_off(self, mock_telnet_constructor):
        """Test reading tracking state when connected and tracking is OFF."""
        mock_tn_instance = MagicMock()
        mock_telnet_constructor.return_value = mock_tn_instance
        mock_tn_instance.read_until.side_effect = [
            b"[ruder]\n",
            b"AUTH OK 0 0\n",
            b"1 COMMAND OK\n",
            b"DATA INLINE 1 POINTING.TRACK=0\n", # Tracking is 0
            b"1 COMMAND COMPLETE\n",
        ]

        is_tracking = self.telescope.tracking
        self.assertFalse(is_tracking)
        self.assertEqual(self.telescope.error, [])

    @patch('noctua.devices.astelco.telnetlib.Telnet')
    def test_telescope_tracking_connection_timeout(self, mock_telnet_constructor):
        """Test tracking state when Telnet connection times out."""
        mock_telnet_constructor.side_effect = socket.timeout("Telnet connection timed out")

        is_tracking = self.telescope.tracking

        # Your @check.telnet_errors decorator should handle this
        self.assertIsNone(is_tracking) # Astelco OpenTSI.get returns res[0] or None.
                                        # Telescope.tracking property parses this.
        self.assertTrue(len(self.telescope.error) > 0)
        self.assertIn("Server timeout!", self.telescope.error[0]) # Message from decorator

    @patch('noctua.devices.astelco.telnetlib.Telnet')
    def test_telescope_tracking_auth_fails(self, mock_telnet_constructor):
        """Test tracking state when Telnet authentication fails."""
        mock_tn_instance = MagicMock()
        mock_telnet_constructor.return_value = mock_tn_instance
        mock_tn_instance.read_until.side_effect = [
            b"[ruder]\n",
            b"AUTH FAILED\n", # Simulate auth failure
        ]

        is_tracking = self.telescope.tracking
        self.assertIsNone(is_tracking)
        self.assertTrue(len(self.telescope.error) > 0)
        # Check for an error message indicating auth failure (this depends on your OpenTSI._send error handling)
        # For instance, if OpenTSI._send adds an error like "Authentication failed"
        self.assertTrue(any("Authentication failed" in e for e in self.telescope.error))


    @patch('noctua.devices.astelco.telnetlib.Telnet')
    def test_telescope_tracking_parse_error_value(self, mock_telnet_constructor):
        """Test tracking when DATA INLINE returns an unparsable value."""
        mock_tn_instance = MagicMock()
        mock_telnet_constructor.return_value = mock_tn_instance
        mock_tn_instance.read_until.side_effect = [
            b"[ruder]\n",
            b"AUTH OK 0 0\n",
            b"1 COMMAND OK\n",
            b"DATA INLINE 1 POINTING.TRACK=GARBAGE\n", # Unparsable value
            b"1 COMMAND COMPLETE\n",
        ]

        is_tracking = self.telescope.tracking # This should result in None due to TypeError in int(res)
        self.assertIsNone(is_tracking)        
        # The error might be logged by the Telescope.tracking property
        # itself if it has a try-except, or the OpenTSI.get might log
        # it if the type conversion fails there.  If
        # Telescope.tracking has `except TypeError: self._tracking =
        # None`, then self.telescope.error might be empty unless the
        # property explicitly adds an error.  This test highlights the
        # need for error logging within property getters if parsing
        # fails.  Assuming the Telescope.tracking property in
        # astelco.py has: except TypeError as e: self._tracking =
        # None; log.warning(f"Failed to parse tracking: {res} - {e}")
        # then self.telescope.error might still be empty from the
        # perspective of the OpenTSI class methods.  If the goal is
        # that self.telescope.error *always* reflects issues, then the
        # property itself needs to populate it.
        

tests/test_devices_stx.py

deleted100644 → 0
+0 −92
Original line number Diff line number Diff line
import unittest
from unittest.mock import patch, MagicMock
import requests # For requests.exceptions

from noctua.devices.stx import Camera # Assuming direct import

class TestSTXCamera(unittest.TestCase):
    def setUp(self):
        self.mock_url = "http://mock-stx-camera.test"
        # If you updated BaseDevice to take device_name:
        self.camera = Camera(self.mock_url)
        # If not:
        # self.camera = Camera(self.mock_url)
        self.camera.error = []


    @patch('noctua.devices.stx.requests.get')
    def test_camera_temperature_connected(self, mock_requests_get):
        """Test reading camera temperature when connected."""
        mock_response = MagicMock()
        # STX Camera CGI often returns plain text, sometimes multi-line
        mock_response.text = "25.5\r\n" # Simulate temp value
        mock_response.raise_for_status.return_value = None
        mock_requests_get.return_value = mock_response

        temp = self.camera.temperature

        self.assertAlmostEqual(temp, 25.5)
        self.assertEqual(self.camera.error, [])
        expected_url_part = f"{self.mock_url}/ImagerGetSettings.cgi" # From STX.get
        mock_requests_get.assert_called_once()
        args, kwargs = mock_requests_get.call_args
        self.assertTrue(args[0].startswith(expected_url_part))
        self.assertIn("CCDTemperature", kwargs['params']) # Check if correct param is in params


    @patch('noctua.devices.stx.requests.get')
    def test_camera_temperature_network_error(self, mock_requests_get):
        """Test reading temperature during a network error."""
        mock_requests_get.side_effect = requests.exceptions.ConnectionError("STX unreachable")

        temp = self.camera.temperature

        # Your @check.request_errors on STX.get should handle this The
        # STX.get method returns the raw text split by \r\n. If it's
        # an error, the property might try to float() it and fail, or
        # STX.get returns None.  Assuming STX.get returns a value that
        # float() can't parse, or None on error.  The
        # Camera.temperature property then `round(res, 1)` If `res` is
        # None due to error, `round(None, 1)` is a TypeError.
        
        # If STX.get returns None due to decorator catching error:
        with self.assertRaises(TypeError): # Because round(None, 1)
             _ = self.camera.temperature
        # Or, if your property handles this: self.assertIsNone(temp) or similar.

        self.assertTrue(len(self.camera.error) > 0)
        self.assertIn("Connection error!", self.camera.error[0]) # Message from decorator


    @patch('noctua.devices.stx.requests.get')
    def test_camera_temperature_empty_response(self, mock_requests_get):
        """Test reading temperature with an empty or malformed response from STX."""
        mock_response = MagicMock()
        mock_response.text = "\r\n" # Empty or just newline
        mock_response.raise_for_status.return_value = None
        mock_requests_get.return_value = mock_response

        # The STX.get method processes `res.text.split("\r\n")`.  If
        # text is "\r\n", value becomes ['', '']. Then value =
        # value[:-1] -> [''] Then if len(value) == 1, value = value[0]
        # -> "" Then int("") or float("") will raise ValueError.  The
        # STX.get's try-except for int/float will return the string
        # value `""`.  Then Camera.temperature tries `round("", 1)`
        # which is a TypeError.
        with self.assertRaises(TypeError):
            _ = self.camera.temperature
        
        # self.camera.error would be empty if the ValueError in
        # STX.get is caught and returns string "" This test depends on
        # how robust the parsing in STX.get and Camera.temperature is.
        # If STX.get's try-except returns `value` as `""`, then
        # `self.camera.error` from STX.get is empty.  The TypeError
        # happens in the Camera.temperature property.  To make this
        # test pass by checking `self.camera.error`, the
        # Camera.temperature property would need its own try-except
        # that populates `self.error`.

        # Let's assume for now the property itself doesn't add to
        # self.error for TypeErrors during conversion
        # self.assertEqual(self.camera.error, []) # If STX.get itself
        # didn't see an HTTP error
Loading