示例#1
0
    async def test_fault(self):
        async with self.make_csc(initial_state=salobj.State.ENABLED):
            await self.assert_next_sample(
                topic=self.remote.evt_softwareVersions,
                cscVersion=mtrotator.__version__,
                subsystemVersions="",
            )
            await self.assert_next_summary_state(salobj.State.ENABLED)
            await self.remote.cmd_fault.start(timeout=STD_TIMEOUT)
            await self.assert_next_summary_state(salobj.State.FAULT)
            await self.assert_next_sample(
                self.remote.evt_errorCode,
                errorCode=mtrotator.ErrorCode.FAULT_COMMAND,
            )

            # Make sure the fault command only works in enabled state
            with salobj.assertRaisesAckError():
                await self.remote.cmd_fault.start(timeout=STD_TIMEOUT)

            await self.remote.cmd_clearError.start(timeout=STD_TIMEOUT)
            await self.assert_next_summary_state(salobj.State.STANDBY)
            with salobj.assertRaisesAckError():
                await self.remote.cmd_fault.start(timeout=STD_TIMEOUT)

            await self.remote.cmd_start.start(timeout=STD_TIMEOUT)
            await self.assert_next_summary_state(salobj.State.DISABLED)
            with salobj.assertRaisesAckError():
                await self.remote.cmd_fault.start(timeout=STD_TIMEOUT)
示例#2
0
 async def test_request_authorization_errors(self):
     async with self.make_csc(
             config_dir=TEST_CONFIG_DIR,
             initial_state=salobj.State.ENABLED,
     ):
         with salobj.assertRaisesAckError():
             # Empty cscsToChange
             await self.remote.cmd_requestAuthorization.set_start(
                 cscsToChange="",
                 authorizedUsers="a@b",
                 nonAuthorizedCSCs="a",
                 timeout=STD_TIMEOUT,
             )
         with salobj.assertRaisesAckError():
             await self.remote.cmd_requestAuthorization.set_start(
                 cscsToChange="_bad_csc_name",
                 authorizedUsers="a@b",
                 nonAuthorizedCSCs="a",
                 timeout=STD_TIMEOUT,
             )
         with salobj.assertRaisesAckError():
             await self.remote.cmd_requestAuthorization.set_start(
                 cscsToChange="Test:2",
                 authorizedUsers="_bad_username@any",
                 nonAuthorizedCSCs="a",
                 timeout=STD_TIMEOUT,
             )
         with salobj.assertRaisesAckError():
             await self.remote.cmd_requestAuthorization.set_start(
                 cscsToChange="Test:2",
                 authorizedUsers="some@any",
                 nonAuthorizedCSCs="_badCscName",
                 timeout=STD_TIMEOUT,
             )
示例#3
0
    async def test_configuration(self):
        async with self.make_csc(initial_state=salobj.State.STANDBY,
                                 config_dir=TEST_CONFIG_DIR):
            await self.assert_next_summary_state(salobj.State.STANDBY)

            for bad_config_name in (
                    "no_such_file.yaml",
                    "invalid_no_such_algorithm.yaml",
                    "invalid_malformed.yaml",
                    "invalid_bad_max_daz.yaml",
            ):
                with self.subTest(bad_config_name=bad_config_name):
                    with salobj.assertRaisesAckError():
                        await self.remote.cmd_start.set_start(
                            configurationOverride=bad_config_name,
                            timeout=STD_TIMEOUT)

            await self.remote.cmd_start.set_start(
                configurationOverride="valid.yaml", timeout=STD_TIMEOUT)

            settings = await self.assert_next_sample(self.remote.evt_algorithm,
                                                     algorithmName="simple")
            # max_delta_azimuth=7.1 is hard coded in the yaml file
            self.assertEqual(yaml.safe_load(settings.algorithmConfig),
                             dict(max_delta_azimuth=7.1))
示例#4
0
    async def test_load(self):
        """Test load command."""
        async with self.make_csc(
                config_dir=TEST_CONFIG_DIR,
                initial_state=salobj.State.STANDBY,
                simulation_mode=SchedulerModes.SIMULATION,
        ), ObservatoryStateMock():
            config = (pathlib.Path(__file__).parents[1].joinpath(
                "tests", "data", "test_observing_list.yaml"))

            bad_config = (pathlib.Path(__file__).parents[1].joinpath(
                "tests", "data", "bad_config.yaml"))

            try:
                await salobj.set_summary_state(self.remote,
                                               salobj.State.ENABLED,
                                               override="simple.yaml")

                await self.remote.cmd_load.set_start(uri=config.as_uri(),
                                                     timeout=SHORT_TIMEOUT)

                with salobj.assertRaisesAckError():
                    await self.remote.cmd_load.set_start(
                        uri=bad_config.as_uri(), timeout=SHORT_TIMEOUT)
            finally:
                await salobj.set_summary_state(self.remote,
                                               salobj.State.STANDBY)
示例#5
0
    async def test_configuration(self):
        async with self.make_csc(initial_state=salobj.State.STANDBY,
                                 config_dir=TEST_CONFIG_DIR):
            assert self.csc.summary_state == salobj.State.STANDBY
            await self.assert_next_summary_state(salobj.State.STANDBY)

            for bad_config_name in (
                    "no_such_file.yaml",
                    "invalid_no_such_algorithm.yaml",
                    "invalid_malformed.yaml",
                    "invalid_bad_max_daz.yaml",
            ):
                with self.subTest(bad_config_name=bad_config_name):
                    self.remote.cmd_start.set(
                        configurationOverride=bad_config_name)
                    with salobj.assertRaisesAckError():
                        await self.remote.cmd_start.start(timeout=STD_TIMEOUT)

            self.remote.cmd_start.set(configurationOverride="valid.yaml")
            await self.remote.cmd_start.start(timeout=STD_TIMEOUT)
            assert self.csc.summary_state == salobj.State.DISABLED
            await self.assert_next_summary_state(salobj.State.DISABLED)
            settings = await self.remote.evt_algorithm.next(
                flush=False, timeout=STD_TIMEOUT)
            assert settings.algorithmName == "simple"
            # max_delta_elevation and max_delta_azimuth are hard coded
            # in data/config/valid.yaml
            assert yaml.safe_load(settings.algorithmConfig) == dict(
                max_delta_azimuth=7.1, max_delta_elevation=5.5)
示例#6
0
    async def test_no_config(self):
        short_config_timeout = 1
        with unittest.mock.patch(
                "lsst.ts.hexrotcomm.base_csc.CONFIG_TIMEOUT",
                short_config_timeout), unittest.mock.patch(
                    "lsst.ts.hexrotcomm.simple_mock_controller.ENABLE_CONFIG",
                    False):
            async with self.make_csc(
                    initial_state=salobj.State.STANDBY,
                    simulation_mode=1,
                    config_dir=LOCAL_CONFIG_DIR,
            ):
                await self.assert_next_summary_state(salobj.State.STANDBY)
                await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                              errorCode=0)

                with salobj.assertRaisesAckError(
                        ack=salobj.SalRetCode.CMD_FAILED):
                    await self.remote.cmd_start.start(timeout=STD_TIMEOUT +
                                                      short_config_timeout)
                await self.assert_next_summary_state(salobj.State.FAULT)
                data = await self.assert_next_sample(
                    topic=self.remote.evt_errorCode,
                    errorCode=ErrorCode.NO_CONFIG,
                    traceback="",
                )
                assert "Timed out" in data.errorReport
示例#7
0
 async def test_bad_config_dirs(self) -> None:
     for bad_config_dir in TEST_CONFIGS_ROOT.glob("bad_*"):
         async with self.make_csc(initial_state=salobj.State.STANDBY,
                                  config_dir=bad_config_dir):
             await self.assert_next_summary_state(salobj.State.STANDBY)
             with salobj.assertRaisesAckError():
                 await self.remote.cmd_start.set_start(
                     configurationOverride="", timeout=STD_TIMEOUT)
示例#8
0
    async def test_expose_good(self):
        """Test that we can take an exposure and that appropriate events are
        emitted.
        """
        async with self.make_csc(
                initial_state=salobj.State.ENABLED,
                simulation_mode=FiberSpectrograph.SimulationMode.S3Server,
                index=FiberSpectrograph.SalIndex.RED,
                config_dir=TEST_CONFIG_DIR,
        ):
            # Check that we are properly in ENABLED at the start
            await self.assert_next_summary_state(salobj.State.ENABLED)
            assert self.csc.s3bucket_name == self.csc.s3bucket.name

            duration = 2  # seconds
            task = asyncio.create_task(
                self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT +
                                                 duration,
                                                 duration=duration))
            await self.check_exposureState(self.remote,
                                           ExposureState.INTEGRATING)
            # Wait for the exposure to finish.
            await task
            await self.check_exposureState(self.remote, ExposureState.DONE)

            # Check the large file event.
            data = await self.remote.evt_largeFileObjectAvailable.next(
                flush=False, timeout=STD_TIMEOUT)
            parsed_url = urllib.parse.urlparse(data.url)
            assert parsed_url.scheme == "s3"
            assert parsed_url.netloc == self.csc.s3bucket.name

            # Minimally check the data written to s3
            key = parsed_url.path[1:]  # Strip leading "/"
            fileobj = await self.csc.s3bucket.download(key)
            hdulist = astropy.io.fits.open(fileobj)
            assert len(hdulist) == 2
            assert hdulist[0].header["ORIGIN"] == "FiberSpectrographCsc"
            assert hdulist[0].header["INSTRUME"] == "FiberSpectrograph.Red"

            # Check that out of range durations do not put us in FAULT,
            # and do not change the exposure state.
            duration = 1e-9  # seconds
            with salobj.assertRaisesAckError(
                    ack=salobj.SalRetCode.CMD_FAILED,
                    result_contains="Exposure duration",
            ):
                await asyncio.create_task(
                    self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT,
                                                     duration=duration))
            # No ExposureState message should have been emitted.
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_exposureState.next(flush=False,
                                                         timeout=STD_TIMEOUT)
            # We should not have left ENABLED.
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_exposureState.next(flush=False,
                                                         timeout=STD_TIMEOUT)
示例#9
0
 async def test_enable_no_ccw_telemetry(self):
     """Test that it is not possible to enable the CSC if it
     is not receiving MTMount cameraCableWrap telemetry.
     """
     async with self.make_csc(initial_state=salobj.State.DISABLED,
                              run_mock_ccw=False):
         with salobj.assertRaisesAckError(ack=salobj.SalRetCode.CMD_FAILED):
             await self.remote.cmd_enable.start(timeout=STD_TIMEOUT)
         await self.assert_next_summary_state(salobj.State.DISABLED)
示例#10
0
    async def test_bad_site(self) -> None:
        config_dir = TEST_CONFIGS_ROOT / "good_with_site_file"
        with utils.modify_environ(LSST_SITE="no_such_site"):
            async with self.make_csc(
                    initial_state=salobj.State.STANDBY,
                    config_dir=config_dir,
            ):
                await self.assert_next_summary_state(salobj.State.STANDBY)

                with salobj.assertRaisesAckError():
                    await self.remote.cmd_start.start(timeout=STD_TIMEOUT)
