def test_exception_raised_while_waiting_causes_termination_and_adds_error_message_to_output(
            self):
        exception_message = 'Something terribly horrible just happened!'
        value_err_exc = ValueError(exception_message)
        timeout_exc = TimeoutExpired(cmd=None, timeout=1)
        fake_failing_return_code = -15
        # Simulate Popen.wait() timing out twice before raising a ValueError exception.
        self.mock_popen.wait.side_effect = [
            timeout_exc, timeout_exc, value_err_exc, fake_failing_return_code
        ]
        self.mock_popen.returncode = fake_failing_return_code
        self.mock_popen.pid = 55555
        self._mock_stdout_and_stderr(b'', b'')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project(
            'echo The power is yours!')

        self.assertEqual(
            self.mock_killpg.call_count, 1,
            'os.killpg should be called when wait() raises exception.')
        self.assertIn(
            exception_message, actual_output,
            'ClusterRunner exception message should be included in output.')
        self.assertEqual(actual_return_code, fake_failing_return_code,
                         'Actual return code should match expected.')
    def test_calling_kill_subprocesses_will_break_out_of_command_execution_wait_loop(
            self):
        self._mock_stdout_and_stderr(b'fake_output', b'fake_error')
        self.mock_popen.pid = 55555
        self._simulate_hanging_popen_process()

        project_type = ProjectType()
        command_thread = SafeThread(
            target=project_type.execute_command_in_project,
            args=('echo The power is yours!', ))

        # This calls execute_command_in_project() on one thread, and calls kill_subprocesses() on another. The
        # kill_subprocesses() call should cause the first thread to exit.
        command_thread.start()
        project_type.kill_subprocesses()

        # This *should* join immediately, but we specify a timeout just in case something goes wrong so that the test
        # doesn't hang. A successful join implies success. We also use the UnhandledExceptionHandler so that exceptions
        # propagate from the child thread to the test thread and fail the test.
        with UnhandledExceptionHandler.singleton():
            command_thread.join(timeout=10)
            if command_thread.is_alive():
                self.mock_killpg(
                )  # Calling killpg() causes the command thread to end.
                self.fail(
                    'project_type.kill_subprocesses should cause the command execution wait loop to exit.'
                )

        self.mock_killpg.assert_called_once_with(
            55555, ANY)  # Note: os.killpg does not accept keyword args.
    def test_timing_out_will_break_out_of_command_execution_wait_loop_and_kill_subprocesses(
            self):
        mock_time = self.patch('time.time')
        mock_time.side_effect = [
            0.0, 100.0, 200.0, 300.0
        ]  # time increases by 100 seconds with each loop
        expected_return_code = 1
        self._simulate_hanging_popen_process(
            fake_returncode=expected_return_code)
        self.mock_popen.pid = 55555
        self._mock_stdout_and_stderr(b'fake output', b'fake error')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project(
            command='sleep 99', timeout=250)

        self.assertEqual(
            self.mock_killpg.call_count, 1,
            'os.killpg should be called when execution times out.')
        self.assertEqual(actual_output, 'fake output\nfake error',
                         'Output should contain stdout and stderr.')
        self.assertEqual(actual_return_code, expected_return_code,
                         'Actual return code should match expected.')
        self.assertTrue(
            all([file.close.called for file in self.mock_temporary_files]),
            'All created TemporaryFiles should be closed so that they are removed from the filesystem.'
        )
    def test_calling_kill_subprocesses_will_break_out_of_command_execution_wait_loop(self):

        def fake_communicate(timeout=None):
            # The fake implementation is that communicate() times out forever until os.killpg is called.
            if mock_killpg.call_count == 0 and timeout is not None:
                raise TimeoutExpired(None, timeout)
            elif mock_killpg.call_count > 0:
                return b'fake output', b'fake error'
            self.fail('Popen.communicate() should not be called without a timeout before os.killpg has been called.')

        mock_killpg = self.patch('os.killpg')
        self.mock_popen.communicate.side_effect = fake_communicate
        self.mock_popen.returncode = 1
        self.mock_popen.pid = 55555
        project_type = ProjectType()
        command_thread = SafeThread(target=project_type.execute_command_in_project, args=('echo The power is yours!',))

        # This calls execute_command_in_project() on one thread, and calls kill_subprocesses() on another. The
        # kill_subprocesses() call should cause the first thread to exit.
        command_thread.start()
        project_type.kill_subprocesses()

        # This *should* join immediately, but we specify a timeout just in case something goes wrong so that the test
        # doesn't hang. A successful join implies success. We also use the UnhandledExceptionHandler so that exceptions
        # propagate from the child thread to the test thread and fail the test.
        with UnhandledExceptionHandler.singleton():
            command_thread.join(timeout=10)
            if command_thread.is_alive():
                mock_killpg()  # Calling killpg() causes the command thread to end.
                self.fail('project_type.kill_subprocesses should cause the command execution wait loop to exit.')

        mock_killpg.assert_called_once_with(pgid=55555, sig=ANY)
    def test_command_exiting_normally_will_break_out_of_command_execution_wait_loop(
            self):
        timeout_exc = TimeoutExpired(cmd=None, timeout=1)
        expected_return_code = 0
        # Simulate Popen.wait() timing out twice before command completes and returns output.
        self.mock_popen.wait.side_effect = [
            timeout_exc, timeout_exc, expected_return_code
        ]
        self.mock_popen.returncode = expected_return_code
        self._mock_stdout_and_stderr(b'fake_output', b'fake_error')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project(
            'echo The power is yours!')

        self.assertEqual(
            self.mock_killpg.call_count, 0,
            'os.killpg should not be called when command exits normally.')
        self.assertEqual(actual_output, 'fake_output\nfake_error',
                         'Output should contain stdout and stderr.')
        self.assertEqual(actual_return_code, expected_return_code,
                         'Actual return code should match expected.')
        self.assertTrue(
            all([file.close.called for file in self.mock_temporary_files]),
            'All created TemporaryFiles should be closed so that they are removed from the filesystem.'
        )
    def test_execute_command_in_project_does_not_choke_on_weird_command_output(self):
        some_weird_output = b'\xbf\xe2\x98\x82'  # the byte \xbf is invalid unicode
        self.mock_popen.communicate.return_value = (some_weird_output, None)
        self.mock_popen.returncode = (some_weird_output, None)

        project_type = ProjectType()
        project_type.execute_command_in_project('fake command')
    def test_teardown_build_runs_teardown(self):
        job_config = JobConfig('name', 'setup', 'teardown', 'command', 'atomizer', 10)
        project_type = ProjectType()
        project_type.job_config = MagicMock(return_value=job_config)
        project_type.execute_command_in_project = MagicMock(return_value=('', 0))

        project_type.teardown_build()

        project_type.execute_command_in_project.assert_called_with('teardown')
    def test_teardown_build_runs_teardown(self):
        job_config = JobConfig("name", "setup", "teardown", "command", "atomizer", 10, 10)
        project_type = ProjectType()
        project_type.job_config = MagicMock(return_value=job_config)
        project_type.execute_command_in_project = MagicMock(return_value=("", 0))

        project_type.teardown_build()

        project_type.execute_command_in_project.assert_called_with("teardown", timeout=None)
    def test_failing_exit_code_is_injected_when_no_return_code_available(self):
        self.mock_popen.returncode = None  # This will happen if we were not able to terminate the process.
        self._mock_stdout_and_stderr(b'', b'')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project('echo The power is yours!')

        self.assertIsInstance(actual_return_code, int, 'Returned exit code should always be an int.')
        self.assertNotEqual(actual_return_code, 0, 'Returned exit code should be failing (non-zero) when subprocess '
                                                   'returncode is not available.')
    def test_execute_command_in_project_does_not_choke_on_weird_command_output(self):
        some_weird_output = b'\xbf\xe2\x98\x82'  # the byte \xbf is invalid unicode
        self._mock_stdout_and_stderr(some_weird_output, b'')
        self.mock_popen.returncode = 0

        project_type = ProjectType()
        actual_output, _ = project_type.execute_command_in_project('fake command')

        # Part of this test is just proving that no exception is raised on invalid output.
        self.assertIsInstance(actual_output, str, 'Invalid output from a process should still be converted to string.')
    def test_execute_command_in_project_does_not_choke_on_weird_command_output(self):
        some_weird_output = b"\xbf\xe2\x98\x82"  # the byte \xbf is invalid unicode
        self._mock_stdout_and_stderr(some_weird_output, b"")
        self.mock_popen.returncode = 0

        project_type = ProjectType()
        actual_output, _ = project_type.execute_command_in_project("fake command")

        # Part of this test is just proving that no exception is raised on invalid output.
        self.assertIsInstance(actual_output, str, "Invalid output from a process should still be converted to string.")
    def test_failing_exit_code_is_injected_when_no_return_code_available(self):
        self.mock_popen.returncode = None  # This will happen if we were not able to terminate the process.
        self._mock_stdout_and_stderr(b'', b'')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project('echo The power is yours!')

        self.assertIsInstance(actual_return_code, int, 'Returned exit code should always be an int.')
        self.assertNotEqual(actual_return_code, 0, 'Returned exit code should be failing (non-zero) when subprocess '
                                                   'returncode is not available.')
    def test_command_exiting_normally_will_break_out_of_command_execution_wait_loop(self):
        # Simulate Popen.communicate() timing out twice before command completes and returns output.
        timeout_exc = TimeoutExpired(None, 1)
        self.mock_popen.communicate.side_effect = [timeout_exc, timeout_exc, (b'fake_output', b'fake_error')]
        self.mock_popen.returncode = 0
        self.mock_popen.pid = 55555

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project('echo The power is yours!')

        self.assertEqual(self.mock_killpg.call_count, 0, 'os.killpg should not be called when command exits normally.')
        self.assertEqual(actual_output, 'fake_output\nfake_error', 'Output should contain stdout and stderr.')
    def test_timing_out_will_break_out_of_command_execution_wait_loop_and_kill_subprocesses(self):
        mock_time = self.patch('time.time')
        mock_time.side_effect = [0.0, 100.0, 200.0, 300.0]  # time increases by 100 seconds with each loop
        self._mock_out_popen_communicate()
        project_type = ProjectType()

        actual_output, actual_return_code = project_type.execute_command_in_project(
            command='sleep 99',
            timeout=250,
        )

        self.assertEqual(self.mock_killpg.call_count, 1, 'os.killpg should be called when execution times out.')
        self.assertEqual(actual_output, 'fake output\nfake error', 'Output should contain stdout and stderr.')
    def test_job_config_uses_passed_in_config_instead_of_clusterrunner_yaml(self):
        config_dict = {
            'commands': ['shell command 1', 'shell command 2;'],
            'atomizers': [{'TESTPATH': 'atomizer command'}],
            'max_executors': 100,
            'max_executors_per_slave': 2,
        }
        project_type = ProjectType(config=config_dict, job_name='some_job_name')

        job_config = project_type.job_config()

        self.assertEquals(job_config.name, 'some_job_name')
        self.assertEquals(job_config.command, 'shell command 1 && shell command 2')
        self.assertEquals(job_config.max_executors, 100)
        self.assertEquals(job_config.max_executors_per_slave, 2)
    def test_exception_raised_while_waiting_for_termination_adds_error_message_to_output(self):
        mock_time = self.patch('time.time')
        mock_time.side_effect = [0.0, 100.0, 200.0, 300.0]  # time increases by 100 seconds with each loop
        fake_failing_return_code = -15
        self.mock_popen.pid = 55555
        self._mock_stdout_and_stderr(b'', b'')
        exception_message = 'Something terribly horrible just happened!'
        self._simulate_hanging_popen_process(
            fake_returncode=fake_failing_return_code, wait_exception=ValueError(exception_message))

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project(
            'echo The power is yours!', timeout=250)

        self.assertIn(exception_message, actual_output, 'ClusterRunner exception message should be included in output.')
        self.assertEqual(actual_return_code, fake_failing_return_code, 'Actual return code should match expected.')
    def test_command_exiting_normally_will_break_out_of_command_execution_wait_loop(self):
        timeout_exc = TimeoutExpired(cmd=None, timeout=1)
        expected_return_code = 0
        # Simulate Popen.wait() timing out twice before command completes and returns output.
        self.mock_popen.wait.side_effect = [timeout_exc, timeout_exc, expected_return_code]
        self.mock_popen.returncode = expected_return_code
        self._mock_stdout_and_stderr(b'fake_output', b'fake_error')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project('echo The power is yours!')

        self.assertEqual(self.mock_killpg.call_count, 0, 'os.killpg should not be called when command exits normally.')
        self.assertEqual(actual_output, 'fake_output\nfake_error', 'Output should contain stdout and stderr.')
        self.assertEqual(actual_return_code, expected_return_code, 'Actual return code should match expected.')
        self.assertTrue(all([file.close.called for file in self.mock_temporary_files]),
                        'All created TemporaryFiles should be closed so that they are removed from the filesystem.')
    def test_compute_subjobs_for_build_only_atomizes_if_override_not_specified(
            self, atoms_override, atomizer_output, atomizer_called):
        """
        :type atoms_override: list[str] | None
        :type atomizer_output: list[Atom] | None
        :type atomizer_called: bool
        """
        self.patch('os.path.isfile').return_value = False
        mock_project = Mock(spec_set=ProjectType())
        mock_project.atoms_override = atoms_override
        mock_project.timing_file_path.return_value = '/some/path/doesnt/matter'
        mock_project.project_directory = '/some/project/directory'
        mock_atomizer = Mock(spec_set=Atomizer)
        mock_atomizer.atomize_in_project.return_value = atomizer_output
        mock_job_config = Mock(spec=JobConfig)
        mock_job_config.name = 'some_config'
        mock_job_config.max_executors = 1
        mock_job_config.atomizer = mock_atomizer

        subjob_calculator = SubjobCalculator()
        subjob_calculator.compute_subjobs_for_build(build_id=1,
                                                    job_config=mock_job_config,
                                                    project_type=mock_project)

        self.assertEquals(mock_atomizer.atomize_in_project.called,
                          atomizer_called)
    def test_exception_raised_while_waiting_for_termination_adds_error_message_to_output(self):
        mock_time = self.patch('time.time')
        mock_time.side_effect = [0.0, 100.0, 200.0, 300.0]  # time increases by 100 seconds with each loop
        fake_failing_return_code = -15
        self.mock_popen.pid = 55555
        self._mock_stdout_and_stderr(b'', b'')
        exception_message = 'Something terribly horrible just happened!'
        self._simulate_hanging_popen_process(
            fake_returncode=fake_failing_return_code, wait_exception=ValueError(exception_message))

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project(
            'echo The power is yours!', timeout=250)

        self.assertIn(exception_message, actual_output, 'ClusterRunner exception message should be included in output.')
        self.assertEqual(actual_return_code, fake_failing_return_code, 'Actual return code should match expected.')
    def test_exception_raised_while_waiting_causes_termination_and_adds_error_message_to_output(self):
        exception_message = "Something terribly horrible just happened!"
        value_err_exc = ValueError(exception_message)
        timeout_exc = TimeoutExpired(cmd=None, timeout=1)
        fake_failing_return_code = -15
        # Simulate Popen.wait() timing out twice before raising a ValueError exception.
        self.mock_popen.wait.side_effect = [timeout_exc, timeout_exc, value_err_exc, fake_failing_return_code]
        self.mock_popen.returncode = fake_failing_return_code
        self.mock_popen.pid = 55555
        self._mock_stdout_and_stderr(b"", b"")

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project("echo The power is yours!")

        self.assertEqual(self.mock_kill.call_count, 1, "os.killpg should be called when wait() raises exception.")
        self.assertIn(exception_message, actual_output, "ClusterRunner exception message should be included in output.")
        self.assertEqual(actual_return_code, fake_failing_return_code, "Actual return code should match expected.")
    def test_timing_out_will_break_out_of_command_execution_wait_loop_and_kill_subprocesses(self):
        mock_time = self.patch('time.time')
        mock_time.side_effect = [0.0, 100.0, 200.0, 300.0]  # time increases by 100 seconds with each loop
        expected_return_code = 1
        self._simulate_hanging_popen_process(fake_returncode=expected_return_code)
        self.mock_popen.pid = 55555
        self._mock_stdout_and_stderr(b'fake output', b'fake error')

        project_type = ProjectType()
        actual_output, actual_return_code = project_type.execute_command_in_project(
            command='sleep 99', timeout=250)

        self.assertEqual(self.mock_killpg.call_count, 1, 'os.killpg should be called when execution times out.')
        self.assertEqual(actual_output, 'fake output\nfake error', 'Output should contain stdout and stderr.')
        self.assertEqual(actual_return_code, expected_return_code, 'Actual return code should match expected.')
        self.assertTrue(all([file.close.called for file in self.mock_temporary_files]),
                        'All created TemporaryFiles should be closed so that they are removed from the filesystem.')
    def test_teardown_build_executes_teardown_command(self, expected_timeout):
        project_type = ProjectType()
        mock_execute = MagicMock(return_value=('fake output', 0))
        project_type.execute_command_in_project = mock_execute
        project_type.job_config = MagicMock()

        if expected_timeout:
            project_type.teardown_build(timeout=expected_timeout)
        else:
            project_type.teardown_build()

        mock_execute.assert_called_once_with(ANY, timeout=expected_timeout)
    def test_calling_kill_subprocesses_will_break_out_of_command_execution_wait_loop(self):
        self._mock_out_popen_communicate()

        project_type = ProjectType()
        command_thread = SafeThread(target=project_type.execute_command_in_project, args=('echo The power is yours!',))

        # This calls execute_command_in_project() on one thread, and calls kill_subprocesses() on another. The
        # kill_subprocesses() call should cause the first thread to exit.
        command_thread.start()
        project_type.kill_subprocesses()

        # This *should* join immediately, but we specify a timeout just in case something goes wrong so that the test
        # doesn't hang. A successful join implies success. We also use the UnhandledExceptionHandler so that exceptions
        # propagate from the child thread to the test thread and fail the test.
        with UnhandledExceptionHandler.singleton():
            command_thread.join(timeout=10)
            if command_thread.is_alive():
                self.mock_killpg()  # Calling killpg() causes the command thread to end.
                self.fail('project_type.kill_subprocesses should cause the command execution wait loop to exit.')

        self.mock_killpg.assert_called_once_with(55555, ANY)  # Note: os.killpg does not accept keyword args.
    def test_teardown_build_executes_teardown_command(self, expected_timeout):
        project_type = ProjectType()
        mock_execute = MagicMock(return_value=("fake output", 0))
        project_type.execute_command_in_project = mock_execute
        project_type.job_config = MagicMock()

        if expected_timeout:
            project_type.teardown_build(timeout=expected_timeout)
        else:
            project_type.teardown_build()

        mock_execute.assert_called_once_with(ANY, timeout=expected_timeout)
    def test_teardown_build_runs_teardown(self):
        job_config = JobConfig('name', 'setup', 'teardown', 'command', 'atomizer', 10, 10)
        project_type = ProjectType()
        project_type.job_config = MagicMock(return_value=job_config)
        project_type.execute_command_in_project = MagicMock(return_value=('', 0))

        project_type.teardown_build()

        project_type.execute_command_in_project.assert_called_with('teardown', timeout=None)
Beispiel #26
0
 def _create_mock_project_type(self):
     return MagicMock(spec_set=ProjectType())