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)
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, )
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))
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)
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)
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
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)
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)
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)
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)
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)
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 )
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))
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="")
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)
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)
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)
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
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)
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)
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
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)
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)
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)
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)
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()
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()
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)
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)
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)