示例#11
0
    async def test_track_bad_values(self):
        """Test the track command with bad values.

        This should go into FAULT.
        """
        async with self.make_csc(initial_state=salobj.State.ENABLED):
            await self.assert_next_summary_state(salobj.State.ENABLED)
            await self.assert_next_sample(
                topic=self.remote.evt_controllerState,
                controllerState=ControllerState.ENABLED,
                enabledSubstate=EnabledSubstate.STATIONARY,
            )
            settings = await self.remote.evt_configuration.next(
                flush=False, timeout=STD_TIMEOUT)
            await self.remote.cmd_trackStart.start(timeout=STD_TIMEOUT)
            await self.assert_next_sample(
                topic=self.remote.evt_controllerState,
                controllerState=ControllerState.ENABLED,
                enabledSubstate=EnabledSubstate.SLEWING_OR_TRACKING,
            )

            # Run these quickly enough and the controller will still be enabled
            curr_tai = salobj.current_tai()
            for pos, vel, tai in (
                    # Position out of range.
                (settings.positionAngleLowerLimit - 0.001, 0, curr_tai),
                (settings.positionAngleUpperLimit + 0.001, 0, curr_tai),
                    # Velocity out of range.
                (0, settings.velocityLimit + 0.001, curr_tai),
                    # Current position and velocity OK but the position
                    # at the specified tai is out of bounds.
                (
                    settings.positionAngleUpperLimit - 0.001,
                    settings.velocityLimit - 0.001,
                    curr_tai + 1,
                ),
            ):
                with self.subTest(pos=pos, vel=vel, tai=tai):
                    with salobj.assertRaisesAckError(
                            ack=salobj.SalRetCode.CMD_FAILED):
                        await self.remote.cmd_track.set_start(
                            angle=pos,
                            velocity=vel,
                            tai=tai,
                            timeout=STD_TIMEOUT)
                    # Send a valid pvt to reset the tracking timer
                    # and give the controller time to deal with it.
                    await self.remote.cmd_track.set_start(
                        angle=0,
                        velocity=0,
                        tai=salobj.current_tai(),
                        timeout=STD_TIMEOUT,
                    )
                    await asyncio.sleep(0.01)
示例#12
0
    async def test_config(self) -> None:
        """Test MonochromatorCsc configuration validator."""
        async with self.make_csc(simulation_mode=1, config_dir=TEST_CONFIG_DIR):
            await self.assert_next_summary_state(salobj.State.STANDBY)

            invalid_files = glob.glob(os.path.join(TEST_CONFIG_DIR, "invalid_*.yaml"))
            bad_config_names = [os.path.basename(name) for name in invalid_files]
            bad_config_names.append("no_such_file.yaml")
            for bad_config_name in bad_config_names:
                with self.subTest(bad_config_name=bad_config_name):
                    with salobj.assertRaisesAckError():
                        await self.remote.cmd_start.set_start(
                            configurationOverride=bad_config_name, timeout=STD_TIMEOUT
                        )
示例#13
0
    async def test_request_authorization_success(self):
        index1 = 5
        index2 = 52
        async with self.make_csc(
                config_dir=TEST_CONFIG_DIR,
                initial_state=salobj.State.ENABLED,
        ), MinimalTestCsc(index=index1) as csc1, MinimalTestCsc(
                index=index2) as csc2:
            await self.remote.evt_logLevel.aget(timeout=STD_TIMEOUT)
            self.assertEqual(csc1.salinfo.authorized_users, set())
            self.assertEqual(csc1.salinfo.non_authorized_cscs, set())
            self.assertEqual(csc2.salinfo.authorized_users, set())
            self.assertEqual(csc2.salinfo.non_authorized_cscs, set())

            # Change the first Test CSC
            desired_users = ("sal@purview", "[email protected]")
            desired_cscs = ("Foo", "Bar:1", "XKCD:47")
            await self.remote.cmd_requestAuthorization.set_start(
                cscsToChange=f"Test:{index1}",
                authorizedUsers=", ".join(desired_users),
                nonAuthorizedCSCs=", ".join(desired_cscs),
                timeout=60,
            )
            self.assertEqual(csc1.salinfo.authorized_users, set(desired_users))
            self.assertEqual(csc1.salinfo.non_authorized_cscs,
                             set(desired_cscs))
            self.assertEqual(csc2.salinfo.authorized_users, set())
            self.assertEqual(csc2.salinfo.non_authorized_cscs, set())

            # Change both Test CSCs
            desired_users = ("meow@validate", "v122s@123")
            desired_cscs = ("AT", "seisen:22")
            # Include a CSC that does not exist. Authorize will try to
            # change it, that will time out, command will fail but other CSCs
            # will be set.
            with salobj.assertRaisesAckError():
                await self.remote.cmd_requestAuthorization.set_start(
                    cscsToChange=f"Test:{index1}, Test:999, Test:{index2}",
                    authorizedUsers=", ".join(desired_users),
                    nonAuthorizedCSCs=", ".join(desired_cscs),
                    timeout=60,
                )
            self.assertEqual(csc1.salinfo.authorized_users, set(desired_users))
            self.assertEqual(csc1.salinfo.non_authorized_cscs,
                             set(desired_cscs))
            self.assertEqual(csc2.salinfo.authorized_users, set(desired_users))
            self.assertEqual(csc2.salinfo.non_authorized_cscs,
                             set(desired_cscs))
示例#14
0
    async def test_expose_fails(self):
        """Test that a failed exposure puts us in the FAULT state, which will
        disconnect the device.
        """
        # Make `GetScopeData` (which is called to get the measured output from
        # the device) return an error code, so that the device controller
        # raises an exception inside `expose()`.
        self.patch.return_value.AVS_GetScopeData.side_effect = None
        self.patch.return_value.AVS_GetScopeData.return_value = (
            FiberSpectrograph.AvsReturnCode.ERR_INVALID_MEAS_DATA.value)
        async with self.make_csc(initial_state=salobj.State.ENABLED,
                                 config_dir=TEST_CONFIG_DIR):
            # Check that we are properly in ENABLED at the start.
            await self.assert_next_summary_state(salobj.State.ENABLED)
            error = await self.assert_next_sample(
                topic=self.remote.evt_errorCode, errorCode=0, errorReport="")

            msg = "Failed to take exposure"
            with salobj.assertRaisesAckError(ack=salobj.SalRetCode.CMD_FAILED,
                                             result_contains=msg):
                await self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT,
                                                       duration=0.5)
            # The exposure state should be Integrating during the exposure.
            await self.check_exposureState(self.remote,
                                           ExposureState.INTEGRATING)
            # The exposure state should be Failed after the exposure has
            # completed, because GetScopeData returned an error code.
            await self.assert_next_summary_state(salobj.State.FAULT)
            error = await self.remote.evt_errorCode.next(flush=False,
                                                         timeout=STD_TIMEOUT)
            errorMsg = str(
                FiberSpectrograph.AvsReturnError(
                    FiberSpectrograph.AvsReturnCode.ERR_INVALID_MEAS_DATA.
                    value,
                    "GetScopeData",
                ))
            assert errorMsg in error.errorReport
            # Going into FAULT should close the device connection.
            assert self.csc.device is None
            # the exposure state should be FAILED after a failed exposure
            await self.check_exposureState(self.remote, ExposureState.FAILED)

            # Test recovery from fault state
            await self.remote.cmd_standby.start(timeout=STD_TIMEOUT)
            await self.assert_next_summary_state(salobj.State.STANDBY)
            error = await self.assert_next_sample(
                topic=self.remote.evt_errorCode, errorCode=0, errorReport="")
