def test_run_step(self): def action_function_a(): return 'test return value' mock_args = MagicMock() mock_kwargs = MagicMock() mock_pre_processor = MagicMock(return_value=(mock_args, mock_kwargs)) step = Step(action_function_a, pre_processor=mock_pre_processor) step.context = 'mock_context' step.tags['n'] = 0 process_patcher = patch('autotrail.layer1.trail.Process') mock_process = process_patcher.start() run_step(step) process_patcher.stop() args, kwargs = mock_process.call_args arg_step, arg_args, arg_kwargs = kwargs['args'] self.assertEqual(step, arg_step) self.assertEqual(arg_args, mock_args) self.assertEqual(arg_kwargs, mock_kwargs) self.assertEqual(kwargs['target'], step_manager) self.assertIn(mock.call().start(), mock_process.mock_calls) self.assertEqual(step.state, step.RUN)
def create_conditional_step(action_function, **tags): """Factory to create a Step that acts like a guard to a branch. If this step fails, all subsequent steps in that branch, will be skipped. Consider the following trail where 'b' is the "conditional" step. +--> d -->+ +--> b -->| |-->+ | +--> e -->+ | a -->| |--> f +--> c --------------- >+ Lets say we want the branch "b" to run only if some condition is matched. We can consider the step 'b' to be the 'conditional' for this branch, i.e., it should succeed only if the condition is satisfied. If the condition is not satisfied, it will fail. Failure of 'b' will have the effect of skipping the progeny i.e., if 'b' fails, steps d and e will be "skipped". Progeny here is strict i.e., progeny of 'b' are 'd' and 'e' but not 'f' (since 'f' has a parent that is not related to 'b'). This is done by setting two of the step's attributes: Step.pause_on_fail = True -- So that the step fails instead of being moved to Step.PAUSE. Step.skip_progeny_on_failure = True -- So that the progeny are skipped on failure. Returns: Step object whose pause_on_fail is False and skip_progeny_on_failure is True. """ step = Step(action_function, **tags) step.pause_on_fail = False step.skip_progeny_on_failure = True return step
def test_deserialize_trail_mismatched_trail(self): # Make another DAG: # +--> step_d -->+ # step_a -->| |--> step_b # +--> step_c -->+ def action_a(): pass def action_b(): pass def action_c(): pass def action_d(): pass step_a = Step(action_a) step_b = Step(action_b) step_c = Step(action_c) step_d = Step(action_d) trail_definition = [ (step_a, step_d), (step_a, step_c), (step_d, step_b), (step_c, step_b), ] mismatching_trail_data = serialize_trail(make_dag(trail_definition)) with self.assertRaises(MatchTrailsException): deserialize_trail(self.root_step, mismatching_trail_data, {})
def test_assign_sequence_numbers_to_steps(self): # Make this DAG: # +--> step_b -->+ # step_a -->| |--> step_d # +--> step_c -->+ step_a = Step(lambda x: x) step_b = Step(lambda x: x) step_c = Step(lambda x: x) step_d = Step(lambda x: x) trail_definition = [ (step_a, step_b), (step_a, step_c), (step_b, step_d), (step_c, step_d), ] root_step = make_dag(trail_definition) assign_sequence_numbers_to_steps(root_step) get_number = lambda x: x.tags['n'] self.assertEqual(get_number(step_a), 0) self.assertEqual(get_number(step_d), 3) self.assertGreater(get_number(step_d), get_number(step_b)) self.assertGreater(get_number(step_d), get_number(step_c)) self.assertLess(get_number(step_a), get_number(step_b)) self.assertLess(get_number(step_a), get_number(step_c)) self.assertNotEqual(get_number(step_b), get_number(step_c))
def test_normal_run(self): trail_manager(self.mock_root_step, self.mock_api_socket, self.mock_backup, delay=12, context=self.mock_context, state_transitions=self.mock_state_transitions) self.mock_topological_traverse.assert_called_once_with( self.mock_root_step) self.mock_state_transition.assert_called_once_with( self.mock_step, self.mock_context) root_step, done_check, ignore_check = self.mock_topological_while.call_args[ 0] self.assertEqual(root_step, self.mock_root_step) # This table is used to check the possible results of done_check and ignore_check functions check_table = [ # State, Expected result of done_check, Expected result of ignore_check (Step.SUCCESS, True, False), (Step.SKIPPED, True, False), (Step.READY, False, False), (Step.RUN, False, False), (Step.FAILURE, False, True), (Step.BLOCKED, False, True), (Step.PAUSED, False, False), (Step.INTERRUPTED, False, False), (Step.PAUSED_ON_FAIL, False, False), (Step.WAIT, False, False), (Step.TOSKIP, False, False), (Step.TOBLOCK, False, False), (Step.TOPAUSE, False, False), ] for state, done_check_result, ignore_check_result in check_table: step = Step(lambda: True) step.state = state self.assertEqual(done_check(step), done_check_result) self.assertEqual(ignore_check(step), ignore_check_result) self.assertEqual(len(self.mock_serve_socket.mock_calls), 2) for mock_call in self.mock_serve_socket.mock_calls: socket, handler = mock_call[1] self.assertEqual(socket, self.mock_api_socket) mock_request = MagicMock() handler(mock_request) self.mock_handle_api_call.assert_called_with( mock_request, steps=[self.mock_step]) self.assertEqual(list(self.step_iterator), []) # Make sure the iterator is drained. self.mock_backup.assert_called_once_with(self.mock_root_step) self.mock_sleep.assert_called_once_with(12) self.mock_api_socket.shutdown.assert_called_once_with(SHUT_RDWR)
def test_log_step(self): def action_function(trail_env, context): return 'test return value {}'.format(context) step = Step(action_function) step.tags[ 'n'] = 7 # Typically this is set automatically. We're setting this manually for testing purposes. mock_logger = MagicMock() log_step(mock_logger, step, 'mock message') mock_logger.assert_called_once_with( '[Step Name=action_function, n=7] mock message')
def test_step_manager(self): def action_function(trail_env, context): return 'test return value {}'.format(context) step = Step(action_function) step.tags[ 'n'] = 7 # Typically this is set automatically. We're setting this manually for testing purposes. step.result_queue = MagicMock() trail_environment = MagicMock() step_manager(step, (trail_environment, ), dict(context='foo')) expected_result = StepResult(result=Step.SUCCESS, return_value='test return value foo') step.result_queue.put.assert_called_once_with(expected_result)
def test_collect_prompt_messages_from_step(self): step = Step(lambda x: x) step.tags['n'] = 0 step.prompt_queue = MagicMock() step.prompt_queue.get_nowait = MagicMock( side_effect=['foo', QueueEmpty()]) collect_prompt_messages_from_step(step) self.assertEqual(step.prompt_queue.get_nowait.mock_calls, [mock.call(), mock.call()]) expected_messages = ['foo'] self.assertEqual(step.prompt_messages, expected_messages)
def test_step_manager_with_exception(self): def action_function(trail_env, context): raise Exception('test exception') return 'test return value' step = Step(action_function) step.tags[ 'n'] = 7 # Typically this is set automatically. We're setting this manually for testing purposes. step.result_queue = MagicMock() trail_environment = MagicMock() step_manager(step, trail_environment, context='foo') expected_result = StepResult(result=Step.PAUSED_ON_FAIL, return_value='test exception') step.result_queue.put.assert_called_once_with(expected_result)
def test_instance_with_callable_object(self): class MockAction(object): def __call__(self): pass def __str__(self): return 'MockAction' mock_action_object = MockAction() step = Step(mock_action_object) self.assertIsInstance(step, Step) self.assertEqual(step.action_function, mock_action_object) self.assertEqual(step.tags, {'name': 'MockAction'}) self.assertEqual(step.state, step.READY) self.assertEqual(step.process, None) self.assertEqual(step.return_value, None) self.assertEqual(step.result_queue, None) self.assertEqual(step.input_queue, None) self.assertEqual(step.output_queue, None) self.assertEqual(step.prompt_queue, None) self.assertEqual(step.prompt_messages, []) self.assertEqual(step.output_messages, []) self.assertEqual(step.input_messages, []) self.assertTrue(step.pause_on_fail) self.assertEqual(str(step), 'MockAction')
def setUp(self): self.log_step_patcher = patch('autotrail.layer1.trail.log_step') self.mock_log_step = self.log_step_patcher.start() self.skip_progeny_patcher = patch( 'autotrail.layer1.trail.skip_progeny') self.mock_skip_progeny = self.skip_progeny_patcher.start() self.collect_output_messages_from_step_patcher = patch( 'autotrail.layer1.trail.collect_output_messages_from_step') self.mock_collect_output_messages_from_step = self.collect_output_messages_from_step_patcher.start( ) self.collect_prompt_messages_from_step_patcher = patch( 'autotrail.layer1.trail.collect_prompt_messages_from_step') self.mock_collect_prompt_messages_from_step = self.collect_prompt_messages_from_step_patcher.start( ) self.mock_result = MagicMock() self.mock_result.result = Step.SUCCESS self.mock_result.return_value = 'mock_return_value' self.mock_return_value = MagicMock() self.mock_failed_return_value = MagicMock() self.mock_post_processor = MagicMock( return_value=self.mock_return_value) self.mock_failure_handler = MagicMock( return_value=self.mock_failed_return_value) self.mock_step = Step(lambda x: x, post_processor=self.mock_post_processor, failure_handler=self.mock_failure_handler) self.mock_step.skip_progeny_on_failure = False self.mock_step.result_queue = MagicMock() self.mock_step.result_queue.get_nowait = MagicMock( return_value=self.mock_result)
def test_step_to_stepstatus_empty_fields(self): step = Step(lambda x: x, n=0) step_status = step_to_stepstatus(step, []) self.assertEqual(step_status, { StatusField.N: 0, StatusField.NAME: str(step) })
def test_make_dag_cyclic(self): # Make this cyclic DAG: # step_a --> step_b -->+ # ^ | # | v # +<----- step_c <--+ step_a = Step(lambda x: x) step_b = Step(lambda x: x) step_c = Step(lambda x: x) trail_definition = [ (step_a, step_b), (step_b, step_c), (step_c, step_a), ] with self.assertRaises(CyclicException): root_step = make_dag(trail_definition)
def test_write_trail_definition_as_dot(self): # Make this DAG: # step_a --> step_b def a(): pass def b(): pass step_a = Step(a) step_b = Step(b) trail_definition = [(step_a, step_b)] dot_file = StringIO() write_trail_definition_as_dot(trail_definition, to=dot_file) self.assertEqual(dot_file.getvalue(), 'digraph trail {\nnode [style=rounded, shape=box];\n"a" -> "b";\n}\n')
def test_run_step_when_pre_processor_fails(self): def action_function_a(): return 'test return value' mock_exception = MockException() mock_pre_processor = MagicMock(side_effect=mock_exception) step = Step(action_function_a, pre_processor=mock_pre_processor) step.context = 'mock_context' step.tags['n'] = 0 process_patcher = patch('autotrail.layer1.trail.Process') mock_process = process_patcher.start() run_step(step) process_patcher.stop() self.assertEqual(step.process, None) self.assertEqual(mock_process.mock_calls, []) self.assertEqual(step.state, step.FAILURE) self.assertEqual(step.return_value, mock_exception)
def setUp(self): # Create a simple trail (root_step) # +--> step_b (group=1) # step_a -->|--> step_c # +--> step_d (group=1) def action_function_a(): pass def action_function_b(): pass def action_function_c(): pass def action_function_d(): pass self.step_a = Step(action_function_a) self.step_b = Step(action_function_b, group=1) self.step_c = Step(action_function_c) self.step_d = Step(action_function_d, group=1)
def test_serialize_trail(self): # Make this DAG: # +--> step_b -->+ # step_a -->| |--> step_d # +--> step_c -->+ def action_a(): pass def action_b(): pass def action_c(): pass def action_d(): pass step_a = Step(action_a) step_b = Step(action_b) step_c = Step(action_c) step_d = Step(action_d) trail_definition = [ (step_a, step_b), (step_a, step_c), (step_b, step_d), (step_c, step_d), ] root_step = make_dag(trail_definition) trail_data = serialize_trail(root_step) for step in [step_a, step_b, step_c, step_d]: self.assertIn(str(step), trail_data) self.assertEqual(trail_data[str(step)][StatusField.STATE], str(step.state)) self.assertEqual(trail_data[str(step)][StatusField.RETURN_VALUE], str(step.return_value)) for parent in step.parents: self.assertIn(str(parent), trail_data[str(step)]['parents'])
def test_run_step_with_rerun_of_a_step(self): def action_function_a(): return 'test return value' step = Step(action_function_a) step.tags['n'] = 0 # Setup some old values for the step. Similar to the effect of running the step might have. step.return_value = 'Some old return value.' step.prompt_messages = ['old message 1', 'old message 2'] step.input_messages = ['response to old message 1'] process_patcher = patch('autotrail.layer1.trail.Process') mock_process = process_patcher.start() run_step(step, 'mock_context') process_patcher.stop() args, kwargs = mock_process.call_args arg_step, arg_trail_env, arg_context = kwargs['args'] self.assertEqual(step, arg_step) self.assertIsInstance(arg_trail_env, TrailEnvironment) self.assertEqual(arg_context, 'mock_context') self.assertEqual(kwargs['target'], step_manager) self.assertIn(call().start(), mock_process.mock_calls) self.assertEqual(step.state, step.RUN) # Because the step is being re-run, the old values of the following attributes should get reset. self.assertEqual(step.return_value, None) self.assertEqual(step.prompt_messages, []) self.assertEqual(step.input_messages, [])
def test_step_to_stepstatus_with_non_json_serializable_return_value(self): step = Step(lambda x: x, n=0) mock_exception = TypeError('is not JSON serializable') step.return_value = mock_exception step_status = step_to_stepstatus(step, [ StatusField.STATE, StatusField.RETURN_VALUE, StatusField.TAGS, StatusField.OUTPUT_MESSAGES, StatusField.PROMPT_MESSAGES, StatusField.UNREPLIED_PROMPT_MESSAGE ]) self.assertEqual( step_status, { StatusField.N: 0, StatusField.NAME: str(step), StatusField.TAGS: step.tags, StatusField.STATE: Step.READY, StatusField.RETURN_VALUE: str(mock_exception), StatusField.OUTPUT_MESSAGES: [], StatusField.PROMPT_MESSAGES: [], StatusField.UNREPLIED_PROMPT_MESSAGE: None })
def test_make_dag(self): # Make this DAG: # +--> step_b -->+ # step_a -->| |--> step_d # +--> step_c -->+ step_a = Step(lambda x: x) step_b = Step(lambda x: x) step_c = Step(lambda x: x) step_d = Step(lambda x: x) trail_definition = [ (step_a, step_b), (step_a, step_c), (step_b, step_d), (step_c, step_d), ] root_step = make_dag(trail_definition) self.assertEqual(root_step, step_a) self.assertEqual(step_a.tags['n'], 0) self.assertEqual(step_d.tags['n'], 3)
def test_run_step(self): def action_function_a(): return 'test return value' step = Step(action_function_a) step.tags['n'] = 0 process_patcher = patch('autotrail.layer1.trail.Process') mock_process = process_patcher.start() run_step(step, 'mock_context') process_patcher.stop() args, kwargs = mock_process.call_args arg_step, arg_trail_env, arg_context = kwargs['args'] self.assertEqual(step, arg_step) self.assertIsInstance(arg_trail_env, TrailEnvironment) self.assertEqual(arg_context, 'mock_context') self.assertEqual(kwargs['target'], step_manager) self.assertIn(call().start(), mock_process.mock_calls) self.assertEqual(step.state, step.RUN)
def setUp(self): # Make this DAG: # +--> step_b -->+ # step_a -->| |--> step_d # +--> step_c -->+ def action_a(): pass def action_b(): pass def action_c(): pass def action_d(): pass self.step_a = Step(action_a) self.step_b = Step(action_b) self.step_c = Step(action_c) self.step_d = Step(action_d) trail_definition = [ (self.step_a, self.step_b), (self.step_a, self.step_c), (self.step_b, self.step_d), (self.step_c, self.step_d), ] self.root_step = make_dag(trail_definition) self.step_a.return_value = 'mock return value' self.step_a.prompt_messages = ['mock prompt_messages'] self.step_a.output_messages = ['mock output_messages'] self.step_a.input_messages = ['mock input_messages'] self.trail_data = serialize_trail(self.root_step)
def test_create_namespaces_for_tag_keys_and_values(self): def action_a(): pass step_a = Step(action_a, foo='bar', n=5) keys, values = create_namespaces_for_tag_keys_and_values(step_a) self.assertEqual(keys.foo, 'foo') self.assertEqual(keys.name, 'name') with self.assertRaises(AttributeError): keys.n self.assertEqual(values.bar, 'bar') self.assertEqual(values.action_a, 'action_a')
def test_search_steps_with_states(self): step_running = Step(lambda x: x) step_running.state = Step.RUN step_paused = Step(lambda x: x) step_paused.state = Step.PAUSED steps = [step_running, step_paused] filtered_steps = search_steps_with_states(steps, [Step.PAUSED]) self.assertEqual(list(filtered_steps), [step_paused])
def test_step_to_stepstatus_all_fields(self): step = Step(lambda x: x, n=0) step_status = step_to_stepstatus(step, [ StatusField.STATE, StatusField.RETURN_VALUE, StatusField.OUTPUT_MESSAGES, StatusField.PROMPT_MESSAGES, StatusField.UNREPLIED_PROMPT_MESSAGE ]) self.assertEqual( step_status, { StatusField.N: 0, StatusField.NAME: str(step), StatusField.STATE: Step.READY, StatusField.RETURN_VALUE: None, StatusField.OUTPUT_MESSAGES: [], StatusField.PROMPT_MESSAGES: [], StatusField.UNREPLIED_PROMPT_MESSAGE: None })
def test_instance_with_name(self): def mock_function(): pass step = Step(mock_function, name='custom_name') self.assertIsInstance(step, Step) self.assertEqual(step.action_function, mock_function) self.assertEqual(step.tags, {'name': 'custom_name'}) self.assertEqual(step.state, step.READY) self.assertEqual(step.process, None) self.assertEqual(step.return_value, None) self.assertEqual(step.result_queue, None) self.assertEqual(step.input_queue, None) self.assertEqual(step.output_queue, None) self.assertEqual(step.prompt_queue, None) self.assertEqual(step.prompt_messages, []) self.assertEqual(step.output_messages, []) self.assertEqual(step.input_messages, []) self.assertTrue(step.pause_on_fail) self.assertEqual(str(step), 'custom_name')
def test_extract_essential_tags(self): step_1 = Step(lambda x: x, n=0) step_1.tags['name'] = 'step_1' step_2 = Step(lambda x: x, n=1) step_2.tags['name'] = 'step_2' essential_tags = list(extract_essential_tags([step_1, step_2])) self.assertEqual(essential_tags, [{ 'n': 0, 'name': 'step_1' }, { 'n': 1, 'name': 'step_2' }])