def __init__( self, domain, log, standardpath, externalpath, next_visit_callback=None, next_visit_canceled_callback=None, queue_callback=None, script_callback=None, min_sal_index=MIN_SAL_INDEX, max_sal_index=salobj.MAX_SAL_INDEX, verbose=False, ): if not os.path.isdir(standardpath): raise ValueError(f"No such dir standardpath={standardpath}") if not os.path.isdir(externalpath): raise ValueError(f"No such dir externalpath={externalpath}") for arg, arg_name in ( (next_visit_callback, "next_visit_callback"), (next_visit_canceled_callback, "next_visit_canceled_callback"), (queue_callback, "queue_callback"), (script_callback, "script_callback"), ): if arg is not None and not inspect.iscoroutinefunction(arg): raise TypeError( f"{arg_name}={arg} must be a coroutine or None") self.domain = domain self.log = log.getChild("QueueModel") self.standardpath = os.path.abspath(standardpath) self.externalpath = os.path.abspath(externalpath) self.next_visit_callback = next_visit_callback self.next_visit_canceled_callback = next_visit_canceled_callback self.queue_callback = queue_callback self.script_callback = script_callback self.min_sal_index = min_sal_index self.max_sal_index = max_sal_index self.verbose = verbose # queue of ScriptInfo instances self.queue = collections.deque() self.history = collections.deque(maxlen=MAX_HISTORY) self.current_script = None self._running = True self._enabled = False self._index_generator = index_generator(imin=min_sal_index, imax=max_sal_index) self._scripts_being_stopped = set() # use index=0 so we get messages for all scripts self.remote = salobj.Remote(domain=domain, name="Script", index=0, evt_max_history=0) self.remote.evt_metadata.callback = self._script_metadata_callback self.remote.evt_state.callback = self._script_state_callback if self.verbose: self.remote.evt_logMessage.callback = self._log_message_callback self.start_task = self.remote.start_task
def __init__(self, models, raw_telemetry, parameters=None, log=None): self.observing_list_dict = dict() self.index_gen = index_generator() self.validator = jsonschema.Draft7Validator(self.schema()) super().__init__(models, raw_telemetry, parameters, log=log)
def __init__( self, *, salinfo: SalInfo, attr_name: str, min_seq_num: typing.Optional[int] = 1, max_seq_num: int = MAX_SEQ_NUM, initial_seq_num: typing.Optional[int] = None, ) -> None: super().__init__(salinfo=salinfo, attr_name=attr_name) self.isopen = True self.min_seq_num = min_seq_num # record for unit tests self.max_seq_num = max_seq_num if min_seq_num is None: self._seq_num_generator: typing.Optional[ typing.Generator[int, None, None] ] = None else: self._seq_num_generator = utils.index_generator( imin=min_seq_num, imax=max_seq_num, i0=initial_seq_num ) # Command topics use a different partition name than # all other topics, including ackcmd, and the partition name # is part of the publisher and subscriber. # This split allows us to create just one subscriber and one publisher # for each Controller or Remote: # `Controller` only needs a cmd_subscriber and data_publisher, # `Remote` only needs a cmd_publisher and data_subscriber. if attr_name.startswith("cmd_"): publisher = salinfo.cmd_publisher else: publisher = salinfo.data_publisher self._writer = publisher.create_datawriter(self._topic, self.qos_set.writer_qos) self._has_data = False self._data = self.DataType() # Record which field names are float, double or array of either, # to make it easy to compare float fields with nan equal. self._float_field_names = set() for name, value in self._data.get_vars().items(): if isinstance(value, list): # In our SAL schemas arrays are fixed length # and must contain at least one element. elt = value[0] else: elt = value if isinstance(elt, float): self._float_field_names.add(name) salinfo.add_writer(self)
import numpy as np import pytest from lsst.ts import salobj from lsst.ts import utils # Long enough to perform any reasonable operation # including starting a CSC or loading a script (seconds) STD_TIMEOUT = 60 HISTORY_TIMEOUT_NAME = "LSST_DDS_HISTORYSYNC" INITIAL_HISTORY_TIMEOUT = os.environ.get(HISTORY_TIMEOUT_NAME, None) np.random.seed(47) index_gen = utils.index_generator() class RemoteTestCase(unittest.IsolatedAsyncioTestCase): async def test_constructor_include_exclude(self) -> None: """Test the include and exclude arguments for salobj.Remote.""" index = next(index_gen) async with salobj.Domain() as domain: salinfo = salobj.SalInfo(domain=domain, name="Test", index=index) # all possible expected topic names all_command_names = set(salinfo.command_names) all_event_names = set(salinfo.event_names) all_telemetry_names = set(salinfo.telemetry_names)
async def _slew_to( self, slew_cmd: typing.Any, slew_timeout: float, offset_cmd: typing.Any = None, stop_before_slew: bool = True, wait_settle: bool = True, check: typing.Any = None, ) -> None: """Encapsulate "slew" activities. Parameters ---------- slew_cmd : `coro` One of the slew commands from the mtptg remote. Command need to be setup before calling this method. slew_timeout : `float` Expected slewtime (seconds). stop_before_slew : `bool` Before starting a slew, send a stop target event? wait_settle : `bool` Once the telescope report in position, add an additional wait before returning? The time is controlled by the internal variable `self.tel_settle_time`. check : `types.SimpleNamespace` or `None`, optional Override internal `check` attribute with a user-provided one. By default (`None`) use internal attribute. """ assert self._dome_az_in_position is not None _check = self.check if check is None else check ccw_following = await self.rem.mtmount.evt_cameraCableWrapFollowing.aget( timeout=self.fast_timeout) if not ccw_following.enabled: # TODO DM-32545: Restore exception in slew method if dome # following is disabled. self.log.warning( "Camera cable wrap following disabled in MTMount.") if stop_before_slew: try: await self.stop_tracking() except Exception: self.log.exception("Error stop tracking.") try: # TODO DM-32546: Remove work around to rotator trajectory # problem that cannot complete 2 subsequent moves. The work # around consist of sending a move command to the rotator # current position then stopping, thus resetting the # trajectory. await self.rem.mtrotator.cmd_stop.start(timeout=self.fast_timeout) self.log.debug( f"Wait {self.fast_timeout}s for rotator to settle down.") await asyncio.sleep(self.fast_timeout) rotator_position = await self.rem.mtrotator.tel_rotation.aget( timeout=self.fast_timeout) rotator_reset_position = rotator_position.actualPosition + ( 0.1 if rotator_position.actualPosition < 0.0 else -0.1) self.log.debug( "Workaround for rotator trajectory problem. " f"Moving rotator to its current position: {rotator_reset_position:0.2f}" ) await self.move_rotator(position=rotator_reset_position) except Exception: self.log.exception("Error trying to reset rotator position.") track_id = next(self.track_id_gen) try: current_target = await self.rem.mtmount.evt_target.next( flush=True, timeout=self.fast_timeout) if track_id <= current_target.trackId: self.track_id_gen = utils.index_generator( current_target.trackId + 1) track_id = next(self.track_id_gen) except asyncio.TimeoutError: pass slew_cmd.data.trackId = track_id self.log.debug("Sending slew command.") if stop_before_slew: self.flush_offset_events() self.rem.mtrotator.evt_inPosition.flush() await slew_cmd.start(timeout=slew_timeout) self._dome_az_in_position.clear() if offset_cmd is not None: await offset_cmd.start(timeout=self.fast_timeout) self.log.debug("Scheduling check coroutines") self.scheduled_coro.append( asyncio.create_task( self.wait_for_inposition(timeout=slew_timeout, wait_settle=wait_settle))) self.scheduled_coro.append(asyncio.create_task( self.monitor_position())) for comp in self.components_attr: if getattr(_check, comp): getattr(self.rem, comp).evt_summaryState.flush() self.scheduled_coro.append( asyncio.create_task(self.check_component_state(comp))) await self.process_as_completed(self.scheduled_coro)
class RemoteGroupTestCase(metaclass=abc.ABCMeta): """Base class for testing groups of CSCs. Subclasses must: * Inherit both from this and `asynctest.TestCase`. * Override `basic_make_group` to make the script and any other controllers, remotes and such, and return a list of scripts, controllers and remotes that you made. A typical test will look like this: async def test_something(self): async with make_group(): # ... test something """ _index_iter = utils.index_generator() def run(self, result: typing.Any = None) -> None: """Override `run` to set a random LSST_DDS_PARTITION_PREFIX and set LSST_SITE=test for every test. https://stackoverflow.com/a/11180583 """ salobj.set_random_lsst_dds_partition_prefix() with utils.modify_environ(LSST_SITE="test"): super().run(result) # type: ignore @abc.abstractmethod async def basic_make_group(self, usage: typing.Optional[int] = None) -> None: """Make a group as self.group. Make all other controllers and remotes, as well and return a list of the items made. Returns ------- items : `List` [``any``] Controllers, Remotes and Group, or any other items for which to initially wait for ``item.start_task`` and finally wait for ``item.close()``. Notes ----- This is a coroutine in the unlikely case that you might want to wait for something. """ raise NotImplementedError() async def close(self) -> None: """Optional cleanup before closing.""" pass @contextlib.asynccontextmanager async def make_group( self, timeout: float = MAKE_TIMEOUT, usage: typing.Optional[int] = None, verbose: bool = False, ) -> typing.AsyncGenerator: """Create a Group. The group is accessed as ``self.group``. Parameters ---------- timeout : `float` Timeout (sec) for waiting for ``item.start_task`` and ``item.close()`` for each item returned by `basic_make_script`, and `self.close`. usage: `int` Combined enumeration with intended usage. verbose : `bool` Log data? This can be helpful for setting ``timeout``. """ items_to_await = await self.wait_for( self.basic_make_group(usage), timeout=timeout, description="self.basic_make_group()", verbose=verbose, ) try: await self.wait_for( asyncio.gather(*[ item.start_task for item in items_to_await if item.start_task is not None ]), timeout=timeout, description=f"item.start_task for {len(items_to_await)} items", verbose=verbose, ) yield finally: await self.wait_for( self.close(), timeout=timeout, description="self.close()", verbose=verbose, ) await self.wait_for( asyncio.gather(*[item.close() for item in items_to_await]), timeout=timeout, description=f"item.close() for {len(items_to_await)} items", verbose=verbose, ) async def wait_for(self, coro: typing.Awaitable, timeout: float, description: str, verbose: bool) -> typing.Any: """A wrapper around asyncio.wait_for that prints timing information. Parameters ---------- coro : ``awaitable`` Coroutine or task to await. timeout : `float` Timeout (seconds) description : `str` Description of what is being awaited. verbose : `bool` If True then print a message before waiting and another after that includes how long it waited. If False only print a message if the wait times out. """ t0 = time.monotonic() if verbose: print(f"wait for {description}") try: result = await asyncio.wait_for(coro, timeout=timeout) except asyncio.TimeoutError: dt = time.monotonic() - t0 print(f"{description} timed out after {dt:0.1f} seconds") raise if verbose: dt = time.monotonic() - t0 print(f"{description} took {dt:0.1f} seconds") return result
class BaseScriptTestCase(metaclass=abc.ABCMeta): """Base class for Script tests. Subclasses must: * Inherit both from this and `unittest.IsolatedAsyncioTestCase`. * Override `basic_make_script` to make the script and any other controllers, remotes and such, and return a list of scripts, controllers and remotes that you made. A typical test will look like this: async def test_something(self): async with make_script(): await self.configure_script(...) # ... test the results of configuring the script (self.script) await self.run_script() # (unless only testing configuration) # ... test the results of running the script """ _index_iter = utils.index_generator() @abc.abstractmethod async def basic_make_script(self, index): """Make a script as self.script. Make all other controllers and remotes, as well and return a list of the items made. Parameters ---------- index : `int` The SAL index of the script. Returns ------- items : `List` [``any``] Controllers, Remotes and Script, or any other items for which to initially wait for ``item.start_task`` and finally wait for ``item.close()``. Notes ----- This is a coroutine in the unlikely case that you might want to wait for something. """ raise NotImplementedError() async def close(self): """Optional cleanup before closing the scripts and etc.""" pass async def check_executable(self, script_path): """Check that an executable script can be launched. Parameter --------- script_path : `str` Full path to script. """ salobj.set_random_lsst_dds_partition_prefix() index = self.next_index() script_path = pathlib.Path(script_path).resolve() assert script_path.is_file() async with salobj.Domain() as domain, salobj.Remote( domain=domain, name="Script", index=index) as remote: initial_path = os.environ["PATH"] try: os.environ["PATH"] = str( script_path.parent) + ":" + initial_path process = await asyncio.create_subprocess_exec( str(script_path), str(index)) state = await remote.evt_state.next(flush=False, timeout=MAKE_TIMEOUT) assert state.state == Script.ScriptState.UNCONFIGURED finally: process.terminate() os.environ["PATH"] = initial_path async def configure_script(self, **kwargs): """Configure the script and set the group ID (if using ts_salobj 4.5 or later). Sets the script state to UNCONFIGURED. This allows you to call configure_script multiple times. Parameters ---------- kwargs : `dict` Keyword arguments for configuration. Returns ------- config : `types.SimpleNamespace` ``kwargs`` expressed as a SimpleNamespace. This is provided as a convenience, to avoid boilerplate and duplication in your unit tests. The data is strictly based on the input arguments; it has nothing to do with the script. """ await self.script.set_state(Script.ScriptState.UNCONFIGURED) config = types.SimpleNamespace(**kwargs) config_data = self.script.cmd_configure.DataType() if kwargs: config_data.config = yaml.safe_dump(kwargs) await self.script.do_configure(config_data) assert self.script.state.state == Script.ScriptState.CONFIGURED if hasattr(self.script, "cmd_setGroupId"): group_id_data = self.script.cmd_setGroupId.DataType( groupId=astropy.time.Time.now().isot) await self.script.do_setGroupId(group_id_data) return config @contextlib.asynccontextmanager async def make_script(self, log_level=logging.INFO, timeout=MAKE_TIMEOUT, verbose=False): """Create a Script. The script is accessed as ``self.script``. Parameters ---------- name : `str` Name of SAL component. log_level : `int` (optional) Logging level, such as `logging.INFO`. timeout : `float` Timeout (sec) for waiting for ``item.start_task`` and ``item.close()`` for each item returned by `basic_make_script`, and `self.close`. verbose : `bool` Log data? This can be helpful for setting ``timeout``. """ salobj.set_random_lsst_dds_partition_prefix() items_to_await = await self.wait_for( self.basic_make_script(index=self.next_index()), timeout=timeout, description="self.basic_make_script()", verbose=verbose, ) try: await self.wait_for( asyncio.gather(*[item.start_task for item in items_to_await]), timeout=timeout, description=f"item.start_task for {len(items_to_await)} items", verbose=verbose, ) yield finally: await self.wait_for( self.close(), timeout=timeout, description="self.close()", verbose=verbose, ) await self.wait_for( asyncio.gather(*[item.close() for item in items_to_await]), timeout=timeout, description=f"item.close() for {len(items_to_await)} items", verbose=verbose, ) def next_index(self): return next(self._index_iter) async def run_script(self): """Run the script. Requires that the script be configured and the group ID set (if using ts_salobj 4.5 or later). """ run_data = self.script.cmd_run.DataType() await self.script.do_run(run_data) await self.script.done_task assert self.script.state.state == Script.ScriptState.DONE async def wait_for(self, coro, timeout, description, verbose): """A wrapper around asyncio.wait_for that prints timing information. Parameters ---------- coro : ``awaitable`` Coroutine or task to await. timeout : `float` Timeout (seconds) description : `str` Description of what is being awaited. verbose : `bool` If True then print a message before waiting and another after that includes how long it waited. If False only print a message if the wait times out. """ t0 = time.monotonic() if verbose: print(f"wait for {description}") try: result = await asyncio.wait_for(coro, timeout=timeout) except asyncio.TimeoutError: dt = time.monotonic() - t0 print(f"{description} timed out after {dt:0.1f} seconds") raise if verbose: dt = time.monotonic() - t0 print(f"{description} took {dt:0.1f} seconds") return result
import json from lsst.ts import salobj from lsst.ts.utils import index_generator from commander_utils import NumpyEncoder index_gen = index_generator() async def test_successful_command(client): # Arrange # setup dds / csc salobj.set_random_lsst_dds_partition_prefix() next(index_gen) csc = salobj.TestCsc(index=1, config_dir=None, initial_state=salobj.State.ENABLED) await csc.start_task # build data cmd_data = csc.make_random_scalars_dict() data = json.loads( json.dumps( { "csc": "Test", "salindex": 1, "cmd": "cmd_setScalars", "params": cmd_data, }, cls=NumpyEncoder, ) ) # Act
class BaseCscTestCase(metaclass=abc.ABCMeta): """Base class for CSC tests. Subclasses must: * Inherit both from this and `unittest.IsolatedAsyncioTestCase`. * Override `basic_make_csc` to return a CSC. Also we suggest: * Add a method ``test_standard_state_transitions`` which calls `check_standard_state_transitions`. * Add a method ``test_bin_script`` which calls `check_bin_script`, assuming you have a binary script to run your CSC. """ _index_iter = utils.index_generator() def run(self, result: typing.Any = None) -> None: # type: ignore """Set a random LSST_DDS_PARTITION_PREFIX and set LSST_SITE=test for every test. Unlike setUp, a user cannot forget to override this. (This is also a good place for context managers). """ testutils.set_random_lsst_dds_partition_prefix() # set LSST_SITE using os.environ instead of utils.modify_environ # so that check_bin_script works. os.environ["LSST_SITE"] = "test" super().run(result) # type: ignore @abc.abstractmethod def basic_make_csc( self, initial_state: typing.Union[sal_enums.State, int], config_dir: typing.Union[str, pathlib.Path, None], simulation_mode: int, **kwargs: typing.Any, ) -> base_csc.BaseCsc: """Make and return a CSC. Parameters ---------- initial_state : `lsst.ts.salobj.State` or `int` The initial state of the CSC. config_dir : `str` or `pathlib.Path` or `None` Directory of configuration files, or None for the standard configuration directory (obtained from `ConfigureCsc._get_default_config_dir`). simulation_mode : `int` Simulation mode. kwargs : `dict` Extra keyword arguments, if needed. """ raise NotImplementedError() def next_index(self) -> int: """Get the next SAL index.""" return next(self._index_iter) @contextlib.asynccontextmanager async def make_csc( self, initial_state: sal_enums.State = sal_enums.State.STANDBY, config_dir: typing.Union[str, pathlib.Path, None] = None, simulation_mode: int = 0, log_level: typing.Optional[int] = None, timeout: float = STD_TIMEOUT, **kwargs: typing.Any, ) -> typing.AsyncGenerator[None, None]: """Create a CSC and remote and wait for them to start, after setting a random $LSST_DDS_PARTITION_PREFIX. The csc is accessed as ``self.csc`` and the remote as ``self.remote``. Reads and checks all but the last ``summaryState`` event during startup. Parameters ---------- name : `str` Name of SAL component. initial_state : `lsst.ts.salobj.State` or `int`, optional The initial state of the CSC. Defaults to STANDBY. config_dir : `str`, optional Directory of configuration files, or `None` (the default) for the standard configuration directory (obtained from `ConfigureCsc._get_default_config_dir`). simulation_mode : `int`, optional Simulation mode. Defaults to 0 because not all CSCs support simulation. However, tests of CSCs that support simulation will almost certainly want to set this nonzero. log_level : `int` or `None`, optional Logging level, such as `logging.INFO`. If `None` then do not set the log level, leaving the default behavior of `SalInfo`: increase the log level to INFO. timeout : `float`, optional Time limit for the CSC to start (seconds). **kwargs : `dict`, optional Extra keyword arguments for `basic_make_csc`. For a configurable CSC this may include ``override``, especially if ``initial_state`` is DISABLED or ENABLED. """ # Redundant with setUp, but preserve in case a subclass # forgets to call super().setUp() testutils.set_random_lsst_dds_partition_prefix() items_to_close: typing.List[typing.Union[base_csc.BaseCsc, Remote]] = [] try: self.csc = self.basic_make_csc( initial_state=initial_state, config_dir=config_dir, simulation_mode=simulation_mode, **kwargs, ) items_to_close.append(self.csc) self.remote = Remote( domain=self.csc.domain, name=self.csc.salinfo.name, index=self.csc.salinfo.index, ) items_to_close.append(self.remote) if log_level is not None: self.csc.log.setLevel(log_level) await asyncio.wait_for( asyncio.gather(self.csc.start_task, self.remote.start_task), timeout=timeout, ) if initial_state != self.csc.default_initial_state: # Check all expected summary states expect the final state. # That is omitted for backwards compatibility. expected_states = get_expected_summary_states( initial_state=self.csc.default_initial_state, final_state=initial_state, )[:-1] for state in expected_states: await self.assert_next_summary_state(state) yield except Exception as e: print(f"BaseCscTestCase.make_csc failed: {e!r}") raise finally: for item in items_to_close: await item.close() async def assert_next_summary_state( self, state: sal_enums.State, flush: bool = False, timeout: float = STD_TIMEOUT, remote: typing.Optional[Remote] = None, ) -> None: """Wait for and check the next ``summaryState`` event. Parameters ---------- state : `lsst.ts.salobj.State` or `int` Desired summary state. flush : `bool`, optional Flush the read queue before waiting? timeout : `float`, optional Time limit for getting the data sample (sec). remote : `Remote`, optional Remote to use; ``self.remote`` if None. """ if remote is None: remote = self.remote await self.assert_next_sample( topic=remote.evt_summaryState, # type: ignore flush=flush, timeout=timeout, summaryState=state, ) async def assert_next_sample( self, topic: ReadTopic, flush: bool = False, timeout: float = STD_TIMEOUT, **kwargs: typing.Any, ) -> type_hints.BaseMsgType: """Wait for the next data sample for the specified topic, check specified fields for equality, and return the data. Parameters ---------- topic : `topics.ReadTopic` Topic to read, e.g. ``remote.evt_logMessage``. flush : `bool`, optional Flush the read queue before waiting? timeout : `double`, optional Time limit for getting the data sample (sec). kwargs : `dict` Dict of field_name: expected_value The specified fields will be checked for equality. Returns ------- data : topic data type The data read. """ data = await topic.next(flush=flush, timeout=timeout) for field_name, expected_value in kwargs.items(): read_value = getattr(data, field_name, None) if read_value is None: raise AssertionError( f"No such field {field_name} in topic {topic}") if isinstance(expected_value, enum.IntEnum): try: read_value = type(expected_value)(read_value) except Exception: pass assert ( read_value == expected_value ), f"Failed on field {field_name}: read {read_value!r} != expected {expected_value!r}" return data async def check_bin_script( self, name: str, index: int, exe_name: str, default_initial_state: sal_enums.State = sal_enums.State.STANDBY, initial_state: typing.Optional[sal_enums.State] = None, override: typing.Optional[str] = None, cmdline_args: typing.Sequence[str] = (), timeout: float = STD_TIMEOUT, ) -> None: """Test running the CSC command line script. Parameters ---------- name : `str` Name of SAL component, e.g. "Rotator" index : `int` or `None` SAL index of component. exe_name : `str` Name of executable, e.g. "run_rotator.py" default_initial_state : `lsst.ts.salobj.State`, optional The default initial state of the CSC. Ignored unless `initial_state` is None. initial_state : `lsst.ts.salobj.State` or `int` or `None`, optional The desired initial state of the CSC; used to specify the ``--state`` command-argument. override : `str` or `None`, optional Value for the ``--override`` command-line argument, which is omitted if override is None. cmdline_args : `List` [`str`] Additional command-line arguments, such as "--simulate". timeout : `float`, optional Time limit for the CSC to start and output the summaryState event. """ # Redundant with setUp, but preserve in case a subclass # forgets to call super().setUp() testutils.set_random_lsst_dds_partition_prefix() exe_path = shutil.which(exe_name) if exe_path is None: raise AssertionError( f"Could not find bin script {exe_name}; did you setup or install this package?" ) args = [exe_name] if index not in (None, 0): args += [str(index)] if initial_state is None: expected_states = [default_initial_state] else: args += ["--state", initial_state.name.lower()] expected_states = get_expected_summary_states( initial_state=default_initial_state, final_state=initial_state, ) if override is not None: args += ["--override", override] args += cmdline_args async with Domain() as domain, Remote(domain=domain, name=name, index=index) as self.remote: print("check_bin_script running:", " ".join(args)) process = await asyncio.create_subprocess_exec( *args, stderr=subprocess.PIPE, ) try: for state in expected_states: await self.assert_next_summary_state(state, timeout=timeout) if override: # The override should appear in # evt_configurationApplied.configurations data = await self.assert_next_sample( topic=self.remote. evt_configurationApplied, # type: ignore ) assert override in data.configurations # type: ignore finally: if process.returncode is None: process.terminate() await asyncio.wait_for(process.wait(), timeout=STD_TIMEOUT) else: print("Warning: subprocess had already quit.") try: assert process.stderr is not None # make mypy happy errbytes = await process.stderr.read() print("Subprocess stderr: ", errbytes.decode()) except Exception as e: print(f"Could not read subprocess stderr: {e}") async def check_standard_state_transitions( self, enabled_commands: typing.Sequence[str], skip_commands: typing.Optional[typing.Sequence[str]] = None, override: str = "", timeout: float = STD_TIMEOUT, ) -> None: """Test standard CSC state transitions. Parameters ---------- enabled_commands : `List` [`str`] List of CSC-specific commands that are valid in the enabled state. Need not include the standard commands, which are "disable" and "setLogLevel" (which is valid in any state). skip_commands : `List` [`str`] or `None`, optional List of commands to skip. override : `str`, optional Configuration override file to apply when the CSC is taken from state `State.STANDBY` to `State.DISABLED`. timeout : `float`, optional Time limit for state transition commands (seconds). Notes ----- ``timeout`` is only used for state transition commands that are expected to succceed. ``STD_TIMEOUT`` is used for things that should happen quickly: * Commands that should fail, due to the CSC being in the wrong state. * The ``summaryState`` event after each state transition: """ enabled_commands = tuple(enabled_commands) skip_commands = tuple(skip_commands) if skip_commands else () # Start in STANDBY state. assert self.csc.summary_state == sal_enums.State.STANDBY await self.assert_next_summary_state(sal_enums.State.STANDBY) await self.check_bad_commands( good_commands=("start", "exitControl", "setAuthList", "setLogLevel") + skip_commands) # Send start; new state is DISABLED. await self.remote.cmd_start.set_start( # type: ignore configurationOverride=override, timeout=timeout) assert self.csc.summary_state == sal_enums.State.DISABLED await self.assert_next_summary_state(sal_enums.State.DISABLED) await self.check_bad_commands( good_commands=("enable", "standby", "setAuthList", "setLogLevel") + skip_commands) # Send enable; new state is ENABLED. await self.remote.cmd_enable.start(timeout=timeout) # type: ignore assert self.csc.summary_state == sal_enums.State.ENABLED await self.assert_next_summary_state(sal_enums.State.ENABLED) all_enabled_commands = tuple( sorted( set(("disable", "setAuthList", "setLogLevel")) | set(enabled_commands))) await self.check_bad_commands(good_commands=all_enabled_commands + skip_commands) # Send disable; new state is DISABLED. await self.remote.cmd_disable.start(timeout=timeout) # type: ignore assert self.csc.summary_state == sal_enums.State.DISABLED await self.assert_next_summary_state(sal_enums.State.DISABLED) # Send standby; new state is STANDBY. await self.remote.cmd_standby.start(timeout=timeout) # type: ignore assert self.csc.summary_state == sal_enums.State.STANDBY await self.assert_next_summary_state(sal_enums.State.STANDBY) # Send exitControl; new state is OFFLINE. await self.remote.cmd_exitControl.start(timeout=timeout ) # type: ignore assert self.csc.summary_state == sal_enums.State.OFFLINE await self.assert_next_summary_state(sal_enums.State.OFFLINE) async def check_bad_commands( self, bad_commands: typing.Optional[typing.Sequence[str]] = None, good_commands: typing.Optional[typing.Sequence[str]] = None, ) -> None: """Check that bad commands fail. Parameters ---------- bad_commands : `List`[`str`] or `None`, optional Names of bad commands to try, or None for all commands. good_commands : `List`[`str`] or `None`, optional Names of good commands to skip, or None to skip none. Notes ----- If a command appears in both lists, it is considered a good command, so it is skipped. """ if bad_commands is None: bad_commands = self.remote.salinfo.command_names if good_commands is None: good_commands = () commands = self.remote.salinfo.command_names for command in commands: if command in good_commands: continue with self.subTest(command=command): # type: ignore cmd_attr = getattr(self.remote, f"cmd_{command}") with testutils.assertRaisesAckError( ack=sal_enums.SalRetCode.CMD_FAILED): await cmd_attr.start(timeout=STD_TIMEOUT)