示例#15
0
    async def test_configure_velocity(self):
        """Test the configureVelocity command."""
        async with self.make_csc(initial_state=salobj.State.ENABLED):
            data = await self.remote.evt_configuration.next(
                flush=False, timeout=STD_TIMEOUT)
            initial_limit = data.velocityLimit
            new_limit = initial_limit - 0.1
            await self.remote.cmd_configureVelocity.set_start(
                vlimit=new_limit, timeout=STD_TIMEOUT)
            data = await self.remote.evt_configuration.next(
                flush=False, timeout=STD_TIMEOUT)
            self.assertAlmostEqual(data.velocityLimit, new_limit)

            for bad_vlimit in (0, -1, mtrotator.MAX_VEL_LIMIT + 0.001):
                with self.subTest(bad_vlimit=bad_vlimit):
                    with salobj.assertRaisesAckError(
                            ack=salobj.SalRetCode.CMD_FAILED):
                        await self.remote.cmd_configureVelocity.set_start(
                            vlimit=bad_vlimit, timeout=STD_TIMEOUT)
示例#16
0
    async def test_configure_acceleration(self):
        """Test the configureAcceleration command."""
        async with self.make_csc(initial_state=salobj.State.ENABLED):
            data = await self.remote.evt_configuration.next(
                flush=False, timeout=STD_TIMEOUT)
            initial_limit = data.accelerationLimit
            print("initial_limit=", initial_limit)
            new_limit = initial_limit - 0.1
            await self.remote.cmd_configureAcceleration.set_start(
                alimit=new_limit, timeout=STD_TIMEOUT)
            data = await self.remote.evt_configuration.next(
                flush=False, timeout=STD_TIMEOUT)
            self.assertAlmostEqual(data.accelerationLimit, new_limit)

            for bad_alimit in (-1, 0, mtrotator.MAX_ACCEL_LIMIT + 0.001):
                with self.subTest(bad_alimit=bad_alimit):
                    with salobj.assertRaisesAckError(
                            ack=salobj.SalRetCode.CMD_FAILED):
                        await self.remote.cmd_configureAcceleration.set_start(
                            alimit=bad_alimit, timeout=STD_TIMEOUT)
示例#17
0
    async def test_configuration_invalid(self):
        async with self.make_csc(config_dir=TEST_CONFIG_DIR,
                                 initial_state=salobj.State.STANDBY):
            invalid_files = glob.glob(str(TEST_CONFIG_DIR / "invalid_*.yaml"))
            # Test the invalid files and a blank settingsToApply
            # (since the schema doesn't have a usable default).
            bad_config_names = [
                os.path.basename(name) for name in invalid_files
            ] + [""]
            for bad_config_name in bad_config_names:
                with self.subTest(bad_config_name=bad_config_name):
                    with salobj.assertRaisesAckError(
                            ack=salobj.SalRetCode.CMD_FAILED):
                        await self.remote.cmd_start.set_start(
                            settingsToApply=bad_config_name,
                            timeout=STD_TIMEOUT)

            # Check that the CSC can still be configured.
            # This also exercises specifying a rule with no configuration.
            await self.remote.cmd_start.set_start(settingsToApply="basic.yaml",
                                                  timeout=STD_TIMEOUT)
示例#18
0
    async def test_invalid_config(self):
        async with self.make_csc(
                initial_state=salobj.State.STANDBY,
                simulation_mode=1,
                config_dir=LOCAL_CONFIG_DIR,
        ):
            # Try config files with invalid data.
            # The command should fail and the summary state remain in STANDBY.
            for bad_config_path in LOCAL_CONFIG_DIR.glob("bad_*.yaml"):
                bad_config_name = bad_config_path.name
                with self.subTest(bad_config_name=bad_config_name):
                    with salobj.assertRaisesAckError():
                        await self.remote.cmd_start.set_start(
                            settingsToApply=bad_config_name,
                            timeout=STD_TIMEOUT)
                    assert self.csc.summary_state == salobj.State.STANDBY

            # Now try a valid config file
            await self.remote.cmd_start.set_start(settingsToApply="valid.yaml",
                                                  timeout=STD_TIMEOUT)
            assert self.csc.summary_state == salobj.State.DISABLED
示例#19
0
    async def test_cannot_connect(self):
        """Being unable to connect should send CSC to fault state.

        The error code should be ErrorCode.CONNECTION_LOST
        """
        async with self.make_csc(
                initial_state=salobj.State.STANDBY,
                simulation_mode=1,
                config_dir=LOCAL_CONFIG_DIR,
        ):
            await self.assert_next_summary_state(salobj.State.STANDBY)
            await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                          errorCode=0)

            # Tell the CSC not to make a mock controller,
            # so it will fail to connect to the low-level controller.
            self.csc.allow_mock_controller = False

            with salobj.assertRaisesAckError():
                await self.remote.cmd_start.start(timeout=STD_TIMEOUT)
            await self.assert_next_summary_state(salobj.State.FAULT)
            await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                          errorCode=ErrorCode.CONNECTION_LOST)
示例#20
0
    async def test_cancelExposure(self):
        """Test that we can stop an active exposure, and that the exposureState
        is changed appropriately.
        """
        async with self.make_csc(initial_state=salobj.State.ENABLED,
                                 config_dir=TEST_CONFIG_DIR):
            # Check that we are properly in ENABLED at the start
            await self.assert_next_summary_state(salobj.State.ENABLED)

            duration = 5  # seconds
            task = asyncio.create_task(
                self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT +
                                                 duration,
                                                 duration=duration))
            # Wait for the exposure to start integrating.
            await self.check_exposureState(self.remote,
                                           ExposureState.INTEGRATING)
            await self.remote.cmd_cancelExposure.set_start(timeout=STD_TIMEOUT)
            with salobj.assertRaisesAckError(
                    ack=salobj.SalRetCode.CMD_ABORTED):
                await task
            await self.check_exposureState(self.remote,
                                           ExposureState.CANCELLED)
