From 8148627f4e8eae8e111f7605af85fbfd9b1890dd Mon Sep 17 00:00:00 2001 From: GermanHydrogen Date: Mon, 12 Feb 2024 17:58:40 +0100 Subject: [PATCH 1/8] Added unit tests to basetelescope --- pyobs/modules/telescope/basetelescope.py | 4 +- tests/modules/telescope/__init__.py | 0 tests/modules/telescope/test_basetelescope.py | 301 ++++++++++++++++++ 3 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 tests/modules/telescope/__init__.py create mode 100644 tests/modules/telescope/test_basetelescope.py diff --git a/pyobs/modules/telescope/basetelescope.py b/pyobs/modules/telescope/basetelescope.py index 98064782d..fb656223c 100644 --- a/pyobs/modules/telescope/basetelescope.py +++ b/pyobs/modules/telescope/basetelescope.py @@ -142,7 +142,7 @@ async def move_radec(self, ra: float, dec: float, **kwargs: Any) -> None: await self._change_motion_status(MotionStatus.TRACKING) # update headers now - asyncio.create_task(self._update_celestial_headers()) + await asyncio.create_task(self._update_celestial_headers()) log.info("Finished moving telescope.") @abstractmethod @@ -197,7 +197,7 @@ async def move_altaz(self, alt: float, az: float, **kwargs: Any) -> None: await self._change_motion_status(MotionStatus.POSITIONED) # update headers now - asyncio.create_task(self._update_celestial_headers()) + await asyncio.create_task(self._update_celestial_headers()) log.info("Finished moving telescope.") async def get_fits_header_before( diff --git a/tests/modules/telescope/__init__.py b/tests/modules/telescope/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/modules/telescope/test_basetelescope.py b/tests/modules/telescope/test_basetelescope.py new file mode 100644 index 000000000..d1c81673d --- /dev/null +++ b/tests/modules/telescope/test_basetelescope.py @@ -0,0 +1,301 @@ +import asyncio +import logging +from typing import Any, Tuple, Optional +from unittest.mock import Mock, AsyncMock + +import numpy as np +import pytest +import astropy.units as u +from astropy.coordinates import SkyCoord, EarthLocation + +from pyobs.comm.dummy import DummyComm +from pyobs.events import MoveAltAzEvent, MoveRaDecEvent +from pyobs.modules.telescope import BaseTelescope +from pyobs.utils.enums import MotionStatus +from pyobs.utils.time import Time + + +class TestBaseTelescope(BaseTelescope): + + async def _move_radec(self, ra: float, dec: float, abort_event: asyncio.Event) -> None: + pass + + async def _move_altaz(self, alt: float, az: float, abort_event: asyncio.Event) -> None: + pass + + async def init(self, **kwargs: Any) -> None: + pass + + async def park(self, **kwargs: Any) -> None: + pass + + async def stop_motion(self, device: Optional[str] = None, **kwargs: Any) -> None: + pass + + async def is_ready(self, **kwargs: Any) -> bool: + pass + + async def get_altaz(self, **kwargs: Any) -> Tuple[float, float]: + pass + + async def get_radec(self, **kwargs: Any) -> Tuple[float, float]: + pass + + +class MockObserver: + def moon_altaz(self, time: Time): + return SkyCoord(alt=60, az=0, unit='deg', frame='altaz') + + def sun_altaz(self, time: Time): + return SkyCoord(alt=60, az=0, unit='deg', frame='altaz') + + def moon_illumination(self, time: Time): + return 0.5 + + @property + def location(self): + return EarthLocation(lat=10. * u.degree, lon=10. * u.degree, height=100 * u.meter) + + +@pytest.mark.asyncio +async def test_update_celestial_headers_no_observer() -> None: + telescope = TestBaseTelescope() + await telescope._update_celestial_headers() + + assert telescope._celestial_headers == {} + + +def compare_telescope_headers(header, moon_alt, moon_frac, moon_dist, sun_alt, sun_dist) -> None: + assert header["MOONALT"] == (moon_alt, "Lunar altitude") + assert header["MOONFRAC"] == (moon_frac, "Fraction of the moon illuminated") + assert header["MOONDIST"] == (moon_dist, "Lunar distance from target") + assert header["SUNALT"] == (sun_alt, "Solar altitude") + assert header["SUNDIST"] == (sun_dist, "Solar Distance from Target") + + +@pytest.mark.asyncio +async def test_update_celestial_headers_no_altaz() -> None: + telescope = TestBaseTelescope() + telescope.observer = MockObserver() + + telescope.get_altaz = AsyncMock(side_effect=Exception) + + await telescope._update_celestial_headers() + + compare_telescope_headers(telescope._celestial_headers, 60, 0.5, None, 60, None) + + +@pytest.mark.asyncio +async def test_update_celestial_headers() -> None: + telescope = TestBaseTelescope() + telescope.observer = MockObserver() + + telescope.get_altaz = AsyncMock(return_value=(60, 0)) + + await telescope._update_celestial_headers() + + compare_telescope_headers(telescope._celestial_headers, 60, 0.5, 0.0, 60, 0.0) + + +@pytest.mark.asyncio +async def test_get_fits_header_before_no_altaz() -> None: + telescope = TestBaseTelescope() + + telescope.get_altaz = AsyncMock(side_effect=Exception) + + assert await telescope.get_fits_header_before() == {} + + +@pytest.mark.asyncio +async def test_get_fits_header_before_optional_headers() -> None: + telescope = TestBaseTelescope() + telescope._fits_headers = {"TEST": (1.0, "TEST")} + telescope._celestial_headers = {"CTEST": (1.0, "CTEST")} + + header = await telescope.get_fits_header_before() + + assert header["TEST"] == (1.0, "TEST") + assert header["CTEST"] == (1.0, "CTEST") + + +@pytest.mark.asyncio +async def test_get_fits_header_before_observer() -> None: + telescope = TestBaseTelescope() + telescope.observer = MockObserver() + + header = await telescope.get_fits_header_before() + + assert header["LATITUDE"][1] == "Latitude of telescope [deg N]" + assert header["LONGITUD"][1] == "Longitude of telescope [deg E]" + assert header["HEIGHT"][1] == "Altitude of telescope [m]" + + np.testing.assert_almost_equal(header["LATITUDE"][0], 10.0) + np.testing.assert_almost_equal(header["LONGITUD"][0], 10.0) + np.testing.assert_almost_equal(header["HEIGHT"][0], 100.0) + + +@pytest.mark.asyncio +async def test_get_fits_header_before_altaz() -> None: + telescope = TestBaseTelescope() + telescope.observer = MockObserver() + + telescope.get_radec = AsyncMock(return_value=(60, 0)) + telescope.get_altaz = AsyncMock(return_value=(60, 0)) + + header = await telescope.get_fits_header_before() + + assert header["TEL-RA"][1] == "Right ascension of telescope [degrees]" + assert header["TEL-DEC"][1] == "Declination of telescope [degrees]" + assert header["TEL-ALT"][1] == "Telescope altitude [degrees]" + assert header["TEL-AZ"][1] == "Telescope azimuth [degrees]" + assert header["TEL-ZD"][1] == "Telescope zenith distance [degrees]" + assert header["AIRMASS"][1] == "Airmass of observation start" + + np.testing.assert_almost_equal(header["TEL-RA"][0], 60.0) + np.testing.assert_almost_equal(header["TEL-DEC"][0], 0.0) + + np.testing.assert_almost_equal(header["TEL-ALT"][0], 60.0) + np.testing.assert_almost_equal(header["TEL-AZ"][0], 0.0) + np.testing.assert_almost_equal(header["TEL-ZD"][0], 30.0) + np.testing.assert_almost_equal(header["AIRMASS"][0], 1.1547005383792517) + + +@pytest.mark.asyncio +async def test_move_altaz_motion_status(): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.INITIALIZING) + telescope._move_altaz = AsyncMock() + + await telescope.move_altaz(60, 0) + telescope._move_altaz.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_move_altaz_min_alt(): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + with pytest.raises(ValueError): + await telescope.move_altaz(0, 0) + + +@pytest.mark.asyncio +async def test_move_altaz(): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + telescope.comm = DummyComm() + telescope.comm.send_event = AsyncMock() + + telescope._change_motion_status = AsyncMock() + telescope._move_altaz = AsyncMock() + + telescope._wait_for_motion = AsyncMock() + + await telescope.move_altaz(60.0, 0.0) + + events, _ = telescope.comm.send_event.call_args + assert isinstance(events[0], MoveAltAzEvent) and events[0].alt == 60.0 and events[0].az == 0 + + status_changes = telescope._change_motion_status.call_args_list + assert status_changes[0][0][0] == MotionStatus.SLEWING + assert status_changes[1][0][0] == MotionStatus.POSITIONED + + telescope._move_altaz.assert_awaited_with(60.0, 0.0, abort_event=telescope._abort_move) + + telescope._wait_for_motion.assert_awaited_with(telescope._abort_move) + + +@pytest.mark.asyncio +async def test_move_altaz_log(caplog): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + telescope._change_motion_status = AsyncMock() # We need to mock this, since this also logs to logging.INFO + + with caplog.at_level(logging.INFO): + await telescope.move_altaz(60.0, 0.0) + + assert caplog.messages[0] == "Moving telescope to Alt=60.00°, Az=0.00°..." + assert caplog.messages[1] == "Reached destination" + assert caplog.messages[2] == "Finished moving telescope." + + +@pytest.mark.asyncio +async def test_move_radec_motion_status(): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.INITIALIZING) + telescope._move_radec = AsyncMock() + + await telescope.move_radec(60, 0) + telescope._move_radec.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_move_radec_no_observer(): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + with pytest.raises(ValueError): + await telescope.move_radec(0, 0) + + +@pytest.mark.asyncio +async def test_move_radec_min_alt(): + telescope = TestBaseTelescope() + telescope.observer = MockObserver() + telescope.observer.altaz = Mock(return_value=SkyCoord(alt=0.0, az=0.0, unit="deg", frame="altaz")) + + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + with pytest.raises(ValueError): + await telescope.move_radec(0, 0) + + + +@pytest.mark.asyncio +async def test_move_radec(): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + telescope.observer = MockObserver() + telescope.observer.altaz = Mock(return_value=SkyCoord(alt=60.0, az=0.0, unit="deg", frame="altaz")) + + telescope.comm = DummyComm() + telescope.comm.send_event = AsyncMock() + + telescope._change_motion_status = AsyncMock() + telescope._move_radec = AsyncMock() + + telescope._wait_for_motion = AsyncMock() + + await telescope.move_radec(60.0, 0.0) + + events, _ = telescope.comm.send_event.call_args + assert isinstance(events[0], MoveRaDecEvent) and events[0].ra == 60.0 and events[0].dec == 0 + + status_changes = telescope._change_motion_status.call_args_list + assert status_changes[0][0][0] == MotionStatus.SLEWING + assert status_changes[1][0][0] == MotionStatus.TRACKING + + telescope._move_radec.assert_awaited_with(60.0, 0.0, abort_event=telescope._abort_move) + + telescope._wait_for_motion.assert_awaited_with(telescope._abort_move) + + +@pytest.mark.asyncio +async def test_move_radec_log(caplog): + telescope = TestBaseTelescope() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + + telescope.observer = MockObserver() + telescope.observer.altaz = Mock(return_value=SkyCoord(alt=60.0, az=0.0, unit="deg", frame="altaz")) + + telescope._change_motion_status = AsyncMock() # We need to mock this, since this also logs to logging.INFO + + with caplog.at_level(logging.INFO): + await telescope.move_radec(60.0, 0.0) + + assert caplog.messages[0] == "Moving telescope to RA=04:00:00 (60.00000°), Dec=00:00:00 (0.00000°)..." + assert caplog.messages[1] == "Reached destination" + assert caplog.messages[2] == "Finished moving telescope." From 25904ec5b4056e6e221059f0a2b6b8c8df1995f7 Mon Sep 17 00:00:00 2001 From: GermanHydrogen Date: Tue, 13 Feb 2024 08:58:53 +0100 Subject: [PATCH 2/8] Added basic unit tests to dummy telescope --- pyobs/modules/telescope/dummytelescope.py | 6 +- tests/modules/telescope/test_basetelescope.py | 1 - .../modules/telescope/test_dummytelescope.py | 149 ++++++++++++++++++ 3 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 tests/modules/telescope/test_dummytelescope.py diff --git a/pyobs/modules/telescope/dummytelescope.py b/pyobs/modules/telescope/dummytelescope.py index d4b6c1705..bb9acfab4 100644 --- a/pyobs/modules/telescope/dummytelescope.py +++ b/pyobs/modules/telescope/dummytelescope.py @@ -75,7 +75,7 @@ async def _move_radec(self, ra: float, dec: float, abort_event: asyncio.Event) - """ # start slewing - await self.__move(ra, dec, abort_event) + await self._move(ra, dec, abort_event) async def _move_altaz(self, alt: float, az: float, abort_event: asyncio.Event) -> None: """Actually moves to given coordinates. Must be implemented by derived classes. @@ -96,9 +96,9 @@ async def _move_altaz(self, alt: float, az: float, abort_event: asyncio.Event) - icrs = coords.icrs # start slewing - await self.__move(icrs.ra.degree, icrs.dec.degree, abort_event) + await self._move(icrs.ra.degree, icrs.dec.degree, abort_event) - async def __move(self, ra: float, dec: float, abort_event: asyncio.Event) -> None: + async def _move(self, ra: float, dec: float, abort_event: asyncio.Event) -> None: """Simulate move. Args: diff --git a/tests/modules/telescope/test_basetelescope.py b/tests/modules/telescope/test_basetelescope.py index d1c81673d..2e1f9b3fe 100644 --- a/tests/modules/telescope/test_basetelescope.py +++ b/tests/modules/telescope/test_basetelescope.py @@ -252,7 +252,6 @@ async def test_move_radec_min_alt(): await telescope.move_radec(0, 0) - @pytest.mark.asyncio async def test_move_radec(): telescope = TestBaseTelescope() diff --git a/tests/modules/telescope/test_dummytelescope.py b/tests/modules/telescope/test_dummytelescope.py new file mode 100644 index 000000000..a680eb2f6 --- /dev/null +++ b/tests/modules/telescope/test_dummytelescope.py @@ -0,0 +1,149 @@ +import asyncio +from unittest.mock import AsyncMock, Mock + +import pytest +from astropy.coordinates import EarthLocation, SkyCoord + +import astropy.units as u +from pyobs.comm.dummy import DummyComm +from pyobs.events import FilterChangedEvent, OffsetsRaDecEvent +from pyobs.modules.telescope import DummyTelescope, BaseTelescope +from pyobs.utils.enums import MotionStatus +from pyobs.utils.time import Time + + +@pytest.mark.asyncio +async def test_open(): + BaseTelescope.open = AsyncMock() + + telescope = DummyTelescope() + telescope._change_motion_status = AsyncMock() + telescope.comm = DummyComm() + telescope.comm.register_event = AsyncMock() + + await telescope.open() + + events = telescope.comm.register_event.call_args_list + assert events[0][0][0] == FilterChangedEvent + assert events[1][0][0] == OffsetsRaDecEvent + + telescope._change_motion_status.assert_awaited_once_with(MotionStatus.IDLE) + + +@pytest.mark.asyncio +async def test_move_radec(): + telescope = DummyTelescope() + telescope._move = AsyncMock() + + abort_event = asyncio.Event() + await telescope._move_radec(60, 0, abort_event) + + telescope._move.assert_awaited_once_with(60, 0, abort_event) + + +@pytest.mark.asyncio +async def test_move_altaz(mocker): + mocker.patch("pyobs.utils.time.Time.now", return_value=Time("2010-01-01T00:00:00", format="isot")) + + telescope = DummyTelescope() + telescope.location = EarthLocation(lat=10. * u.degree, lon=10. * u.degree, height=100 * u.meter) + telescope._move = AsyncMock() + + abort_event = asyncio.Event() + await telescope._move_altaz(60, 0, abort_event) + + telescope._move.assert_awaited_with(110.35706910444917, 40.020387331332806, abort_event) + + +@pytest.mark.asyncio +async def test_set_focus_invalid(): + telescope = DummyTelescope() + + with pytest.raises(ValueError): + await telescope.set_focus(-1) + + with pytest.raises(ValueError): + await telescope.set_focus(101) + + +@pytest.mark.asyncio +async def test_set_focus(mocker): + mocker.patch("asyncio.sleep") + telescope = DummyTelescope() + + await telescope.set_focus(60.0) + assert telescope._telescope.focus == 60.0 + + +@pytest.mark.asyncio +async def test_set_filter_invalid_filter(): + telescope = DummyTelescope() + + telescope._telescope.filters = ["clear"] + + with pytest.raises(ValueError): + await telescope.set_filter("B") + + +@pytest.mark.asyncio +async def test_set_filter(mocker): + mocker.patch("asyncio.sleep") + telescope = DummyTelescope() + + telescope._telescope.filters = ["clear", "B"] + telescope._telescope.filter_name = "clear" + + await telescope.set_filter("B") + + assert telescope._telescope.filter_name == "B" + + +@pytest.mark.asyncio +async def test_init(mocker): + mocker.patch("asyncio.sleep") + telescope = DummyTelescope() + telescope._change_motion_status = AsyncMock() + + await telescope.init() + + status = telescope._change_motion_status.call_args_list + assert status[0][0][0] == MotionStatus.INITIALIZING + assert status[1][0][0] == MotionStatus.IDLE + + +@pytest.mark.asyncio +async def test_park(mocker): + mocker.patch("asyncio.sleep") + telescope = DummyTelescope() + telescope._change_motion_status = AsyncMock() + + await telescope.park() + + status = telescope._change_motion_status.call_args_list + assert status[0][0][0] == MotionStatus.PARKING + assert status[1][0][0] == MotionStatus.PARKED + + +@pytest.mark.asyncio +async def test_set_offsets_radec(): + telescope = DummyTelescope() + telescope.comm = DummyComm() + telescope.comm.send_event = AsyncMock() + telescope._telescope.set_offsets = Mock() + + await telescope.set_offsets_radec(1, 1) + + telescope._telescope.set_offsets.assert_called_once_with(1, 1) + event, _ = telescope.comm.send_event.call_args + assert isinstance(event[0], OffsetsRaDecEvent) and event[0].ra == 1 and event[0].dec == 1 + + +@pytest.mark.asyncio +async def test_get_fits_header_before(): + BaseTelescope._get_fits_header_before = AsyncMock(return_value={}) + + telescope = DummyTelescope() + telescope._telescope.focus = 10.0 + + result = await telescope.get_fits_header_before(sender="sender") + assert result["TEL-FOCU"] == (10.0, "Focus position [mm]") From 64cddd4c6ff2e17b025b7f1439b4a909133d97af Mon Sep 17 00:00:00 2001 From: germanhydrogen Date: Mon, 19 Feb 2024 11:03:31 +0100 Subject: [PATCH 3/8] Refactored set focus for dummy telescope --- pyobs/modules/telescope/dummytelescope.py | 36 ++++++++++------ .../modules/telescope/test_dummytelescope.py | 43 ++++++++++++++----- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/pyobs/modules/telescope/dummytelescope.py b/pyobs/modules/telescope/dummytelescope.py index bb9acfab4..1b58a000d 100644 --- a/pyobs/modules/telescope/dummytelescope.py +++ b/pyobs/modules/telescope/dummytelescope.py @@ -139,20 +139,28 @@ async def set_focus(self, focus: float, **kwargs: Any) -> None: # acquire lock async with LockWithAbort(self._lock_focus, self._abort_focus): - log.info("Setting focus to %.2f..." % focus) - await self._change_motion_status(MotionStatus.SLEWING, interface="IFocuser") - ifoc = self._telescope.focus * 1.0 - dfoc = (focus - ifoc) / 300.0 - for i in range(300): - # abort? - if self._abort_focus.is_set(): - raise InterruptedError("Setting focus was interrupted.") - - # move focus and sleep a little - self._telescope.focus = ifoc + i * dfoc - await asyncio.sleep(0.01) - await self._change_motion_status(MotionStatus.POSITIONED, interface="IFocuser") - self._telescope.focus = focus + await self._set_focus(focus) + + async def _set_focus(self, focus: float) -> None: + log.info("Setting focus to %.2f..." % focus) + await self._change_motion_status(MotionStatus.SLEWING, interface="IFocuser") + + focus_steps = 300 + initial_focus = self._telescope.focus * 1.0 + focus_step_size = (focus - initial_focus) / focus_steps + for i in range(focus_steps): + await self._step_focus(focus_step_size) + + await self._change_motion_status(MotionStatus.POSITIONED, interface="IFocuser") + self._telescope.focus = focus + + async def _step_focus(self, step: float) -> None: + if self._abort_focus.is_set(): + raise InterruptedError("Setting focus was interrupted.") + + # move focus and sleep a little + self._telescope.focus += step + await asyncio.sleep(0.01) async def list_filters(self, **kwargs: Any) -> List[str]: """List available filters. diff --git a/tests/modules/telescope/test_dummytelescope.py b/tests/modules/telescope/test_dummytelescope.py index a680eb2f6..a692d1f3c 100644 --- a/tests/modules/telescope/test_dummytelescope.py +++ b/tests/modules/telescope/test_dummytelescope.py @@ -13,7 +13,7 @@ @pytest.mark.asyncio -async def test_open(): +async def test_open() -> None: BaseTelescope.open = AsyncMock() telescope = DummyTelescope() @@ -31,7 +31,7 @@ async def test_open(): @pytest.mark.asyncio -async def test_move_radec(): +async def test_move_radec() -> None: telescope = DummyTelescope() telescope._move = AsyncMock() @@ -42,7 +42,7 @@ async def test_move_radec(): @pytest.mark.asyncio -async def test_move_altaz(mocker): +async def test_move_altaz(mocker) -> None: mocker.patch("pyobs.utils.time.Time.now", return_value=Time("2010-01-01T00:00:00", format="isot")) telescope = DummyTelescope() @@ -56,7 +56,7 @@ async def test_move_altaz(mocker): @pytest.mark.asyncio -async def test_set_focus_invalid(): +async def test_set_focus_invalid() -> None: telescope = DummyTelescope() with pytest.raises(ValueError): @@ -67,7 +67,7 @@ async def test_set_focus_invalid(): @pytest.mark.asyncio -async def test_set_focus(mocker): +async def test_set_focus(mocker) -> None: mocker.patch("asyncio.sleep") telescope = DummyTelescope() @@ -76,7 +76,28 @@ async def test_set_focus(mocker): @pytest.mark.asyncio -async def test_set_filter_invalid_filter(): +async def test_stop_focus(mocker) -> None: + mocker.patch("asyncio.sleep") + telescope = DummyTelescope() + telescope._telescope.focus = 10.0 + await telescope._step_focus(10.0) + + assert telescope._telescope.focus == 20.0 + asyncio.sleep.assert_called_once_with(0.01) + + +@pytest.mark.asyncio +async def test_stop_focus_abord(mocker) -> None: + mocker.patch("asyncio.sleep") + telescope = DummyTelescope() + telescope._telescope.focus = 10.0 + telescope._abort_focus.set() + with pytest.raises(InterruptedError): + await telescope._step_focus(10.0) + + +@pytest.mark.asyncio +async def test_set_filter_invalid_filter() -> None: telescope = DummyTelescope() telescope._telescope.filters = ["clear"] @@ -86,7 +107,7 @@ async def test_set_filter_invalid_filter(): @pytest.mark.asyncio -async def test_set_filter(mocker): +async def test_set_filter(mocker) -> None: mocker.patch("asyncio.sleep") telescope = DummyTelescope() @@ -99,7 +120,7 @@ async def test_set_filter(mocker): @pytest.mark.asyncio -async def test_init(mocker): +async def test_init(mocker) -> None: mocker.patch("asyncio.sleep") telescope = DummyTelescope() telescope._change_motion_status = AsyncMock() @@ -112,7 +133,7 @@ async def test_init(mocker): @pytest.mark.asyncio -async def test_park(mocker): +async def test_park(mocker) -> None: mocker.patch("asyncio.sleep") telescope = DummyTelescope() telescope._change_motion_status = AsyncMock() @@ -125,7 +146,7 @@ async def test_park(mocker): @pytest.mark.asyncio -async def test_set_offsets_radec(): +async def test_set_offsets_radec() -> None: telescope = DummyTelescope() telescope.comm = DummyComm() telescope.comm.send_event = AsyncMock() @@ -139,7 +160,7 @@ async def test_set_offsets_radec(): @pytest.mark.asyncio -async def test_get_fits_header_before(): +async def test_get_fits_header_before() -> None: BaseTelescope._get_fits_header_before = AsyncMock(return_value={}) telescope = DummyTelescope() From 5321f030727bc1b3a1bdaa43773f1d039f2ac13e Mon Sep 17 00:00:00 2001 From: germanhydrogen Date: Mon, 19 Feb 2024 11:26:18 +0100 Subject: [PATCH 4/8] Removed calculation loop of celestial headers --- pyobs/modules/telescope/basetelescope.py | 36 +++++-------------- tests/modules/telescope/test_basetelescope.py | 28 ++++++++------- 2 files changed, 24 insertions(+), 40 deletions(-) diff --git a/pyobs/modules/telescope/basetelescope.py b/pyobs/modules/telescope/basetelescope.py index fb656223c..242e3b4fb 100644 --- a/pyobs/modules/telescope/basetelescope.py +++ b/pyobs/modules/telescope/basetelescope.py @@ -49,12 +49,6 @@ def __init__( self._lock_moving = asyncio.Lock() self._abort_move = asyncio.Event() - # celestial status - self._celestial_headers: Dict[str, Any] = {} - - # add thread func - self.add_background_task(self._celestial, True) - # init mixins WeatherAwareMixin.__init__(self, **kwargs) MotionStatusMixin.__init__(self, **kwargs) @@ -142,7 +136,7 @@ async def move_radec(self, ra: float, dec: float, **kwargs: Any) -> None: await self._change_motion_status(MotionStatus.TRACKING) # update headers now - await asyncio.create_task(self._update_celestial_headers()) + await asyncio.create_task(self._calc_celestial_headers()) log.info("Finished moving telescope.") @abstractmethod @@ -196,8 +190,7 @@ async def move_altaz(self, alt: float, az: float, **kwargs: Any) -> None: # finish slewing await self._change_motion_status(MotionStatus.POSITIONED) - # update headers now - await asyncio.create_task(self._update_celestial_headers()) + await asyncio.create_task(self._calc_celestial_headers()) # log.info("Finished moving telescope.") async def get_fits_header_before( @@ -252,27 +245,14 @@ async def get_fits_header_before( hdr[key] = tuple(value) # add celestial headers - for key, value in self._celestial_headers.items(): + celestial_headers = await self._calc_celestial_headers() + for key, value in celestial_headers.items(): hdr[key] = tuple(value) # finish return hdr - async def _celestial(self) -> None: - """Thread for continuously calculating positions and distances to celestial objects like moon and sun.""" - - # wait a little - await asyncio.sleep(10) - - # run until closing - while True: - # update headers - await self._update_celestial_headers() - - # sleep a little - await asyncio.sleep(30) - - async def _update_celestial_headers(self) -> None: + async def _calc_celestial_headers(self) -> Dict[str, Tuple[Optional[float], str]]: """Calculate positions and distances to celestial objects like moon and sun.""" # get now now = Time.now() @@ -281,7 +261,7 @@ async def _update_celestial_headers(self) -> None: # no observer? if self.observer is None: - return + return {} # get telescope alt/az try: @@ -300,7 +280,7 @@ async def _update_celestial_headers(self) -> None: sun_dist = tel_altaz.separation(sun_altaz) if tel_altaz is not None else None # store it - self._celestial_headers = { + celestial_headers = { "MOONALT": (float(moon_altaz.alt.degree), "Lunar altitude"), "MOONFRAC": (float(moon_frac), "Fraction of the moon illuminated"), "MOONDIST": (None if moon_dist is None else float(moon_dist.degree), "Lunar distance from target"), @@ -308,5 +288,7 @@ async def _update_celestial_headers(self) -> None: "SUNDIST": (None if sun_dist is None else float(sun_dist.degree), "Solar Distance from Target"), } + return celestial_headers + __all__ = ["BaseTelescope"] diff --git a/tests/modules/telescope/test_basetelescope.py b/tests/modules/telescope/test_basetelescope.py index 2e1f9b3fe..2112ca344 100644 --- a/tests/modules/telescope/test_basetelescope.py +++ b/tests/modules/telescope/test_basetelescope.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Any, Tuple, Optional +from typing import Any, Tuple, Optional, Dict from unittest.mock import Mock, AsyncMock import numpy as np @@ -43,29 +43,31 @@ async def get_radec(self, **kwargs: Any) -> Tuple[float, float]: class MockObserver: - def moon_altaz(self, time: Time): + def moon_altaz(self, time: Time) -> SkyCoord: return SkyCoord(alt=60, az=0, unit='deg', frame='altaz') - def sun_altaz(self, time: Time): + def sun_altaz(self, time: Time) -> SkyCoord: return SkyCoord(alt=60, az=0, unit='deg', frame='altaz') - def moon_illumination(self, time: Time): + def moon_illumination(self, time: Time) -> float: return 0.5 @property - def location(self): + def location(self) -> EarthLocation: return EarthLocation(lat=10. * u.degree, lon=10. * u.degree, height=100 * u.meter) @pytest.mark.asyncio async def test_update_celestial_headers_no_observer() -> None: telescope = TestBaseTelescope() - await telescope._update_celestial_headers() + celestial_headers = await telescope._calc_celestial_headers() - assert telescope._celestial_headers == {} + assert celestial_headers == {} -def compare_telescope_headers(header, moon_alt, moon_frac, moon_dist, sun_alt, sun_dist) -> None: +def compare_telescope_headers(header: Dict[str, Tuple[Optional[float], str]], + moon_alt: Optional[float], moon_frac: Optional[float], moon_dist: Optional[float], + sun_alt: Optional[float], sun_dist: Optional[float]) -> None: assert header["MOONALT"] == (moon_alt, "Lunar altitude") assert header["MOONFRAC"] == (moon_frac, "Fraction of the moon illuminated") assert header["MOONDIST"] == (moon_dist, "Lunar distance from target") @@ -80,9 +82,9 @@ async def test_update_celestial_headers_no_altaz() -> None: telescope.get_altaz = AsyncMock(side_effect=Exception) - await telescope._update_celestial_headers() + celestial_headers = await telescope._calc_celestial_headers() - compare_telescope_headers(telescope._celestial_headers, 60, 0.5, None, 60, None) + compare_telescope_headers(celestial_headers, 60, 0.5, None, 60, None) @pytest.mark.asyncio @@ -92,9 +94,9 @@ async def test_update_celestial_headers() -> None: telescope.get_altaz = AsyncMock(return_value=(60, 0)) - await telescope._update_celestial_headers() + celestial_headers = await telescope._calc_celestial_headers() - compare_telescope_headers(telescope._celestial_headers, 60, 0.5, 0.0, 60, 0.0) + compare_telescope_headers(celestial_headers, 60, 0.5, 0.0, 60, 0.0) @pytest.mark.asyncio @@ -110,7 +112,7 @@ async def test_get_fits_header_before_no_altaz() -> None: async def test_get_fits_header_before_optional_headers() -> None: telescope = TestBaseTelescope() telescope._fits_headers = {"TEST": (1.0, "TEST")} - telescope._celestial_headers = {"CTEST": (1.0, "CTEST")} + telescope._calc_celestial_headers = AsyncMock(return_value={"CTEST": (1.0, "CTEST")}) header = await telescope.get_fits_header_before() From abea13ac9e97c610a3a5199fa7a896fb71a46d94 Mon Sep 17 00:00:00 2001 From: GermanHydrogen Date: Tue, 19 Mar 2024 14:40:49 +0100 Subject: [PATCH 5/8] Added test for open basetelescope --- pyobs/modules/telescope/basetelescope.py | 18 ++++++------------ tests/modules/telescope/test_basetelescope.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pyobs/modules/telescope/basetelescope.py b/pyobs/modules/telescope/basetelescope.py index fb656223c..96d520873 100644 --- a/pyobs/modules/telescope/basetelescope.py +++ b/pyobs/modules/telescope/basetelescope.py @@ -26,11 +26,11 @@ class BaseTelescope( __module__ = "pyobs.modules.telescope" def __init__( - self, - fits_headers: Optional[Dict[str, Any]] = None, - min_altitude: float = 10, - wait_for_dome: Optional[str] = None, - **kwargs: Any, + self, + fits_headers: Optional[Dict[str, Any]] = None, + min_altitude: float = 10, + wait_for_dome: Optional[str] = None, + **kwargs: Any, ): """Initialize a new base telescope. @@ -88,7 +88,6 @@ async def _move_radec(self, ra: float, dec: float, abort_event: asyncio.Event) - Raises: MoveError: If telescope cannot be moved. """ - ... @timeout(1200) async def move_radec(self, ra: float, dec: float, **kwargs: Any) -> None: @@ -157,7 +156,6 @@ async def _move_altaz(self, alt: float, az: float, abort_event: asyncio.Event) - Raises: MoveError: If telescope cannot be moved. """ - ... @timeout(1200) async def move_altaz(self, alt: float, az: float, **kwargs: Any) -> None: @@ -201,7 +199,7 @@ async def move_altaz(self, alt: float, az: float, **kwargs: Any) -> None: log.info("Finished moving telescope.") async def get_fits_header_before( - self, namespaces: Optional[List[str]] = None, **kwargs: Any + self, namespaces: Optional[List[str]] = None, **kwargs: Any ) -> Dict[str, Tuple[Any, str]]: """Returns FITS header for the current status of this module. @@ -261,15 +259,11 @@ async def get_fits_header_before( async def _celestial(self) -> None: """Thread for continuously calculating positions and distances to celestial objects like moon and sun.""" - # wait a little await asyncio.sleep(10) # run until closing while True: - # update headers await self._update_celestial_headers() - - # sleep a little await asyncio.sleep(30) async def _update_celestial_headers(self) -> None: diff --git a/tests/modules/telescope/test_basetelescope.py b/tests/modules/telescope/test_basetelescope.py index 2e1f9b3fe..810e9f9e5 100644 --- a/tests/modules/telescope/test_basetelescope.py +++ b/tests/modules/telescope/test_basetelescope.py @@ -8,6 +8,7 @@ import astropy.units as u from astropy.coordinates import SkyCoord, EarthLocation +import pyobs from pyobs.comm.dummy import DummyComm from pyobs.events import MoveAltAzEvent, MoveRaDecEvent from pyobs.modules.telescope import BaseTelescope @@ -57,6 +58,20 @@ def location(self): return EarthLocation(lat=10. * u.degree, lon=10. * u.degree, height=100 * u.meter) +@pytest.mark.asyncio +async def test_open(mocker): + mocker.patch("pyobs.mixins.WeatherAwareMixin.open") + mocker.patch("pyobs.mixins.MotionStatusMixin.open") + mocker.patch("pyobs.modules.Module.open") + + telescope = TestBaseTelescope() + await telescope.open() + + pyobs.mixins.WeatherAwareMixin.open.assert_called_once_with(telescope) + pyobs.mixins.MotionStatusMixin.open.assert_called_once_with(telescope) + pyobs.modules.Module.open.assert_called_once_with(telescope) + + @pytest.mark.asyncio async def test_update_celestial_headers_no_observer() -> None: telescope = TestBaseTelescope() From 1f88f01f709a709ff92da0064fb5e0993596b2e5 Mon Sep 17 00:00:00 2001 From: GermanHydrogen Date: Tue, 19 Mar 2024 15:04:04 +0100 Subject: [PATCH 6/8] Added missing tests --- pyobs/modules/telescope/dummytelescope.py | 1 - .../modules/telescope/test_dummytelescope.py | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pyobs/modules/telescope/dummytelescope.py b/pyobs/modules/telescope/dummytelescope.py index 1b58a000d..cf4a6fdb3 100644 --- a/pyobs/modules/telescope/dummytelescope.py +++ b/pyobs/modules/telescope/dummytelescope.py @@ -305,7 +305,6 @@ async def stop_motion(self, device: Optional[str] = None, **kwargs: Any) -> None Args: device: Name of device to stop, or None for all. """ - pass async def get_focus_offset(self, **kwargs: Any) -> float: """Return current focus offset. diff --git a/tests/modules/telescope/test_dummytelescope.py b/tests/modules/telescope/test_dummytelescope.py index a692d1f3c..d363ace55 100644 --- a/tests/modules/telescope/test_dummytelescope.py +++ b/tests/modules/telescope/test_dummytelescope.py @@ -54,6 +54,12 @@ async def test_move_altaz(mocker) -> None: telescope._move.assert_awaited_with(110.35706910444917, 40.020387331332806, abort_event) +@pytest.mark.asyncio +async def test_get_focus() -> None: + telescope = DummyTelescope() + telescope._telescope.focus = 10.0 + + assert await telescope.get_focus() == 10.0 @pytest.mark.asyncio async def test_set_focus_invalid() -> None: @@ -96,6 +102,22 @@ async def test_stop_focus_abord(mocker) -> None: await telescope._step_focus(10.0) +@pytest.mark.asyncio +async def test_list_filters() -> None: + telescope = DummyTelescope() + telescope._telescope.filters = ["clear"] + + assert await telescope.list_filters() == ["clear"] + + +@pytest.mark.asyncio +async def test_get_filter() -> None: + telescope = DummyTelescope() + telescope._telescope.filter_name = "not_clear" + + assert await telescope.get_filter() == "not_clear" + + @pytest.mark.asyncio async def test_set_filter_invalid_filter() -> None: telescope = DummyTelescope() @@ -145,6 +167,14 @@ async def test_park(mocker) -> None: assert status[1][0][0] == MotionStatus.PARKED +@pytest.mark.asyncio +async def test_get_offsets_radec() -> None: + telescope = DummyTelescope() + telescope._telescope._offsets = (10.0, 0.0) + + assert await telescope.get_offsets_radec() == (10.0, 0.0) + + @pytest.mark.asyncio async def test_set_offsets_radec() -> None: telescope = DummyTelescope() @@ -168,3 +198,15 @@ async def test_get_fits_header_before() -> None: result = await telescope.get_fits_header_before(sender="sender") assert result["TEL-FOCU"] == (10.0, "Focus position [mm]") + + +@pytest.mark.asyncio +async def test_get_focus_offset() -> None: + telescope = DummyTelescope() + assert await telescope.get_focus_offset() == 0 + + +@pytest.mark.asyncio +async def test_is_ready() -> None: + telescope = DummyTelescope() + assert await telescope.is_ready() is True From b2afacded2653c6b7652f759fb8b6a2627ed6ccb Mon Sep 17 00:00:00 2001 From: GermanHydrogen Date: Tue, 7 May 2024 09:15:46 +0200 Subject: [PATCH 7/8] Refactored get fits header before --- pyobs/modules/telescope/basetelescope.py | 76 ++++++++-------- tests/modules/telescope/test_basetelescope.py | 87 ++++++++++--------- 2 files changed, 81 insertions(+), 82 deletions(-) diff --git a/pyobs/modules/telescope/basetelescope.py b/pyobs/modules/telescope/basetelescope.py index 3c7fb2a42..b5126e685 100644 --- a/pyobs/modules/telescope/basetelescope.py +++ b/pyobs/modules/telescope/basetelescope.py @@ -206,67 +206,65 @@ async def get_fits_header_before( # define base header hdr: Dict[str, Union[Any, Tuple[Any, str]]] = {} - # positions + await self._add_coordinate_header(hdr) + await self._add_side_information_header(hdr) + + self._copy_header(self._fits_headers, hdr) + + celestial_headers = await self._calc_celestial_headers() + self._copy_header(celestial_headers, hdr) + + return hdr + + async def _add_coordinate_header(self, header: Dict[str, Union[Any, Tuple[Any, str]]]) -> None: try: ra, dec = await self.get_radec() coords_ra_dec = SkyCoord(ra=ra * u.deg, dec=dec * u.deg, frame=ICRS) alt, az = await self.get_altaz() coords_alt_az = SkyCoord(alt=alt * u.deg, az=az * u.deg, frame=AltAz) - except Exception as e: log.warning("Could not fetch telescope position: %s", e) - coords_ra_dec, coords_alt_az = None, None - - # set coordinate headers - if coords_ra_dec is not None: - hdr["TEL-RA"] = (float(coords_ra_dec.ra.degree), "Right ascension of telescope [degrees]") - hdr["TEL-DEC"] = (float(coords_ra_dec.dec.degree), "Declination of telescope [degrees]") - if coords_alt_az is not None: - hdr["TEL-ALT"] = (float(coords_alt_az.alt.degree), "Telescope altitude [degrees]") - hdr["TEL-AZ"] = (float(coords_alt_az.az.degree), "Telescope azimuth [degrees]") - hdr["TEL-ZD"] = (90.0 - hdr["TEL-ALT"][0], "Telescope zenith distance [degrees]") - hdr["AIRMASS"] = (float(coords_alt_az.secz.value), "Airmass of observation start") - - # convert to sexagesimal - if coords_ra_dec is not None: - hdr["RA"] = (str(coords_ra_dec.ra.to_string(sep=":", unit=u.hour, pad=True)), "Right ascension of object") - hdr["DEC"] = (str(coords_ra_dec.dec.to_string(sep=":", unit=u.deg, pad=True)), "Declination of object") - - # site location - if self.observer is not None: - hdr["LATITUDE"] = (float(self.observer.location.lat.degree), "Latitude of telescope [deg N]") - hdr["LONGITUD"] = (float(self.observer.location.lon.degree), "Longitude of telescope [deg E]") - hdr["HEIGHT"] = (float(self.observer.location.height.value), "Altitude of telescope [m]") - - # add static fits headers - for key, value in self._fits_headers.items(): - hdr[key] = tuple(value) - - # add celestial headers - celestial_headers = await self._calc_celestial_headers() - for key, value in celestial_headers.items(): - hdr[key] = tuple(value) + return - # finish - return hdr + header["TEL-RA"] = (float(coords_ra_dec.ra.degree), "Right ascension of telescope [degrees]") + header["TEL-DEC"] = (float(coords_ra_dec.dec.degree), "Declination of telescope [degrees]") + + header["TEL-ALT"] = (float(coords_alt_az.alt.degree), "Telescope altitude [degrees]") + header["TEL-AZ"] = (float(coords_alt_az.az.degree), "Telescope azimuth [degrees]") + header["TEL-ZD"] = (90.0 - header["TEL-ALT"][0], "Telescope zenith distance [degrees]") + header["AIRMASS"] = (float(coords_alt_az.secz.value), "Airmass of observation start") + + header["RA"] = (str(coords_ra_dec.ra.to_string(sep=":", unit=u.hour, pad=True)), "Right ascension of object") + header["DEC"] = (str(coords_ra_dec.dec.to_string(sep=":", unit=u.deg, pad=True)), "Declination of object") + + async def _add_side_information_header(self, header: Dict[str, Union[Any, Tuple[Any, str]]]) -> None: + if self.observer is None: + return + + header["LATITUDE"] = (float(self.observer.location.lat.degree), "Latitude of telescope [deg N]") + header["LONGITUD"] = (float(self.observer.location.lon.degree), "Longitude of telescope [deg E]") + header["HEIGHT"] = (float(self.observer.location.height.value), "Altitude of telescope [m]") + + @staticmethod + def _copy_header(source: Dict[str, Any], target: Dict[str, Union[Any, Tuple[Any, str]]]) -> None: + for key, value in source.items(): + target[key] = tuple(value) async def _calc_celestial_headers(self) -> Dict[str, Tuple[Optional[float], str]]: """Calculate positions and distances to celestial objects like moon and sun.""" # get now now = Time.now() - alt: Optional[float] - az: Optional[float] # no observer? if self.observer is None: return {} - # get telescope alt/az + tel_altaz: Optional[SkyCoord] = None try: alt, az = await self.get_altaz() tel_altaz = SkyCoord(alt=alt * u.deg, az=az * u.deg, frame="altaz") except: - alt, az, tel_altaz = None, None, None + pass # get current moon and sun information moon_altaz = self.observer.moon_altaz(now) diff --git a/tests/modules/telescope/test_basetelescope.py b/tests/modules/telescope/test_basetelescope.py index 31985d961..2398dd382 100644 --- a/tests/modules/telescope/test_basetelescope.py +++ b/tests/modules/telescope/test_basetelescope.py @@ -6,6 +6,7 @@ import numpy as np import pytest import astropy.units as u +import pytest_mock from astropy.coordinates import SkyCoord, EarthLocation import pyobs @@ -59,7 +60,7 @@ def location(self) -> EarthLocation: @pytest.mark.asyncio -async def test_open(mocker): +async def test_open(mocker: pytest_mock.MockFixture) -> None: mocker.patch("pyobs.mixins.WeatherAwareMixin.open") mocker.patch("pyobs.mixins.MotionStatusMixin.open") mocker.patch("pyobs.modules.Module.open") @@ -67,9 +68,9 @@ async def test_open(mocker): telescope = TestBaseTelescope() await telescope.open() - pyobs.mixins.WeatherAwareMixin.open.assert_called_once_with(telescope) - pyobs.mixins.MotionStatusMixin.open.assert_called_once_with(telescope) - pyobs.modules.Module.open.assert_called_once_with(telescope) + pyobs.mixins.WeatherAwareMixin.open.assert_called_once_with(telescope) # type: ignore + pyobs.mixins.MotionStatusMixin.open.assert_called_once_with(telescope) # type: ignore + pyobs.modules.Module.open.assert_called_once_with(telescope) # type: ignore @pytest.mark.asyncio @@ -95,7 +96,7 @@ async def test_update_celestial_headers_no_altaz() -> None: telescope = TestBaseTelescope() telescope.observer = MockObserver() - telescope.get_altaz = AsyncMock(side_effect=Exception) + telescope.get_altaz = AsyncMock(side_effect=Exception) # type: ignore celestial_headers = await telescope._calc_celestial_headers() @@ -107,7 +108,7 @@ async def test_update_celestial_headers() -> None: telescope = TestBaseTelescope() telescope.observer = MockObserver() - telescope.get_altaz = AsyncMock(return_value=(60, 0)) + telescope.get_altaz = AsyncMock(return_value=(60, 0)) # type: ignore celestial_headers = await telescope._calc_celestial_headers() @@ -118,7 +119,7 @@ async def test_update_celestial_headers() -> None: async def test_get_fits_header_before_no_altaz() -> None: telescope = TestBaseTelescope() - telescope.get_altaz = AsyncMock(side_effect=Exception) + telescope.get_altaz = AsyncMock(side_effect=Exception) # type: ignore assert await telescope.get_fits_header_before() == {} @@ -127,7 +128,7 @@ async def test_get_fits_header_before_no_altaz() -> None: async def test_get_fits_header_before_optional_headers() -> None: telescope = TestBaseTelescope() telescope._fits_headers = {"TEST": (1.0, "TEST")} - telescope._calc_celestial_headers = AsyncMock(return_value={"CTEST": (1.0, "CTEST")}) + telescope._calc_celestial_headers = AsyncMock(return_value={"CTEST": (1.0, "CTEST")}) # type: ignore header = await telescope.get_fits_header_before() @@ -156,8 +157,8 @@ async def test_get_fits_header_before_altaz() -> None: telescope = TestBaseTelescope() telescope.observer = MockObserver() - telescope.get_radec = AsyncMock(return_value=(60, 0)) - telescope.get_altaz = AsyncMock(return_value=(60, 0)) + telescope.get_radec = AsyncMock(return_value=(60, 0)) # type: ignore + telescope.get_altaz = AsyncMock(return_value=(60, 0)) # type: ignore header = await telescope.get_fits_header_before() @@ -178,36 +179,36 @@ async def test_get_fits_header_before_altaz() -> None: @pytest.mark.asyncio -async def test_move_altaz_motion_status(): +async def test_move_altaz_motion_status() -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.INITIALIZING) - telescope._move_altaz = AsyncMock() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.INITIALIZING) # type: ignore + telescope._move_altaz = AsyncMock() # type: ignore await telescope.move_altaz(60, 0) telescope._move_altaz.assert_not_awaited() @pytest.mark.asyncio -async def test_move_altaz_min_alt(): +async def test_move_altaz_min_alt() -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore with pytest.raises(ValueError): await telescope.move_altaz(0, 0) @pytest.mark.asyncio -async def test_move_altaz(): +async def test_move_altaz() -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore telescope.comm = DummyComm() - telescope.comm.send_event = AsyncMock() + telescope.comm.send_event = AsyncMock() # type: ignore - telescope._change_motion_status = AsyncMock() - telescope._move_altaz = AsyncMock() + telescope._change_motion_status = AsyncMock() # type: ignore + telescope._move_altaz = AsyncMock() # type: ignore - telescope._wait_for_motion = AsyncMock() + telescope._wait_for_motion = AsyncMock() # type: ignore await telescope.move_altaz(60.0, 0.0) @@ -224,11 +225,11 @@ async def test_move_altaz(): @pytest.mark.asyncio -async def test_move_altaz_log(caplog): +async def test_move_altaz_log(caplog: pytest.LogCaptureFixture) -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore - telescope._change_motion_status = AsyncMock() # We need to mock this, since this also logs to logging.INFO + telescope._change_motion_status = AsyncMock() # type: ignore # We need to mock this, since this also logs to logging.INFO with caplog.at_level(logging.INFO): await telescope.move_altaz(60.0, 0.0) @@ -239,51 +240,51 @@ async def test_move_altaz_log(caplog): @pytest.mark.asyncio -async def test_move_radec_motion_status(): +async def test_move_radec_motion_status() -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.INITIALIZING) - telescope._move_radec = AsyncMock() + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.INITIALIZING) # type: ignore + telescope._move_radec = AsyncMock() # type: ignore await telescope.move_radec(60, 0) telescope._move_radec.assert_not_awaited() @pytest.mark.asyncio -async def test_move_radec_no_observer(): +async def test_move_radec_no_observer() -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore with pytest.raises(ValueError): await telescope.move_radec(0, 0) @pytest.mark.asyncio -async def test_move_radec_min_alt(): +async def test_move_radec_min_alt() -> None: telescope = TestBaseTelescope() telescope.observer = MockObserver() - telescope.observer.altaz = Mock(return_value=SkyCoord(alt=0.0, az=0.0, unit="deg", frame="altaz")) + telescope.observer.altaz = Mock(return_value=SkyCoord(alt=0.0, az=0.0, unit="deg", frame="altaz")) # type: ignore - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore with pytest.raises(ValueError): await telescope.move_radec(0, 0) @pytest.mark.asyncio -async def test_move_radec(): +async def test_move_radec() -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore telescope.observer = MockObserver() - telescope.observer.altaz = Mock(return_value=SkyCoord(alt=60.0, az=0.0, unit="deg", frame="altaz")) + telescope.observer.altaz = Mock(return_value=SkyCoord(alt=60.0, az=0.0, unit="deg", frame="altaz")) # type: ignore telescope.comm = DummyComm() - telescope.comm.send_event = AsyncMock() + telescope.comm.send_event = AsyncMock() # type: ignore - telescope._change_motion_status = AsyncMock() - telescope._move_radec = AsyncMock() + telescope._change_motion_status = AsyncMock() # type: ignore + telescope._move_radec = AsyncMock() # type: ignore - telescope._wait_for_motion = AsyncMock() + telescope._wait_for_motion = AsyncMock() # type: ignore await telescope.move_radec(60.0, 0.0) @@ -300,14 +301,14 @@ async def test_move_radec(): @pytest.mark.asyncio -async def test_move_radec_log(caplog): +async def test_move_radec_log(caplog: pytest.LogCaptureFixture) -> None: telescope = TestBaseTelescope() - telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) + telescope.get_motion_status = AsyncMock(return_value=MotionStatus.IDLE) # type: ignore telescope.observer = MockObserver() - telescope.observer.altaz = Mock(return_value=SkyCoord(alt=60.0, az=0.0, unit="deg", frame="altaz")) + telescope.observer.altaz = Mock(return_value=SkyCoord(alt=60.0, az=0.0, unit="deg", frame="altaz")) # type: ignore - telescope._change_motion_status = AsyncMock() # We need to mock this, since this also logs to logging.INFO + telescope._change_motion_status = AsyncMock() # type: ignore # We need to mock this, since this also logs to logging.INFO with caplog.at_level(logging.INFO): await telescope.move_radec(60.0, 0.0) From 68cf6b1447d1cad563aa8bd755acbec68e2dab85 Mon Sep 17 00:00:00 2001 From: GermanHydrogen Date: Tue, 7 May 2024 10:24:15 +0200 Subject: [PATCH 8/8] Optimized dummy telescope test --- pyobs/modules/telescope/dummytelescope.py | 2 +- tests/modules/telescope/test_dummytelescope.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyobs/modules/telescope/dummytelescope.py b/pyobs/modules/telescope/dummytelescope.py index cf4a6fdb3..fb57d158c 100644 --- a/pyobs/modules/telescope/dummytelescope.py +++ b/pyobs/modules/telescope/dummytelescope.py @@ -93,7 +93,7 @@ async def _move_altaz(self, alt: float, az: float, abort_event: asyncio.Event) - coords = SkyCoord( alt=alt * u.degree, az=az * u.degree, obstime=Time.now(), location=self.location, frame="altaz" ) - icrs = coords.icrs + icrs = coords.transform_to("icrs") # start slewing await self._move(icrs.ra.degree, icrs.dec.degree, abort_event) diff --git a/tests/modules/telescope/test_dummytelescope.py b/tests/modules/telescope/test_dummytelescope.py index d363ace55..d0b406469 100644 --- a/tests/modules/telescope/test_dummytelescope.py +++ b/tests/modules/telescope/test_dummytelescope.py @@ -1,7 +1,9 @@ import asyncio from unittest.mock import AsyncMock, Mock +import astropy import pytest +import pytest_mock from astropy.coordinates import EarthLocation, SkyCoord import astropy.units as u @@ -42,8 +44,9 @@ async def test_move_radec() -> None: @pytest.mark.asyncio -async def test_move_altaz(mocker) -> None: +async def test_move_altaz(mocker: pytest_mock.MockFixture) -> None: mocker.patch("pyobs.utils.time.Time.now", return_value=Time("2010-01-01T00:00:00", format="isot")) + mocker.patch("astropy.coordinates.SkyCoord.transform_to", return_value=SkyCoord(0, 0, unit="deg", frame="icrs")) telescope = DummyTelescope() telescope.location = EarthLocation(lat=10. * u.degree, lon=10. * u.degree, height=100 * u.meter) @@ -52,7 +55,8 @@ async def test_move_altaz(mocker) -> None: abort_event = asyncio.Event() await telescope._move_altaz(60, 0, abort_event) - telescope._move.assert_awaited_with(110.35706910444917, 40.020387331332806, abort_event) + astropy.coordinates.SkyCoord.transform_to.assert_called_once_with("icrs") + telescope._move.assert_awaited_with(0, 0, abort_event) @pytest.mark.asyncio async def test_get_focus() -> None: