def test_scan_id_persists_between_executions( script_that_increments_and_returns_scan_id, ): """ The scan ID should be shared and persisted between process executions. """ manager = ProcessManager() queue = multiprocessing.Queue() run_args = ProcedureInput(queue) pid = manager.create( script_uri=script_that_increments_and_returns_scan_id, init_args=ProcedureInput(), ) manager.run(pid, run_args=run_args) wait_for_process_to_complete(manager) scan_id = queue.get(timeout=1) pid = manager.create( script_uri=script_that_increments_and_returns_scan_id, init_args=ProcedureInput(), ) manager.run(pid, run_args=run_args) wait_for_process_to_complete(manager) next_scan_id = queue.get(timeout=1) assert next_scan_id == scan_id + 1
def test_ses_prepare_call_sequence_and_returns_summary_for_created_process(): """ Verify that ScriptExecutionService.prepare() calls the appropriate domain object methods for process creation and returns the expected summary object """ script_uri = "test://test.py" cmd = PrepareProcessCommand(script_uri=script_uri, init_args=ProcedureInput()) procedure = Procedure(script_uri, procedure_id=123) procedures = {123: procedure} expected = ProcedureSummary( id=123, script_uri=procedure.script_uri, script_args=procedure.script_args, history=procedure.history, state=procedure.state, ) with mock.patch( "oet.procedure.application.application.domain.ProcessManager" ) as mock_pm: # get the mock ProcessManager instance instance = mock_pm.return_value # tell ProcessManager.create to return PID 123, which is subsequently # used for lookup instance.create.return_value = 123 # the manager's procedures attribute holds created procedures and is # used for retrieval instance.procedures = procedures service = ScriptExecutionService() returned = service.prepare(cmd) instance.create.assert_called_once_with(script_uri, init_args=ProcedureInput()) assert returned == expected
def test_process_manager_updates_state_of_completed_procedures(manager, script_path): """ Verify that ProcessManager updates procedure state to COMPLETED when finished successfully """ pid = manager.create(script_path, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) wait_for_process_to_complete(manager) assert manager.procedures[pid].state == ProcedureState.COMPLETED
def test_process_manager_sets_running_to_none_on_script_failure(manager, fail_script): """ Verify that ProcessManager sets running procedure attribute to None when script execution fails """ pid = manager.create(fail_script, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) wait_for_process_to_complete(manager) assert manager.running is None
def test_process_manager_updates_procedure_state_on_stop(manager, abort_script): """ Verify that ProcessManager removes an stopped procedure from the procedures list """ pid = manager.create(abort_script, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) manager.stop(pid) wait_for_process_to_complete(manager) assert manager.procedures[pid].state == ProcedureState.STOPPED
def test_procedure_input_eq_works_as_expected(): """ Verify ProcedureInput equality """ pi1 = ProcedureInput(1, 2, 3, a=1, b=2) pi2 = ProcedureInput(1, 2, 3, a=1, b=2) pi3 = ProcedureInput(4, a=1) assert pi1 == pi2 assert pi1 != pi3 assert pi1 != object()
def test_process_manager_sets_running_to_none_on_stop(manager, abort_script): """ Verify that ProcessManager sets running procedure attribute to None when script is stopped """ pid = manager.create(abort_script, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) manager.stop(pid) wait_for_process_to_complete(manager) assert manager.running is None
def test_process_manager_run_changes_state_of_procedure_to_running( manager, script_path, process_cleanup ): """ Verify that procedure state changes when ProcessManager starts procedure execution """ pid = manager.create(script_path, init_args=ProcedureInput()) assert manager.procedures[pid].state == ProcedureState.CREATED manager.run(pid, run_args=ProcedureInput()) assert manager.procedures[pid].state == ProcedureState.RUNNING
def test_process_manager_run_fails_on_process_that_is_already_running( manager, script_path, process_cleanup ): """ Verify that an exception is raised when requesting run() for a procedure that is already running """ pid = manager.create(script_path, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) with pytest.raises(ValueError): manager.run(pid, run_args=ProcedureInput())
def test_process_manager_stop_terminates_the_process(manager, abort_script): """ Verify that ProcessManager stops a script execution """ pid = manager.create(abort_script, init_args=ProcedureInput()) created = manager.procedures[pid] queue = multiprocessing.Queue() manager.run(pid, run_args=ProcedureInput(queue, created)) manager.stop(pid) wait_for_process_to_complete(manager, timeout=3) assert queue.empty()
def test_process_manager_sets_running_to_none_when_process_completes( manager, script_path ): """ Verify that ProcessManager sets running procedure attribute to None when process completes """ pid = manager.create(script_path, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) wait_for_process_to_complete(manager) assert manager.running is None
def test_process_manager_updates_procedure_state_on_script_failure( manager, fail_script ): """ Verify that ProcessManager removes a failed procedure from the procedures list """ pid = manager.create(fail_script, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) wait_for_process_to_complete(manager) assert manager.procedures[pid].state == ProcedureState.FAILED
def test_calling_process_manager_run_sets_run_args_on_procedure( manager, script_path, process_cleanup ): """ Verify that the arguments to ProcessManager run() are captured and stored on the procedure instance """ pid = manager.create(script_path, init_args=ProcedureInput()) expected = ProcedureInput(5, 6, 7, kw3="c", kw4="d") created = manager.procedures[pid] manager.run(pid, run_args=expected) assert created.script_args["run"] == expected
def test_process_manager_create_adds_new_procedure(manager, script_path): """ Verify that ProcessManager keeps references to the processes it creates """ len_before = len(manager.procedures) manager.create(script_path, init_args=ProcedureInput()) assert len(manager.procedures) == len_before + 1
def test_process_manager_create_sets_pid_of_new_procedure(manager, script_path): """ Verify that procedures are assigned IDs on process creation """ pid = manager.create(script_path, init_args=ProcedureInput()) created = manager.procedures[pid] assert created.id == pid
def test_process_manager_run_fails_on_invalid_pid(manager): """ Verify that an exception is raised when run() is requested for an invalid PID """ with pytest.raises(ValueError): manager.run(321, run_args=ProcedureInput())
def test_procedure_input_accepts_expected_constructor_values(): """ Verify that ProcedureInput arguments are slurped into positional and keyword/value attributes. """ procedure_input = ProcedureInput(1, 2, 3, a=1, b=2) assert procedure_input.args == (1, 2, 3) assert procedure_input.kwargs == dict(a=1, b=2)
def test_process_manager_updates_history_of_completed_procedures(manager, script_path): """ Verify that ProcessManager updates procedure state to COMPLETED when finished successfully """ pid = manager.create(script_path, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) wait_for_process_to_complete(manager) procedure = manager.procedures[pid] assert ProcedureState.CREATED in procedure.history.process_states assert isinstance(procedure.history.process_states[ProcedureState.CREATED], float) assert ProcedureState.RUNNING in procedure.history.process_states assert isinstance(procedure.history.process_states[ProcedureState.RUNNING], float) assert ProcedureState.COMPLETED in procedure.history.process_states assert isinstance(procedure.history.process_states[ProcedureState.COMPLETED], float) assert procedure.history.stacktrace is None
def test_process_manager_updates_procedure_history_on_script_failure( manager, fail_script ): """ Verify that ProcessManager updates FAILED to procedure history when script fails """ pid = manager.create(fail_script, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput()) wait_for_process_to_complete(manager) procedure = manager.procedures[pid] assert ProcedureState.CREATED in procedure.history.process_states assert isinstance(procedure.history.process_states[ProcedureState.CREATED], float) assert ProcedureState.RUNNING in procedure.history.process_states assert isinstance(procedure.history.process_states[ProcedureState.RUNNING], float) assert ProcedureState.FAILED in procedure.history.process_states assert isinstance(procedure.history.process_states[ProcedureState.FAILED], float) assert procedure.history.stacktrace is not None
def test_runtime_arguments_are_passed_to_user_script(procedure): """ Verify that arguments passed from procedure are accessible in the user script """ run_args = ProcedureInput(5, 6, 7, kw3="c", kw4="d") procedure.script_args["run"] = run_args procedure.user_module = MagicMock() procedure.run() procedure.user_module.main.assert_called_with(5, 6, 7, kw3="c", kw4="d")
def test_process_manager_create_captures_initialisation_arguments(manager, script_path): """ Verify that ProcessManager passes through initialisation arguments to the procedures it creates """ expected = ProcedureInput(1, 2, 3, a=4, b=5) pid = manager.create(script_path, init_args=expected) created = manager.procedures[pid] assert created.script_args["init"] == expected
def test_process_manager_run_executes_procedure_start(manager, process_cleanup): """ Verify that a call to ProcessManager run() executes Procedure.start() instead of Procedure.run(). This confirms that Procedure will execute in a child process. """ procedure = MagicMock() manager.procedures[1] = procedure manager.run(1, run_args=ProcedureInput()) procedure.start.assert_called_once()
def create_empty_procedure_summary( procedure_id: int, script_uri: str, history: ProcedureHistory ): """ Utility function to create a null procedure summary. The returned procedure defines zero script arguments. :param procedure_id: procedure ID :param script_uri: path to script :param history: Procedure history :return: corresponding ProcedureSummary object """ return ProcedureSummary( id=procedure_id, script_uri=script_uri, script_args={"init": ProcedureInput(), "run": ProcedureInput()}, history=history, state=ProcedureState.CREATED, )
def test_process_manager_stop_fails_on_process_that_is_not_running( manager, script_path ): """ Verify that an exception is raised when requesting stop() for a procedure that is not running """ pid = manager.create(script_path, init_args=ProcedureInput()) with pytest.raises(ValueError): manager.stop(pid)
def test_process_manager_run_sets_running_procedure(manager, tmpdir, process_cleanup): """ Verify that ProcessManager sets the running procedure attribute appropriately when run() is called """ script_path = tmpdir.join("sleep.py") script_path.write( """ def main(shutdown_event, *args, **kwargs): while not shutdown_event.is_set(): continue """ ) script_uri = f"file://{str(script_path)}" shutdown_event = multiprocessing.Event() pid = manager.create(script_uri, init_args=ProcedureInput()) manager.run(pid, run_args=ProcedureInput(shutdown_event)) assert manager.running == manager.procedures[pid] shutdown_event.set()
def test_process_manager_create_removes_oldest_procedure_on_max_procedures( manager, script_path ): """ Verify that ProcessManager removes the oldest procedure when the maximum number of saved procedures is reached """ manager.procedures.clear() max_procedures = PROCEDURE_QUEUE_MAX_LENGTH for _ in range(len(manager.procedures), max_procedures): manager.create(script_path, init_args=ProcedureInput()) assert len(manager.procedures) == max_procedures assert 1 in manager.procedures # adding procedure should not increase the number of procedures # and should remove the oldest procedure (with ID 1) manager.create(script_path, init_args=ProcedureInput()) assert len(manager.procedures) == max_procedures assert 1 not in manager.procedures
def test_ses_start_calls_process_manager_function_and_returns_summary(): """ Verify that ScriptExecutionService.start() calls the appropriate domain object methods for starting process execution and returns the expected summary object """ script_uri = "test://test.py" cmd = StartProcessCommand(process_uid=123, run_args=ProcedureInput()) procedure = Procedure(script_uri, procedure_id=123) procedures = {123: procedure} expected = ProcedureSummary( id=123, script_uri=procedure.script_uri, script_args=procedure.script_args, history=procedure.history, state=procedure.state, ) with mock.patch( "oet.procedure.application.application.domain.ProcessManager" ) as mock_pm: # get the mock ProcessManager instance instance = mock_pm.return_value # the manager's procedures attribute holds created procedures and is # used for retrieval instance.procedures = procedures service = ScriptExecutionService() returned = service.start(cmd) # service should call run() and return the summary for the executed # procedure instance.run.assert_called_once_with(123, run_args=ProcedureInput()) assert returned == expected # we don't validate or modify procedure state, so this should still be # READY rather than RUNNING assert returned.state == ProcedureState.CREATED
def test_ses_stop_calls_process_manager_function_with_no_script_execution(abort_script): """ Verify that ScriptExecutionService.stop() calls the appropriate domain object methods for stopping process execution without executing abort python script. """ # PID of running process running_pid = 123 # Test script/procedures will target sub-array 2 init_args = ProcedureInput(subarray_id=2) # Create Procedure representing the script to be stopped procedure_to_stop = Procedure("test://a") procedure_to_stop.script_args["init"] = init_args # Prepare a dict of PIDs to Procedures that we can use to mock the internal # data structure held by ProcessManager. process_manager_procedures = {running_pid: procedure_to_stop} cmd = StopProcessCommand(process_uid=running_pid, run_abort=False) # returned summary list should be empty if abort script is bypassed expected = [] with mock.patch( "oet.procedure.application.application.domain.ProcessManager" ) as mock_pm: # get the mock ProcessManager instance, preparing it for SES access instance = mock_pm.return_value instance.procedures = process_manager_procedures service = ScriptExecutionService(abort_script_uri=abort_script) returned = service.stop(cmd) # service should call stop() and return empty list instance.stop.assert_called_once_with(running_pid) assert returned == expected
def test_ses_get_subarray_id_for_requested_pid(): """ Verify that the private method _get_subarray_id returns subarray id correctly """ subarray_id = 123 process_pid = 456 procedure = Procedure("test://a") init_args = ProcedureInput(subarray_id=subarray_id) procedure.script_args["init"] = init_args procedures = {process_pid: procedure} process_summary = ProcedureSummary( id=process_pid, script_uri=procedure.script_uri, script_args=procedure.script_args, history=procedure.history, state=procedure.state, ) expected = [process_summary] with mock.patch( "oet.procedure.application.application.domain.ProcessManager" ) as mock_pm: # get the mock ProcessManager instance instance = mock_pm.return_value # the manager's procedures attribute holds created procedures and is # used for retrieval instance.procedures = procedures service = ScriptExecutionService() returned = service._get_subarray_id( process_pid ) # pylint: disable=protected-access assert returned == expected[0].script_args["init"].kwargs["subarray_id"]
def test_procedure_init_stores_initial_arguments(procedure): """ Verify that the procedure constructor arguments are captured and persisted on the procedure instance. """ assert procedure.script_args["init"] == ProcedureInput(1, 2, 3, kw1="a", kw2="b")