示例#21
0
    async def test_enable_fails(self):
        """Test that exceptions raised when connecting cause a fault when
        switching the CSC from STANDBY to DISABLED.
        """
        self.patch.return_value.AVS_Activate.return_value = (
            FiberSpectrograph.AvsReturnCode.invalidHandle.value)
        async with self.make_csc(initial_state=salobj.State.STANDBY,
                                 config_dir=TEST_CONFIG_DIR):
            # Check that we are properly in STANDBY at the start
            await self.assert_next_summary_state(salobj.State.STANDBY)
            error = await self.assert_next_sample(
                topic=self.remote.evt_errorCode, errorCode=0, errorReport="")

            msg = "Failed to connect"
            with salobj.assertRaisesAckError(ack=salobj.SalRetCode.CMD_FAILED,
                                             result_contains=msg):
                await self.remote.cmd_start.start(timeout=STD_TIMEOUT)
            await self.assert_next_summary_state(salobj.State.FAULT)
            error = await self.remote.evt_errorCode.next(flush=False,
                                                         timeout=STD_TIMEOUT)
            assert "RuntimeError" in error.errorReport
            assert "Invalid device handle; cannot activate device" in error.errorReport
            assert self.csc.device is None
示例#22
0
    async def test_configuration(self):
        async with self.make_csc(
                initial_state=salobj.State.STANDBY,
                config_dir=TEST_CONFIG_DIR,
                simulation_mode=1,
        ):
            self.assertEqual(self.csc.summary_state, salobj.State.STANDBY)
            await self.assert_next_summary_state(salobj.State.STANDBY)

            invalid_files = glob.glob(
                os.path.join(TEST_CONFIG_DIR, "invalid_*.yaml"))
            bad_config_names = [
                os.path.basename(name) for name in invalid_files
            ]
            bad_config_names.append("no_such_file.yaml")
            for bad_config_name in bad_config_names:
                with self.subTest(bad_config_name=bad_config_name):
                    with salobj.assertRaisesAckError():
                        await self.remote.cmd_start.set_start(
                            settingsToApply=bad_config_name,
                            timeout=STD_TIMEOUT)

            await self.remote.cmd_start.set_start(settingsToApply="all_fields",
                                                  timeout=STD_TIMEOUT)
            await self.assert_next_sample(
                self.remote.evt_softwareVersions,
                cscVersion=OCPS.__version__,
                subsystemVersions="",
            )
            self.assertEqual(self.csc.summary_state, salobj.State.DISABLED)
            await self.assert_next_summary_state(salobj.State.DISABLED)
            all_fields_path = os.path.join(TEST_CONFIG_DIR, "all_fields.yaml")
            with open(all_fields_path, "r") as f:
                all_fields_raw = f.read()
            all_fields_data = yaml.safe_load(all_fields_raw)
            for field, value in all_fields_data.items():
                self.assertEqual(getattr(self.csc.config, field), value)
示例#23
0
    async def test_invalid_configs(self) -> None:
        config_dir = TEST_CONFIGS_ROOT / "good_no_site_file"

        async with self.make_csc(initial_state=salobj.State.STANDBY,
                                 config_dir=config_dir):
            await self.assert_next_summary_state(salobj.State.STANDBY)
            for name in ("all_bad_types", "bad_format", "one_bad_type",
                         "extra_field"):
                config_file = f"invalid_{name}.yaml"
                with self.subTest(config_file=config_file):
                    with salobj.assertRaisesAckError(
                            ack=salobj.SalRetCode.CMD_FAILED):
                        await self.remote.cmd_start.set_start(
                            configurationOverride=config_file,
                            timeout=STD_TIMEOUT)
                    data = self.remote.evt_summaryState.get()
                    assert self.csc.summary_state == salobj.State.STANDBY
                    assert data.summaryState == salobj.State.STANDBY

            # Make sure the CSC can still be started.
            await self.remote.cmd_start.set_start(
                configurationOverride="all_fields.yaml", timeout=10)
            assert self.csc.summary_state == salobj.State.DISABLED
            await self.assert_next_summary_state(salobj.State.DISABLED)
示例#24
0
    async def test_expose_timeout(self):
        """Test that an exposure whose read times out puts us in FAULT and
        exposureState is set to TIMEOUT.
        """
        # Have the PollScan just run forever.
        self.patch.return_value.AVS_PollScan.side_effect = itertools.repeat(0)

        async with self.make_csc(initial_state=salobj.State.ENABLED,
                                 config_dir=TEST_CONFIG_DIR):
            # Check that we are properly in ENABLED at the start.
            await self.assert_next_summary_state(salobj.State.ENABLED)

            msg = "Timeout waiting for exposure"
            duration = 0.1
            with salobj.assertRaisesAckError(ack=salobj.SalRetCode.CMD_FAILED,
                                             result_contains=msg):
                await self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT +
                                                       duration,
                                                       duration=duration)
            # The exposure state should be Integrating during the exposure.
            await self.check_exposureState(self.remote,
                                           ExposureState.INTEGRATING)
            await self.check_exposureState(self.remote, ExposureState.TIMEDOUT)
            await self.assert_next_summary_state(salobj.State.FAULT)
