| #!/usr/bin/env python |
| # Copyright 2017 Google Inc. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| import contextlib |
| import os.path |
| import random |
| import shutil |
| import sys |
| import tempfile |
| import threading |
| import unittest |
| |
| import gtest_parallel |
| |
| from gtest_parallel_mocks import LoggerMock |
| from gtest_parallel_mocks import SubprocessMock |
| from gtest_parallel_mocks import TestTimesMock |
| from gtest_parallel_mocks import TestResultsMock |
| from gtest_parallel_mocks import TaskManagerMock |
| from gtest_parallel_mocks import TaskMockFactory |
| from gtest_parallel_mocks import TaskMock |
| |
| |
| @contextlib.contextmanager |
| def guard_temp_dir(): |
| try: |
| temp_dir = tempfile.mkdtemp() |
| yield temp_dir |
| finally: |
| shutil.rmtree(temp_dir) |
| |
| @contextlib.contextmanager |
| def guard_temp_subdir(temp_dir, *path): |
| assert path, 'Path should not be empty' |
| |
| try: |
| temp_subdir = os.path.join(temp_dir, *path) |
| os.makedirs(temp_subdir) |
| yield temp_subdir |
| finally: |
| shutil.rmtree(os.path.join(temp_dir, path[0])) |
| |
| @contextlib.contextmanager |
| def guard_patch_module(import_name, new_val): |
| def patch(module, names, val): |
| if len(names) == 1: |
| old = getattr(module, names[0]) |
| setattr(module, names[0], val) |
| return old |
| else: |
| return patch(getattr(module, names[0]), names[1:], val) |
| |
| try: |
| old_val = patch(gtest_parallel, import_name.split('.'), new_val) |
| yield old_val |
| finally: |
| patch(gtest_parallel, import_name.split('.'), old_val) |
| |
| |
| class TestTaskManager(unittest.TestCase): |
| def setUp(self): |
| self.passing_task = ( |
| ('fake_binary', 'Fake.PassingTest'), { |
| 'runtime_ms': [10], |
| 'exit_code': [0], |
| 'last_execution_time': [10], |
| }) |
| self.failing_task = ( |
| ('fake_binary', 'Fake.FailingTest'), { |
| 'runtime_ms': [20, 30, 40], |
| 'exit_code': [1, 1, 1], |
| 'last_execution_time': [None, None, None], |
| }) |
| self.fails_once_then_succeeds = ( |
| ('another_binary', 'Fake.Test.FailOnce'), { |
| 'runtime_ms': [21, 22], |
| 'exit_code': [1, 0], |
| 'last_execution_time': [None, 22], |
| }) |
| self.fails_twice_then_succeeds = ( |
| ('yet_another_binary', 'Fake.Test.FailTwice'), { |
| 'runtime_ms': [23, 25, 24], |
| 'exit_code': [1, 1, 0], |
| 'last_execution_time': [None, None, 24], |
| }) |
| |
| def execute_tasks(self, tasks, retries, expected_exit_code): |
| repeat = 1 |
| |
| times = TestTimesMock(self) |
| logger = LoggerMock(self) |
| test_results = TestResultsMock(self) |
| |
| task_mock_factory = TaskMockFactory(dict(tasks)) |
| task_manager = gtest_parallel.TaskManager( |
| times, logger, test_results, task_mock_factory, retries, repeat) |
| |
| for test_id, expected in tasks: |
| task = task_mock_factory.get_task(test_id) |
| task_manager.run_task(task) |
| expected['execution_number'] = list(range(len(expected['exit_code']))) |
| |
| logger.assertRecorded(test_id, expected, retries + 1) |
| times.assertRecorded(test_id, expected, retries + 1) |
| test_results.assertRecorded(test_id, expected, retries + 1) |
| |
| self.assertEqual(len(task_manager.started), 0) |
| self.assertListEqual( |
| sorted(task.task_id for task in task_manager.passed), |
| sorted(task.task_id for task in task_mock_factory.passed)) |
| self.assertListEqual( |
| sorted(task.task_id for task in task_manager.failed), |
| sorted(task.task_id for task in task_mock_factory.failed)) |
| |
| self.assertEqual(task_manager.global_exit_code, expected_exit_code) |
| |
| def test_passing_task_succeeds(self): |
| self.execute_tasks(tasks=[self.passing_task], retries=0, |
| expected_exit_code=0) |
| |
| def test_failing_task_fails(self): |
| self.execute_tasks(tasks=[self.failing_task], retries=0, |
| expected_exit_code=1) |
| |
| def test_failing_task_fails_even_with_retries(self): |
| self.execute_tasks(tasks=[self.failing_task], retries=2, |
| expected_exit_code=1) |
| |
| def test_executing_passing_and_failing_fails(self): |
| # Executing both a faling test and a passing one should make gtest-parallel |
| # fail, no matter if the failing task is run first or last. |
| self.execute_tasks(tasks=[self.failing_task, self.passing_task], |
| retries=2, expected_exit_code=1) |
| |
| self.execute_tasks(tasks=[self.passing_task, self.failing_task], |
| retries=2, expected_exit_code=1) |
| |
| def test_task_succeeds_with_one_retry(self): |
| # Executes test and retries once. The first run should fail and the second |
| # succeed, so gtest-parallel should succeed. |
| self.execute_tasks(tasks=[self.fails_once_then_succeeds], |
| retries=1, expected_exit_code=0) |
| |
| def test_task_fails_with_one_retry(self): |
| # Executes test and retries once, not enough for the test to start passing, |
| # so gtest-parallel should return an error. |
| self.execute_tasks(tasks=[self.fails_twice_then_succeeds], |
| retries=1, expected_exit_code=1) |
| |
| def test_runner_succeeds_when_all_tasks_eventually_succeeds(self): |
| # Executes the test and retries twice. One test should pass in the first |
| # attempt, another should take two runs, and the last one should take three |
| # runs. All tests should succeed, so gtest-parallel should succeed too. |
| self.execute_tasks(tasks=[self.passing_task, |
| self.fails_once_then_succeeds, |
| self.fails_twice_then_succeeds], |
| retries=2, expected_exit_code=0) |
| |
| |
| class TestSaveFilePath(unittest.TestCase): |
| class StreamMock(object): |
| def write(*args): |
| # Suppress any output. |
| pass |
| |
| def test_get_save_file_path_unix(self): |
| with guard_temp_dir() as temp_dir, \ |
| guard_patch_module('os.path.expanduser', lambda p: temp_dir), \ |
| guard_patch_module('sys.stderr', TestSaveFilePath.StreamMock()), \ |
| guard_patch_module('sys.platform', 'darwin'): |
| with guard_patch_module('os.environ', {}), \ |
| guard_temp_subdir(temp_dir, '.cache'): |
| self.assertEqual(os.path.join(temp_dir, '.cache', 'gtest-parallel'), |
| gtest_parallel.get_save_file_path()) |
| |
| with guard_patch_module('os.environ', {'XDG_CACHE_HOME': temp_dir}): |
| self.assertEqual(os.path.join(temp_dir, 'gtest-parallel'), |
| gtest_parallel.get_save_file_path()) |
| |
| with guard_patch_module('os.environ', |
| {'XDG_CACHE_HOME': os.path.realpath(__file__)}): |
| self.assertEqual(os.path.join(temp_dir, '.gtest-parallel-times'), |
| gtest_parallel.get_save_file_path()) |
| |
| def test_get_save_file_path_win32(self): |
| with guard_temp_dir() as temp_dir, \ |
| guard_patch_module('os.path.expanduser', lambda p: temp_dir), \ |
| guard_patch_module('sys.stderr', TestSaveFilePath.StreamMock()), \ |
| guard_patch_module('sys.platform', 'win32'): |
| with guard_patch_module('os.environ', {}), \ |
| guard_temp_subdir(temp_dir, 'AppData', 'Local'): |
| self.assertEqual(os.path.join(temp_dir, 'AppData', |
| 'Local', 'gtest-parallel'), |
| gtest_parallel.get_save_file_path()) |
| |
| with guard_patch_module('os.environ', {'LOCALAPPDATA': temp_dir}): |
| self.assertEqual(os.path.join(temp_dir, 'gtest-parallel'), |
| gtest_parallel.get_save_file_path()) |
| |
| with guard_patch_module('os.environ', |
| {'LOCALAPPDATA': os.path.realpath(__file__)}): |
| self.assertEqual(os.path.join(temp_dir, '.gtest-parallel-times'), |
| gtest_parallel.get_save_file_path()) |
| |
| |
| class TestSerializeTestCases(unittest.TestCase): |
| def _execute_tasks(self, max_number_of_test_cases, |
| max_number_of_tests_per_test_case, |
| max_number_of_repeats, max_number_of_workers, |
| serialize_test_cases): |
| tasks = [] |
| for test_case in range(max_number_of_test_cases): |
| for test_name in range(max_number_of_tests_per_test_case): |
| # All arguments for gtest_parallel.Task except for test_name are fake. |
| test_name = 'TestCase{}.test{}'.format(test_case, test_name) |
| |
| for execution_number in range(random.randint(1, max_number_of_repeats)): |
| tasks.append(gtest_parallel.Task( |
| 'path/to/binary', test_name, ['path/to/binary', '--gtest_filter=*'], |
| execution_number + 1, None, 'path/to/output')) |
| |
| expected_tasks_number = len(tasks) |
| |
| task_manager = TaskManagerMock() |
| |
| gtest_parallel.execute_tasks(tasks, max_number_of_workers, |
| task_manager, None, serialize_test_cases) |
| |
| self.assertEqual(serialize_test_cases, |
| not task_manager.had_running_parallel_groups) |
| self.assertEqual(expected_tasks_number, task_manager.total_tasks_run) |
| |
| def test_running_parallel_test_cases_without_repeats(self): |
| self._execute_tasks(max_number_of_test_cases=4, |
| max_number_of_tests_per_test_case=32, |
| max_number_of_repeats=1, |
| max_number_of_workers=16, |
| serialize_test_cases=True) |
| |
| def test_running_parallel_test_cases_with_repeats(self): |
| self._execute_tasks(max_number_of_test_cases=4, |
| max_number_of_tests_per_test_case=32, |
| max_number_of_repeats=4, |
| max_number_of_workers=16, |
| serialize_test_cases=True) |
| |
| def test_running_parallel_tests(self): |
| self._execute_tasks(max_number_of_test_cases=4, |
| max_number_of_tests_per_test_case=128, |
| max_number_of_repeats=1, |
| max_number_of_workers=16, |
| serialize_test_cases=False) |
| |
| |
| class TestTestTimes(unittest.TestCase): |
| def test_race_in_test_times_load_save(self): |
| max_number_of_workers = 8 |
| max_number_of_read_write_cycles = 64 |
| test_times_file_name = 'test_times.pickle' |
| |
| def start_worker(save_file): |
| def test_times_worker(): |
| thread_id = threading.current_thread().ident |
| path_to_binary = 'path/to/binary' + hex(thread_id) |
| |
| for cnt in range(max_number_of_read_write_cycles): |
| times = gtest_parallel.TestTimes(save_file) |
| |
| threads_test_times = [ |
| binary for (binary, _) in times._TestTimes__times.keys() |
| if binary.startswith(path_to_binary)] |
| |
| self.assertEqual(cnt, len(threads_test_times)) |
| |
| times.record_test_time('{}-{}'.format(path_to_binary, cnt), |
| 'TestFoo.testBar', 1000) |
| |
| times.write_to_file(save_file) |
| |
| self.assertEqual( |
| 1000, |
| times.get_test_time('{}-{}'.format(path_to_binary, cnt), |
| 'TestFoo.testBar')) |
| self.assertIsNone( |
| times.get_test_time('{}-{}'.format(path_to_binary, cnt), |
| 'baz')) |
| |
| t = threading.Thread(target=test_times_worker) |
| t.start() |
| return t |
| |
| with guard_temp_dir() as temp_dir: |
| try: |
| workers = [start_worker(os.path.join(temp_dir, test_times_file_name)) |
| for _ in range(max_number_of_workers)] |
| finally: |
| for worker in workers: |
| worker.join() |
| |
| |
| class TestTask(unittest.TestCase): |
| def test_log_file_names(self): |
| def root(): |
| return 'C:\\' if sys.platform == 'win32' else '/' |
| |
| self.assertEqual( |
| os.path.join('.', 'bin-Test_case-100.log'), |
| gtest_parallel.Task._logname('.', 'bin', 'Test.case', 100)) |
| |
| self.assertEqual( |
| os.path.join('..', 'a', 'b', 'bin-Test_case_2-1.log'), |
| gtest_parallel.Task._logname(os.path.join('..', 'a', 'b'), |
| os.path.join('..', 'bin'), |
| 'Test.case/2', 1)) |
| |
| self.assertEqual( |
| os.path.join('..', 'a', 'b', 'bin-Test_case_2-5.log'), |
| gtest_parallel.Task._logname(os.path.join('..', 'a', 'b'), |
| os.path.join(root(), 'c', 'd', 'bin'), |
| 'Test.case/2', 5)) |
| |
| self.assertEqual( |
| os.path.join(root(), 'a', 'b', 'bin-Instantiation_Test_case_2-3.log'), |
| gtest_parallel.Task._logname(os.path.join(root(), 'a', 'b'), |
| os.path.join('..', 'c', 'bin'), |
| 'Instantiation/Test.case/2', 3)) |
| |
| self.assertEqual( |
| os.path.join(root(), 'a', 'b', 'bin-Test_case-1.log'), |
| gtest_parallel.Task._logname(os.path.join(root(), 'a', 'b'), |
| os.path.join(root(), 'c', 'd', 'bin'), |
| 'Test.case', 1)) |
| |
| def test_logs_to_temporary_files_without_output_dir(self): |
| log_file = gtest_parallel.Task._logname(None, None, None, None) |
| self.assertEqual(tempfile.gettempdir(), os.path.dirname(log_file)) |
| os.remove(log_file) |
| |
| def _execute_run_test(self, run_test_body, interrupt_test): |
| def popen_mock(*_args, **_kwargs): |
| return None |
| |
| class SigHandlerMock(object): |
| class ProcessWasInterrupted(Exception): |
| pass |
| |
| def wait(*_args): |
| if interrupt_test: |
| raise SigHandlerMock.ProcessWasInterrupted() |
| |
| return 42 |
| |
| with guard_temp_dir() as temp_dir, \ |
| guard_patch_module('subprocess.Popen', popen_mock), \ |
| guard_patch_module('sigint_handler', SigHandlerMock()), \ |
| guard_patch_module('thread.exit', lambda: None): |
| run_test_body(temp_dir) |
| |
| def test_run_normal_task(self): |
| def run_test(temp_dir): |
| task = gtest_parallel.Task('fake/binary', 'test', ['fake/binary'], |
| 1, None, temp_dir) |
| |
| self.assertFalse(os.path.isfile(task.log_file)) |
| |
| task.run() |
| |
| self.assertTrue(os.path.isfile(task.log_file)) |
| self.assertEqual(42, task.exit_code) |
| |
| self._execute_run_test(run_test, False) |
| |
| def test_run_interrupted_task_with_transient_log(self): |
| def run_test(_): |
| task = gtest_parallel.Task('fake/binary', 'test', ['fake/binary'], |
| 1, None, None) |
| |
| self.assertTrue(os.path.isfile(task.log_file)) |
| |
| task.run() |
| |
| self.assertTrue(os.path.isfile(task.log_file)) |
| self.assertIsNone(task.exit_code) |
| |
| self._execute_run_test(run_test, True) |
| |
| |
| class TestFilterFormat(unittest.TestCase): |
| def _execute_test(self, test_body, drop_output): |
| class StdoutMock(object): |
| def isatty(*_args): |
| return False |
| |
| def write(*args): |
| pass |
| |
| with guard_temp_dir() as temp_dir, \ |
| guard_patch_module('sys.stdout', StdoutMock()): |
| logger = gtest_parallel.FilterFormat(None if drop_output else temp_dir) |
| logger.log_tasks(42) |
| |
| test_body(logger) |
| |
| logger.flush() |
| |
| def test_no_output_dir(self): |
| def run_test(logger): |
| passed = [ |
| TaskMock( |
| ('fake/binary', 'FakeTest'), 0, { |
| 'runtime_ms': [10], |
| 'exit_code': [0], |
| 'last_execution_time': [10], |
| 'log_file': [os.path.join(tempfile.gettempdir(), 'fake.log')] |
| }) |
| ] |
| |
| open(passed[0].log_file, 'w').close() |
| self.assertTrue(os.path.isfile(passed[0].log_file)) |
| |
| logger.log_exit(passed[0]) |
| |
| self.assertFalse(os.path.isfile(passed[0].log_file)) |
| |
| logger.print_tests('', passed, True) |
| logger.move_to(None, passed) |
| |
| logger.summarize(passed, [], []) |
| |
| self._execute_test(run_test, True) |
| |
| def test_with_output_dir(self): |
| def run_test(logger): |
| failed = [ |
| TaskMock( |
| ('fake/binary', 'FakeTest'), 0, { |
| 'runtime_ms': [10], |
| 'exit_code': [1], |
| 'last_execution_time': [10], |
| 'log_file': [os.path.join(logger.output_dir, 'fake.log')] |
| }) |
| ] |
| |
| open(failed[0].log_file, 'w').close() |
| self.assertTrue(os.path.isfile(failed[0].log_file)) |
| |
| logger.log_exit(failed[0]) |
| |
| self.assertTrue(os.path.isfile(failed[0].log_file)) |
| |
| logger.print_tests('', failed, True) |
| logger.move_to('failed', failed) |
| |
| self.assertFalse(os.path.isfile(failed[0].log_file)) |
| self.assertTrue( |
| os.path.isfile(os.path.join(logger.output_dir, 'failed', 'fake.log'))) |
| |
| logger.summarize([], failed, []) |
| |
| self._execute_test(run_test, False) |
| |
| |
| class TestFindTests(unittest.TestCase): |
| ONE_DISABLED_ONE_ENABLED_TEST = { |
| "fake_unittests": { |
| "FakeTest": { |
| "Test1": None, |
| "DISABLED_Test2": None, |
| } |
| } |
| } |
| ONE_FAILED_ONE_PASSED_TEST = { |
| "fake_unittests": { |
| "FakeTest": { |
| # Failed (and new) tests have no recorded runtime. |
| "FailedTest": None, |
| "Test": 1, |
| } |
| } |
| } |
| ONE_TEST = { |
| "fake_unittests": { |
| "FakeTest": { |
| "TestSomething": None, |
| } |
| } |
| } |
| MULTIPLE_BINARIES_MULTIPLE_TESTS_ONE_FAILURE = { |
| "fake_unittests": { |
| "FakeTest": { |
| "TestSomething": None, |
| "TestSomethingElse": 2, |
| }, |
| "SomeOtherTest": { |
| "YetAnotherTest": 3, |
| }, |
| }, |
| "fake_tests": { |
| "Foo": { |
| "Bar": 4, |
| "Baz": 4, |
| } |
| } |
| } |
| |
| def _process_options(self, options): |
| parser = gtest_parallel.default_options_parser() |
| options, binaries = parser.parse_args(options) |
| self.assertEqual(len(binaries), 0) |
| return options |
| |
| def _call_find_tests(self, test_data, options=None): |
| subprocess_mock = SubprocessMock(test_data) |
| options = self._process_options(options or []) |
| with guard_patch_module('subprocess.check_output', subprocess_mock): |
| tasks = gtest_parallel.find_tests( |
| test_data.keys(), [], options, TestTimesMock(self, test_data)) |
| # Clean transient tasks' log files created because |
| # by default now output_dir is None. |
| for task in tasks: |
| if os.path.isfile(task.log_file): |
| os.remove(task.log_file) |
| return tasks, subprocess_mock |
| |
| def test_tasks_are_sorted(self): |
| tasks, _ = self._call_find_tests( |
| self.MULTIPLE_BINARIES_MULTIPLE_TESTS_ONE_FAILURE) |
| self.assertEqual([task.last_execution_time for task in tasks], |
| [None, 4, 4, 3, 2]) |
| |
| def test_does_not_run_disabled_tests_by_default(self): |
| tasks, subprocess_mock = self._call_find_tests( |
| self.ONE_DISABLED_ONE_ENABLED_TEST) |
| self.assertEqual(len(tasks), 1) |
| self.assertFalse("DISABLED_" in tasks[0].test_name) |
| self.assertNotIn("--gtest_also_run_disabled_tests", |
| subprocess_mock.last_invocation) |
| |
| def test_runs_disabled_tests_when_asked(self): |
| tasks, subprocess_mock = self._call_find_tests( |
| self.ONE_DISABLED_ONE_ENABLED_TEST, |
| ['--gtest_also_run_disabled_tests']) |
| self.assertEqual(len(tasks), 2) |
| self.assertEqual(sorted([task.test_name for task in tasks]), |
| ["FakeTest.DISABLED_Test2", "FakeTest.Test1"]) |
| self.assertIn("--gtest_also_run_disabled_tests", |
| subprocess_mock.last_invocation) |
| |
| def test_runs_failed_tests_by_default(self): |
| tasks, _ = self._call_find_tests(self.ONE_FAILED_ONE_PASSED_TEST) |
| self.assertEqual(len(tasks), 2) |
| self.assertEqual(sorted([task.test_name for task in tasks]), |
| ["FakeTest.FailedTest", "FakeTest.Test"]) |
| self.assertEqual({task.last_execution_time for task in tasks}, |
| {None, 1}) |
| |
| def test_runs_only_failed_tests_when_asked(self): |
| tasks, _ = self._call_find_tests( |
| self.ONE_FAILED_ONE_PASSED_TEST, ['--failed']) |
| self.assertEqual(len(tasks), 1) |
| self.assertEqual(tasks[0].test_binary, "fake_unittests") |
| self.assertEqual(tasks[0].test_name, "FakeTest.FailedTest") |
| self.assertIsNone(tasks[0].last_execution_time) |
| |
| def test_does_not_apply_gtest_filter_by_default(self): |
| _, subprocess_mock = self._call_find_tests(self.ONE_TEST) |
| self.assertFalse(any( |
| arg.startswith('--gtest_filter=SomeFilter') |
| for arg in subprocess_mock.last_invocation |
| )) |
| |
| def test_applies_gtest_filter(self): |
| _, subprocess_mock = self._call_find_tests( |
| self.ONE_TEST, ['--gtest_filter=SomeFilter']) |
| self.assertIn('--gtest_filter=SomeFilter', subprocess_mock.last_invocation) |
| |
| def test_applies_gtest_color_by_default(self): |
| tasks, _ = self._call_find_tests(self.ONE_TEST) |
| self.assertEqual(len(tasks), 1) |
| self.assertIn('--gtest_color=yes', tasks[0].test_command) |
| |
| def test_applies_gtest_color(self): |
| tasks, _ = self._call_find_tests(self.ONE_TEST, ['--gtest_color=Lemur']) |
| self.assertEqual(len(tasks), 1) |
| self.assertIn('--gtest_color=Lemur', tasks[0].test_command) |
| |
| def test_repeats_tasks_once_by_default(self): |
| tasks, _ = self._call_find_tests(self.ONE_TEST) |
| self.assertEqual(len(tasks), 1) |
| |
| def test_repeats_tasks_multiple_times(self): |
| tasks, _ = self._call_find_tests(self.ONE_TEST, ['--repeat=3']) |
| self.assertEqual(len(tasks), 3) |
| # Test all tasks have the same test_name, test_binary and test_command |
| all_tasks_set = set( |
| (task.test_name, task.test_binary, tuple(task.test_command)) |
| for task in tasks |
| ) |
| self.assertEqual(len(all_tasks_set), 1) |
| # Test tasks have consecutive execution_numbers starting from 1 |
| self.assertEqual(sorted(task.execution_number for task in tasks), |
| [1, 2, 3]) |
| |
| def test_gtest_list_tests_fails(self): |
| def exit_mock(*args): |
| raise AssertionError('Foo') |
| |
| options = self._process_options([]) |
| with guard_patch_module('sys.exit', exit_mock): |
| self.assertRaises(AssertionError, gtest_parallel.find_tests, |
| [sys.executable], [], options, None) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |