def test_sh_captures_output_in_correct_order_with_various_timing(self): """Test if output is containing stdout and stderr lines mixed in proper order (as it is defined in shell script) Notice: Test is interacting with shell, to reduce possibility of weird behavior it is retried multiple times """ for i in range(1, 100): self.maxDiff = None # unittest setting task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): task.sh(''' set +e; sleep 0.05; echo "FIRST"; sleep 0.05; echo "SECOND" >&2; echo "THIRD"; echo "FOURTH" >&2; echo "FIFTH" >&2; echo "SIXTH"; echo "SEVENTH" >&2; echo "NINETH"; echo "TENTH"; ''') self.assertEqual( "FIRST\r\nSECOND\r\nTHIRD\r\nFOURTH\r\nFIFTH\r\nSIXTH\r\nSEVENTH\r\nNINETH\r\nTENTH\r\n", out.getvalue())
def test_sh_producing_large_outputs(self): """Process a few megabytes of output and assert that: - It will consume not more than 10 megabytes (assuming also output capturing in tests by io.capture_descriptors()) - The whole output would be printed correctly """ self.maxDiff = None # unittest setting task = InitTask() task._io = IO() io = IO() out = StringIO() text = "History isn't made by kings and politicians, it is made by us." memory_before = psutil.Process( os.getpid()).memory_info().rss / 1024 / 1024 with io.capture_descriptors(stream=out, enable_standard_out=False): task.py(''' for i in range(0, 1024 * 128): print("''' + text + '''") ''') iterations = 1024 * 128 text_with_newlines_length = len(text) + 2 # \r + \n memory_after = psutil.Process( os.getpid()).memory_info().rss / 1024 / 1024 self.assertEqual(iterations * text_with_newlines_length, len(out.getvalue())) self.assertLessEqual( memory_after - memory_before, 16, msg='Expected less than 16 megabytes of memory usage')
def test_full_command_is_shown_only_in_debug_output_level(self): """Test that sh() will show full bash script only in case, when '-rl debug' is used :return: """ task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): # CASE 1 with self.subTest('NORMAL output level'): try: task.sh('python3 -m rkd :sh -c "exit 5"') except subprocess.CalledProcessError as e: self.assertIn( "Command 'exit 5' returned non-zero exit status 5.", e.output) # CASE 2 with self.subTest('DEBUG output level'): try: task.sh('python3 -m rkd -rl debug :sh -c "exit 5"') except subprocess.CalledProcessError as e: self.assertIn( "Command '#!/bin/bash -eopipefail \r\nset -euo pipefail;" + " exit 5' returned non-zero exit status 5.", e.output)
def test_sh_captures_output_in_correct_order_with_fixed_timing(self): """Test if output contains stdout and stderr lines printed out in proper order, while there is a sleep between prints Notice: Test is interacting with shell, to reduce possibility of weird behavior it is retried multiple times """ for i in range(1, 30): self.maxDiff = None # unittest setting task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): task.sh(''' set +e; sleep 0.05; echo "FIRST"; sleep 0.05; echo "SECOND" >&2; sleep 0.05; echo "THIRD"; ''') self.assertEqual("FIRST\r\nSECOND\r\nTHIRD\r\n", out.getvalue())
def test_sh_rkd_in_rkd_shows_first_lines_on_error(self): """Bugfix: sh() was loosing first line(s) of output, when exception was raised Notice: Test is interacting with shell, to reduce possibility of weird behavior it is retried multiple times """ for i in range(1, 5): for std_redirect in ['', '>&2']: task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): try: task.sh(''' python3 -m rkd --silent :sh -c 'echo "Bartolomeo Vanzetti" ''' + std_redirect + '''; exit 127' ''') except subprocess.CalledProcessError: pass self.assertIn( 'Bartolomeo Vanzetti', out.getvalue(), msg='Expected that output will be shown for std_redirect=%s' % std_redirect)
def test_one_failed_step_is_preventing_next_steps_from_execution_and_result_is_marked_as_failure(self): """Check the correctness of error handling""" io = IO() str_io = StringIO() buffered = BufferedSystemIO() task_declaration = get_test_declaration() BasicTestingCase.satisfy_task_dependencies(task_declaration.get_task_to_execute(), io=buffered) ctx = ExecutionContext(task_declaration) executor = DeclarativeExecutor() executor.add_step('python', 'this.io().outln("Peter Kropotkin"); return True', task_name=':first', rkd_path='', envs={}) executor.add_step('bash', 'echo "Buenaventura Durruti"; exit 1', task_name=':second', rkd_path='', envs={}) executor.add_step('python', 'this.io().outln("This one will not show"); return True', task_name=':third', rkd_path='', envs={}) with io.capture_descriptors(target_files=[], stream=str_io, enable_standard_out=False): final_result = executor.execute_steps_one_by_one(ctx, task_declaration.get_task_to_execute()) output = str_io.getvalue() + buffered.get_value() self.assertIn('Peter Kropotkin', output) self.assertIn('Buenaventura Durruti', output) self.assertNotIn('This one will not show', output) self.assertEqual(False, final_result)
def run_and_capture_output(self, argv: list, verbose: bool = False) -> Tuple[str, int]: """ Run task(s) and capture output + exit code. Whole RKD from scratch will be bootstrapped there. Example usage: full_output, exit_code = self.run_and_capture_output([':tasks']) :param list argv: List of tasks, arguments, commandline switches :param bool verbose: Print all output also to stdout :return: """ io = IO() out = StringIO() exit_code = 0 try: with io.capture_descriptors(stream=out, enable_standard_out=verbose): app = RiotKitDoApplication() app.main(['test_functional.py'] + argv) except SystemExit as e: self._restore_standard_out() exit_code = e.code return out.getvalue(), exit_code
def test_functional_hooks_are_executed_when_exists_and_files_with_extension_only_are_skipped( self): """Given we have an example hooks in pre-upgrade/whoami.sh and in post-upgrade/history.sh And we try to run those hooks using hooks_executed() Then we will see output produced by those scripts And .dotfiles will be ignored """ self._prepare_test_data() buffer = StringIO() hooks_capturing_io = IO() task = TestTask() task._io = BufferedSystemIO() ctx = ExecutionContext(TaskDeclaration(task), args={}, env={}) with hooks_capturing_io.capture_descriptors(stream=buffer, enable_standard_out=True): with task.hooks_executed(ctx, 'upgrade'): pass self.assertIn('>> This is a whoami.sh hook, test:', buffer.getvalue(), msg='Expected pre-upgrade hook to be ran') self.assertIn('25 June 1978 the rainbow flag was first flown', buffer.getvalue(), msg='Expected post-upgrade hook to be ran') self.assertIn('pre-upgrade/whoami.sh', task._io.get_value()) self.assertNotIn('.gitkeep', task._io.get_value())
def test_bash_case_verify_env_variables_are_present(self): io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): self._create_callable_tester('echo "Boolean: ${ARG_TEST}, Text: ${ARG_MESSAGE}"', language='bash') self.assertIn('ARG_TEST: unbound variable', out.getvalue())
def test_quotes_are_escaped_in_shell_commands(self): task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): task.sh('echo ${NAME}', env={'NAME': 'Ferdinando "Nicola" Sacco'}) self.assertIn('Ferdinando "Nicola" Sacco', out.getvalue())
def execute_mocked_task_and_get_output(self, task: TaskInterface, args=None, env=None) -> str: """ Run a single task, capturing it's output in a simplified way. There is no whole RKD bootstrapped in this operation. :param TaskInterface task: :param dict args: :param dict env: :return: """ if args is None: args = {} if env is None: env = {} ctx = ApplicationContext([], [], '') ctx.io = BufferedSystemIO() task.internal_inject_dependencies( io=ctx.io, ctx=ctx, executor=OneByOneTaskExecutor(ctx=ctx), temp_manager=TempManager()) merged_env = deepcopy(os.environ) merged_env.update(env) r_io = IO() str_io = StringIO() defined_args = {} for arg, arg_value in args.items(): defined_args[arg] = {'default': ''} with r_io.capture_descriptors(enable_standard_out=True, stream=str_io): try: # noinspection PyTypeChecker result = task.execute( ExecutionContext(TaskDeclaration(task), args=args, env=merged_env, defined_args=defined_args)) except Exception: self._restore_standard_out() print(ctx.io.get_value() + "\n" + str_io.getvalue()) raise return ctx.io.get_value() + "\n" + str_io.getvalue( ) + "\nTASK_EXIT_RESULT=" + str(result)
def test_bash_successful_case(self): """ Bash callable test: Successful case """ io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): self._create_callable_tester('python --version', language='bash') self.assertIn("Python", out.getvalue()) self.assertTrue(out.getvalue(), msg='python --version should result with a True')
def test_ps(self): """Simply check if docker-compose ps can be executed, if the standard switches are correct""" io_str = StringIO() io = IO() with io.capture_descriptors(stream=io_str, enable_standard_out=False): drv = self._get_prepared_compose_driver() drv.ps([]) self.assertIn('Name', io_str.getvalue()) self.assertIn('Ports', io_str.getvalue())
def test_io_capturing_is_restoring_both_stdout_and_stderr_to_previous_state( self): """Assert that capture_descriptors() restores sys.stdout and sys.stderr to original state after mocking them for output capturing""" io = IO() stdout_backup = sys.stdout stderr_backup = sys.stderr with io.capture_descriptors(target_files=None): pass self.assertEqual(stdout_backup, sys.stdout) self.assertEqual(stderr_backup, sys.stderr)
def test_environment_is_passed_and_system_environment_still_available( self) -> None: os.environ['COMING_FROM_PARENT_CONTEXT'] = 'Buenaventura Durruti' io = IO() out = StringIO() try: with io.capture_descriptors(stream=out, enable_standard_out=False): check_call('env', env={'PROTEST_TYPE': 'Sabotage'}) finally: del os.environ['COMING_FROM_PARENT_CONTEXT'] self.assertIn('PROTEST_TYPE=Sabotage', out.getvalue()) self.assertIn('COMING_FROM_PARENT_CONTEXT=Buenaventura Durruti', out.getvalue())
def test_dollar_symbols_are_escaped_in_shell_commands(self): """Check that in envrionment variable there can be defined a value that contains dollar symbols""" task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): task.sh('env | grep TEST_ENV', env={ 'TEST_ENV': "Mikhail\\$1Bakunin\\$PATHtest", 'TEST_ENV_NOT_ESCAPED': 'Hello $TEST_ENV' }) self.assertIn('TEST_ENV=Mikhail$1Bakunin$PATHtest', out.getvalue()) self.assertIn('TEST_ENV_NOT_ESCAPED=Hello Mikhail$1Bakunin$PATHtest', out.getvalue())
def test_non_interactive_session_returns_output(self): """Checks functionally if process.py is implementing a fall-back for non-interactive sessions 'true |' part enforces the console to be non-interactive, which should cause "termios.error: (25, 'Inappropriate ioctl for device')" that should be handled and interactive mode should be turned off for stdin """ task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): task.sh( ''' true | python3 -m rkd --silent :sh -c 'echo "Strajk Kobiet! Jebac PiS!"' ''' ) self.assertIn('Strajk Kobiet! Jebac PiS!', out.getvalue())
def test_return_false_is_added_to_the_code(self): """Assert that code in Python without a "return" will have "return false" by default Previously, when return was not added automatically there was an error raised (not always - depending on the code), example: AttributeError: 'Try' object has no attribute 'value' https://github.com/riotkit-org/riotkit-do/issues/37 :return: """ io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): returned_value = self._create_callable_tester( '''print('History isn't made by kings and politicians, it is made by us.');''', language='python') self.assertIn("History isn't made by kings and politicians, it is made by us.", out.getvalue()) self.assertFalse(returned_value)
def test_executing_multiple_steps_one_by_one_the_order_is_preserved(self): """Assert that execution order is preserved - as we register steps""" io = IO() str_io = StringIO() task_declaration = get_test_declaration() BasicTestingCase.satisfy_task_dependencies(task_declaration.get_task_to_execute(), io=io) ctx = ExecutionContext(task_declaration) executor = DeclarativeExecutor() executor.add_step('python', 'this.io().outln("First"); return True', task_name=':first', rkd_path='', envs={}) executor.add_step('bash', 'echo "Second"; exit 0', task_name=':second', rkd_path='', envs={}) executor.add_step('python', 'this.io().outln("Third"); return True', task_name=':third', rkd_path='', envs={}) with io.capture_descriptors(target_files=[], stream=str_io, enable_standard_out=False): executor.execute_steps_one_by_one(ctx, task_declaration.get_task_to_execute()) self.assertEqual("First\nSecond\r\nThird\n", str_io.getvalue())
def test_functional_execute_hooks_executes_post_upgrade_hooks(self): """Assert that post-upgrade/history.sh is executed""" self._prepare_test_data() buffer = StringIO() hooks_capturing_io = IO() task = TestTask() task._io = BufferedSystemIO() ctx = ExecutionContext(TaskDeclaration(task), args={}, env={}) with hooks_capturing_io.capture_descriptors(stream=buffer, enable_standard_out=True): task.execute_hooks(ctx, 'post-upgrade') self.assertIn('25 June 1978 the rainbow flag was first flown', buffer.getvalue(), msg='Expected post-upgrade hook to be ran')
def test_parse_tasks_successful_case(self): """Successful case with description, arguments and bash steps """ input_tasks = { ':resistentia': { 'description': 'Against moving the costs of the crisis to the workers!', 'arguments': { '--picket': { 'help': 'Picket form', 'required': False, 'action': 'store_true' } }, 'steps': ['echo "Resistentia!"'] } } io = IO() out = StringIO() factory = YamlSyntaxInterpreter(io, YamlFileLoader([])) parsed_tasks = factory.parse_tasks(input_tasks, '', './makefile.yaml', {}) self.assertEqual(':resistentia', parsed_tasks[0].to_full_name(), msg='Expected that the task name will be present') declaration = parsed_tasks[0] declaration.get_task_to_execute()._io = NullSystemIO() with io.capture_descriptors(stream=out, enable_standard_out=False): declaration.get_task_to_execute().execute( ExecutionContext(declaration)) self.assertIn('Resistentia!', out.getvalue(), msg='Expected that echo contents will be visible')
def test_sh_3rd_depth_rkd_calls(self): """Bugfix: sh() of 3-depth calls -> test -> rkd -> rkd returns first line of output """ for std_redirect in ['', '>&2']: task = InitTask() task._io = IO() io = IO() out = StringIO() with io.capture_descriptors(stream=out, enable_standard_out=False): try: task.sh(''' python3 -m rkd --silent :sh -c 'python -m rkd --silent :sh -c \'echo "Nicola Sacco" ''' + std_redirect + '''; exit 1\'' ''') except subprocess.CalledProcessError: pass self.assertIn( 'Nicola Sacco', out.getvalue(), msg='Expected that output will be shown for std_redirect=%s' % std_redirect)