示例#25
0
    async def test_set_instrument_port(self):
        async with self.make_csc(initial_state=salobj.State.STANDBY):
            # Change states manually to make the test compatible
            # with both ts_salobj 6.0 and 6.1: 6.0 does not output
            # evt_nasmyth1DriveStatus with enable=False
            # if initial_state=salobj.State.ENABLE).
            # Once we are not longer using salobj 6, it is safe to
            # remove the following line and specify
            # ``initial_state=salobj.State.ENABLED`` above
            await salobj.set_summary_state(self.remote, state=salobj.State.ENABLED)
            self.csc.configure(
                max_velocity=(100,) * 5,
                max_acceleration=(200,) * 5,
            )

            await self.assert_next_sample(
                self.remote.evt_m3State, state=M3State.NASMYTH1
            )
            await self.assert_next_sample(
                self.remote.evt_nasmyth1DriveStatus, enable=False
            )
            await self.assert_next_sample(
                self.remote.evt_nasmyth1DriveStatus, enable=True
            )
            await self.assert_next_sample(
                self.remote.evt_nasmyth2DriveStatus, enable=False
            )

            await self.remote.cmd_setInstrumentPort.set_start(
                port=M3ExitPort.PORT3, timeout=STD_TIMEOUT
            )
            await self.assert_next_sample(
                self.remote.evt_m3PortSelected, selected=M3ExitPort.PORT3
            )
            await self.assert_next_sample(
                self.remote.evt_m3State, state=M3State.INMOTION
            )

            # Nasmyth1 should now be disabled
            # and Nasmyth1 should remain disabled.
            await self.assert_next_sample(
                self.remote.evt_nasmyth1DriveStatus, enable=False
            )
            data = self.remote.evt_nasmyth2DriveStatus.get()
            self.assertFalse(data.enable)

            start_tai = utils.current_tai()

            await asyncio.sleep(0.2)

            # Attempts to start tracking should fail while M3 is moving.
            with salobj.assertRaisesAckError():
                await self.remote.cmd_startTracking.start()

            actuator = self.csc.actuators[ATMCSSimulator.Axis.M3]
            curr_segment = actuator.path.at(utils.current_tai())
            self.assertNotEqual(curr_segment.velocity, 0)

            # M3 is pointing to Port 3; neither rotator should be enabled.
            await self.assert_next_sample(
                self.remote.evt_m3State, state=M3State.PORT3, timeout=5
            )
            dt = utils.current_tai() - start_tai
            print(f"test_set_instrument_port M3 rotation took {dt:0.2f} sec")
            data = self.remote.evt_nasmyth1DriveStatus.get()
            self.assertFalse(data.enable)
            data = self.remote.evt_nasmyth2DriveStatus.get()
            self.assertFalse(data.enable)

            await self.remote.cmd_setInstrumentPort.set_start(
                port=M3ExitPort.NASMYTH2, timeout=STD_TIMEOUT
            )

            start_tai = utils.current_tai()
            await self.assert_next_sample(
                self.remote.evt_m3PortSelected, selected=M3ExitPort.NASMYTH2
            )
            await self.assert_next_sample(
                self.remote.evt_m3State, state=M3State.INMOTION
            )

            # Both rotators should remain disabled.
            data = self.remote.evt_nasmyth1DriveStatus.get()
            self.assertFalse(data.enable)
            data = self.remote.evt_nasmyth2DriveStatus.get()
            self.assertFalse(data.enable)
            self.remote.evt_nasmyth2DriveStatus.flush()

            await self.assert_next_sample(
                self.remote.evt_m3State, state=M3State.NASMYTH2, timeout=5
            )
            dt = utils.current_tai() - start_tai
            print(f"test_set_instrument_port M3 rotation took {dt:0.2f} sec")

            # M3 is pointing to Nasmyth2; that rotator
            # should be enabled and Nasmyth1 should not.
            await self.assert_next_sample(
                self.remote.evt_nasmyth2DriveStatus, enable=True
            )
            data = self.remote.evt_nasmyth1DriveStatus.get()
            self.assertFalse(data.enable)
示例#26
0
    async def test_expose_failed_s3_upload(self):
        """Test that we can take an exposure and that the file is saved locally
        if s3 upload fails
        """
        async with self.make_csc(
                initial_state=salobj.State.ENABLED,
                simulation_mode=FiberSpectrograph.SimulationMode.S3Server,
                index=FiberSpectrograph.SalIndex.RED,
                config_dir=TEST_CONFIG_DIR,
        ):
            # Check that we are properly in ENABLED at the start
            await self.assert_next_summary_state(salobj.State.ENABLED)
            assert self.csc.s3bucket_name == self.csc.s3bucket.name

            def bad_upload(*args, **kwargs):
                raise RuntimeError("Failed on purpose")

            self.csc.s3bucket.upload = bad_upload

            duration = 2  # seconds
            task = asyncio.create_task(
                self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT +
                                                 duration,
                                                 duration=duration))
            await self.check_exposureState(self.remote,
                                           ExposureState.INTEGRATING)
            # Wait for the exposure to finish.
            await task
            await self.check_exposureState(self.remote, ExposureState.DONE)

            # Check the large file event.
            data = await self.remote.evt_largeFileObjectAvailable.next(
                flush=False, timeout=STD_TIMEOUT)
            parsed_url = urllib.parse.urlparse(data.url)
            filepath = urllib.parse.unquote(parsed_url.path)
            assert parsed_url.scheme == "file"
            desired_path_start = "/tmp/" + self.csc.s3bucket.name + "/"
            start_nchar = len(desired_path_start)
            assert filepath[0:start_nchar] == desired_path_start

            # Minimally check the data file
            hdulist = astropy.io.fits.open(filepath)
            assert len(hdulist) == 2
            assert hdulist[0].header["ORIGIN"] == "FiberSpectrographCsc"
            assert hdulist[0].header["INSTRUME"] == "FiberSpectrograph.Red"

            # Check that out of range durations do not put us in FAULT,
            # and do not change the exposure state.
            duration = 1e-9  # seconds
            with salobj.assertRaisesAckError(
                    ack=salobj.SalRetCode.CMD_FAILED,
                    result_contains="Exposure duration",
            ):
                await asyncio.create_task(
                    self.remote.cmd_expose.set_start(timeout=STD_TIMEOUT,
                                                     duration=duration))
            # No ExposureState message should have been emitted.
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_exposureState.next(flush=False,
                                                         timeout=STD_TIMEOUT)
            # We should not have left ENABLED.
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_exposureState.next(flush=False,
                                                         timeout=STD_TIMEOUT)
            # Delete the file on success; leave it on failure, for diagnosis
            pathlib.Path(filepath).unlink()
示例#27
0
    async def test_logging(self) -> None:
        async with self.make_csc(initial_state=salobj.State.ENABLED,
                                 config_dir=TEST_CONFIG_DIR):
            logLevel = await self.remote.evt_logLevel.next(flush=False,
                                                           timeout=STD_TIMEOUT)
            assert logLevel.level == logging.INFO

            self.remote.evt_logMessage.flush()

            # We may still get one or two startup log messages
            # so read until we see the one we want.
            info_message = "test info message"
            self.csc.log.info(info_message)
            while True:
                msg = await self.remote.evt_logMessage.next(
                    flush=False, timeout=STD_TIMEOUT)
                if msg.message == info_message:
                    break
            assert msg.level == logging.INFO
            assert msg.traceback == ""

            filepath = pathlib.Path(__file__)
            subpath = "/".join(filepath.parts[-2:])
            assert msg.filePath.endswith(
                subpath), f"{msg.filePath} does not end with {subpath!r}"
            assert msg.functionName == "test_logging"
            assert msg.lineNumber > 0
            assert msg.process == os.getpid()

            # Test a warning with an unencodable character
            encodable_message = "test warn message"
            warn_message = encodable_message + "\u2013"
            self.csc.log.warning(warn_message)
            msg = await self.remote.evt_logMessage.next(flush=False,
                                                        timeout=STD_TIMEOUT)
            encodable_len = len(encodable_message)
            assert msg.message[0:encodable_len] == encodable_message
            assert msg.level == logging.WARNING
            assert msg.traceback == ""

            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_logMessage.next(flush=False,
                                                      timeout=NODATA_TIMEOUT)

            await self.remote.cmd_setLogLevel.set_start(level=logging.ERROR,
                                                        timeout=STD_TIMEOUT)

            logLevel = await self.remote.evt_logLevel.next(flush=False,
                                                           timeout=STD_TIMEOUT)
            assert logLevel.level == logging.ERROR

            info_message = "test info message"
            self.csc.log.info(info_message)
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_logMessage.next(flush=False,
                                                      timeout=NODATA_TIMEOUT)

            warn_message = "test warn message"
            self.csc.log.warning(warn_message)
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_logMessage.next(flush=False,
                                                      timeout=NODATA_TIMEOUT)

            with salobj.assertRaisesAckError():
                await self.remote.cmd_wait.set_start(duration=5,
                                                     timeout=STD_TIMEOUT)

            msg = await self.remote.evt_logMessage.next(flush=False,
                                                        timeout=STD_TIMEOUT)
            assert self.csc.exc_msg in msg.traceback
            assert "Traceback" in msg.traceback
            assert "RuntimeError" in msg.traceback
            assert msg.level == logging.ERROR
            assert msg.filePath.endswith("topics/controller_command.py")
            assert msg.functionName != ""
            assert msg.lineNumber > 0
            assert msg.process == os.getpid()
示例#28
0
    async def test_track(self):
        async with self.make_csc(initial_state=salobj.State.ENABLED):
            self.csc.configure(
                max_velocity=(100,) * 5,
                max_acceleration=(200,) * 5,
            )

            await self.assert_next_sample(
                self.remote.evt_atMountState, state=AtMountState.TRACKINGDISABLED
            )

            await self.assert_next_sample(
                self.remote.evt_m3State, state=M3State.NASMYTH1
            )

            # M3 should be in position, the other axes should not.
            for event in self.in_position_events:
                desired_in_position = event is self.remote.evt_m3InPosition
                await self.assert_next_sample(event, inPosition=desired_in_position)
            await self.assert_next_sample(
                self.remote.evt_allAxesInPosition, inPosition=False
            )

            await self.remote.cmd_startTracking.start(timeout=STD_TIMEOUT)

            await self.assert_next_sample(
                self.remote.evt_atMountState, state=AtMountState.TRACKINGENABLED
            )

            # attempts to set instrument port should fail
            with salobj.assertRaisesAckError():
                self.remote.cmd_setInstrumentPort.set(port=1)
                await self.remote.cmd_setInstrumentPort.start()

            start_tai = utils.current_tai()
            path_dict = dict(
                elevation=simactuators.path.PathSegment(
                    tai=start_tai, position=75, velocity=0.001
                ),
                azimuth=simactuators.path.PathSegment(
                    tai=start_tai, position=5, velocity=-0.001
                ),
                nasmyth1RotatorAngle=simactuators.path.PathSegment(
                    tai=start_tai, position=1, velocity=-0.001
                ),
            )
            trackId = 20  # arbitary
            while True:
                tai = utils.current_tai() + 0.1  # offset is arbitrary but reasonable
                target_kwargs = self.compute_track_target_kwargs(
                    tai=tai, path_dict=path_dict, trackId=trackId
                )
                await self.remote.cmd_trackTarget.set_start(**target_kwargs, timeout=1)

                target = await self.remote.evt_target.next(flush=False, timeout=1)
                self.assertTargetsAlmostEqual(self.remote.cmd_trackTarget.data, target)

                data = self.remote.evt_allAxesInPosition.get()
                if data.inPosition:
                    break

                if utils.current_tai() - start_tai > 5:
                    raise self.fail("Timed out waiting for slew to finish")

                await asyncio.sleep(0.5)

            print(f"test_track slew took {utils.current_tai() - start_tai:0.2f} sec")

            with self.assertRaises(asyncio.TimeoutError):
                await self.remote.evt_target.next(flush=False, timeout=0.1)

            for event in self.in_position_events:
                if event is self.remote.evt_m3InPosition:
                    continue  # M3 was already in position.
                if event is self.remote.evt_nasmyth2RotatorInPosition:
                    continue  # Nasmyth2 is not in use.
                await self.assert_next_sample(event, inPosition=True)

            await self.remote.cmd_stopTracking.start(timeout=1)
示例#29
0
    async def test_fault_method(self) -> None:
        """Test BaseCsc.fault with and without optional arguments."""
        async with self.make_csc(initial_state=salobj.State.STANDBY):
            await self.assert_next_summary_state(salobj.State.STANDBY)
            await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                          errorCode=0,
                                          errorReport="")

            code = 52
            report = "Report for error code"
            traceback = "Traceback for error code"

            # if an invalid code is specified then errorCode is not output
            # but the CSC stil goes into a FAULT state
            await self.csc.fault(code="not a valid code", report=report)
            await self.assert_next_summary_state(salobj.State.FAULT)
            with pytest.raises(asyncio.TimeoutError):
                await self.remote.evt_errorCode.next(flush=False,
                                                     timeout=NODATA_TIMEOUT)

            await self.remote.cmd_standby.start(timeout=STD_TIMEOUT)
            await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                          errorCode=0,
                                          errorReport="")
            await self.assert_next_summary_state(salobj.State.STANDBY)

            # if code is specified then errorReport is output;
            # first test with report and traceback specified,
            # then without, to make sure those values are not cached
            await self.csc.fault(code=code, report=report, traceback=traceback)
            await self.assert_next_summary_state(salobj.State.FAULT)
            await self.assert_next_sample(
                topic=self.remote.evt_errorCode,
                errorCode=code,
                errorReport=report,
                traceback=traceback,
            )

            # Try a disallowed command and check that the error report
            # is part of the traceback.
            with salobj.assertRaisesAckError(result_contains=report):
                await self.remote.cmd_wait.set_start(duration=5,
                                                     timeout=STD_TIMEOUT)

            await self.remote.cmd_standby.start(timeout=STD_TIMEOUT)
            await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                          errorCode=0,
                                          errorReport="")
            await self.assert_next_summary_state(salobj.State.STANDBY)

            await self.csc.fault(code=code, report="")
            await self.assert_next_summary_state(salobj.State.FAULT)
            await self.assert_next_sample(
                topic=self.remote.evt_errorCode,
                errorCode=code,
                errorReport="",
                traceback="",
            )

            await self.remote.cmd_standby.start(timeout=STD_TIMEOUT)
            await self.assert_next_sample(topic=self.remote.evt_errorCode,
                                          errorCode=0,
                                          errorReport="")
            await self.remote.cmd_exitControl.start(timeout=STD_TIMEOUT)
示例#30
0
    async def test_authorization(self) -> None:
        """Test authorization.

        For simplicity this test calls setAuthList without a +/- prefix.
        The prefix is tested elsewhere.
        """
        # TODO DM-36605 remove use of utils.modify_environ
        # once authlist support is enabled by default
        with utils.modify_environ(LSST_DDS_ENABLE_AUTHLIST="1"):
            async with self.make_csc(initial_state=salobj.State.ENABLED):
                await self.assert_next_sample(
                    self.remote.evt_authList,
                    authorizedUsers="",
                    nonAuthorizedCSCs="",
                )

                domain = self.csc.salinfo.domain

                # Note that self.csc and self.remote have the same user_host.
                csc_user_host = domain.user_host

                # Make a remote that pretends to be from a different CSC
                # and test non-authorized CSCs
                other_name_index = "Script:5"
                async with self.make_remote(
                        identity=other_name_index) as other_csc_remote:

                    all_csc_names = ["ATDome", "Hexapod:1", other_name_index]
                    for csc_names in all_permutations(all_csc_names):
                        csc_names_str = ", ".join(csc_names)
                        with self.subTest(csc_names_str=csc_names_str):
                            await self.remote.cmd_setAuthList.set_start(
                                nonAuthorizedCSCs=csc_names_str,
                                timeout=STD_TIMEOUT)
                            await self.assert_next_sample(
                                self.remote.evt_authList,
                                authorizedUsers="",
                                nonAuthorizedCSCs=", ".join(sorted(csc_names)),
                            )
                            if other_name_index in csc_names:
                                # A blocked CSC; this should fail.
                                with salobj.assertRaisesAckError(
                                        ack=salobj.SalRetCode.CMD_NOPERM):
                                    await other_csc_remote.cmd_wait.set_start(
                                        duration=0, timeout=STD_TIMEOUT)
                            else:
                                # Not a blocked CSC; this should work.
                                await other_csc_remote.cmd_wait.set_start(
                                    duration=0, timeout=STD_TIMEOUT)

                            # My user_host should work regardless of
                            # non-authorized CSCs.
                            await self.remote.cmd_wait.set_start(
                                duration=0, timeout=STD_TIMEOUT)

                            # Disabling authorization should always work
                            self.csc.cmd_wait.authorize = False
                            try:
                                await other_csc_remote.cmd_wait.set_start(
                                    duration=0, timeout=STD_TIMEOUT)
                            finally:
                                self.csc.cmd_wait.authorize = True

                # Test authorized users that are not me.
                # Reported auth users should always be in alphabetical order;
                # test this by sending users NOT in alphabetical order.
                all_other_user_hosts = [
                    f"notme{i}{csc_user_host}" for i in (3, 2, 1)
                ]
                other_user_host = all_other_user_hosts[1]

                async with self.make_remote(
                        identity=other_user_host) as other_user_remote:
                    for auth_user_hosts in all_permutations(
                            all_other_user_hosts):
                        users_str = ", ".join(auth_user_hosts)
                        with self.subTest(users_str=users_str):
                            await self.remote.cmd_setAuthList.set_start(
                                authorizedUsers=users_str,
                                nonAuthorizedCSCs="",
                                timeout=STD_TIMEOUT,
                            )
                            await self.assert_next_sample(
                                self.remote.evt_authList,
                                authorizedUsers=", ".join(
                                    sorted(auth_user_hosts)),
                                nonAuthorizedCSCs="",
                            )
                            if other_user_host in auth_user_hosts:
                                # An allowed user; this should work.
                                await other_user_remote.cmd_wait.set_start(
                                    duration=0, timeout=STD_TIMEOUT)
                            else:
                                # Not an allowed user; this should fail.
                                with salobj.assertRaisesAckError(
                                        ack=salobj.SalRetCode.CMD_NOPERM):
                                    await other_user_remote.cmd_wait.set_start(
                                        duration=0, timeout=STD_TIMEOUT)

                            # Temporarily disable authorization and try again;
                            # this should always work.
                            self.csc.cmd_wait.authorize = False
                            try:
                                await other_user_remote.cmd_wait.set_start(
                                    duration=0, timeout=STD_TIMEOUT)
                            finally:
                                self.csc.cmd_wait.authorize = True

                            # My user_host should work regardless of
                            # authorized users.
                            self.remote.salinfo.domain.identity = csc_user_host
                            await self.remote.cmd_wait.set_start(
                                duration=0, timeout=STD_TIMEOUT)