| #!/usr/bin/env vpython3 |
| # Copyright 2018 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unittests for xcodebuild_runner.py.""" |
| |
| import logging |
| import mock |
| import os |
| import unittest |
| import sys |
| |
| import mac_util |
| import iossim_util |
| import result_sink_util |
| import test_apps |
| from test_result_util import ResultCollection, TestResult, TestStatus |
| import test_runner |
| import test_runner_test |
| import xcode_log_parser |
| import xcode_util |
| import xcodebuild_runner |
| |
| # if the current directory is in scripts, then we need to add plugin |
| # path in order to import from that directory |
| if os.path.split(os.path.dirname(__file__))[1] != 'plugin': |
| sys.path.append( |
| os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plugin')) |
| import test_plugin_service |
| |
| _ROOT_FOLDER_PATH = 'root/folder' |
| _XCODE_BUILD_VERSION = '10B61' |
| _DESTINATION = 'A4E66321-177A-450A-9BA1-488D85B7278E' |
| _OUT_DIR = 'out/dir' |
| _XTEST_RUN = '/tmp/temp_file.xctestrun' |
| _EGTESTS_APP_PATH = '%s/any_egtests.app' % _ROOT_FOLDER_PATH |
| _ALL_EG_TEST_NAMES = [('Class1', 'passedTest1'), ('Class1', 'passedTest2')] |
| _FLAKY_EGTEST_APP_PATH = 'path/to/ios_chrome_flaky_eg2test_module.app' |
| |
| _ENUMERATE_TESTS_OUTPUT = """ |
| { |
| "errors" : [ |
| |
| ], |
| "values" : [ |
| { |
| "children" : [ |
| { |
| "children" : [ |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "BaseEarlGreyTestCase" |
| }, |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "ChromeTestCase" |
| }, |
| { |
| "children" : [ |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "test", |
| "name" : "passedTest1" |
| }, |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "test", |
| "name" : "passedTest2" |
| } |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "Class1" |
| }, |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "WebHttpServerChromeTestCase" |
| } |
| ], |
| "disabled" : false, |
| "kind" : "target", |
| "name" : "ios_chrome_ui_eg2tests_module-Runner_module" |
| } |
| ], |
| "disabled" : false, |
| "kind" : "plan", |
| "name" : "" |
| } |
| ] |
| } |
| """ |
| |
| _ENUMERATE_DISABLED_TESTS_OUTPUT = """ |
| { |
| "errors" : [ |
| |
| ], |
| "values" : [ |
| { |
| "children" : [ |
| { |
| "children" : [ |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "BaseEarlGreyTestCase" |
| }, |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "ChromeTestCase" |
| }, |
| { |
| "children" : [ |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "test", |
| "name" : "disabled_test3" |
| } |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "Class2" |
| }, |
| { |
| "children" : [ |
| |
| ], |
| "disabled" : false, |
| "kind" : "class", |
| "name" : "WebHttpServerChromeTestCase" |
| } |
| ], |
| "disabled" : false, |
| "kind" : "target", |
| "name" : "ios_chrome_ui_eg2tests_module-Runner_module" |
| } |
| ], |
| "disabled" : false, |
| "kind" : "plan", |
| "name" : "" |
| } |
| ] |
| } |
| """ |
| |
| class XCodebuildRunnerTest(test_runner_test.TestCase): |
| """Test case to test xcodebuild_runner.""" |
| |
| def setUp(self): |
| super(XCodebuildRunnerTest, self).setUp() |
| self.mock(os.path, 'exists', lambda _: True) |
| self.mock(os, 'listdir', lambda _: ['any_egtests.xctest']) |
| self.mock(iossim_util, 'is_device_with_udid_simulator', lambda _: False) |
| self.mock(result_sink_util.ResultSinkClient, |
| 'post', lambda *args, **kwargs: None) |
| self.mock(test_apps.EgtestsApp, 'get_all_tests', |
| lambda _: ['Class1/passedTest1', 'Class1/passedTest2']) |
| self.mock(test_apps.EgtestsApp, 'fill_xctest_run', |
| lambda _1, _2: 'xctestrun') |
| self.mock(iossim_util, 'get_simulator', lambda _1, _2: 'sim-UUID') |
| self.mock(test_apps, 'get_bundle_id', lambda _: "fake-bundle-id") |
| self.mock(test_apps, 'is_running_rosetta', lambda: False) |
| self.mock(test_apps.plistlib, 'dump', lambda _1, _2: '') |
| self.mock(test_runner.SimulatorTestRunner, 'tear_down', lambda _: None) |
| self.mock(test_runner.DeviceTestRunner, 'tear_down', lambda _: None) |
| self.mock(xcodebuild_runner.subprocess, |
| 'Popen', lambda cmd, env, stdout, stderr: 'fake-out') |
| self.mock(test_runner, 'print_process_output', lambda _, timeout: []) |
| self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path') |
| self.mock(os.path, 'isfile', lambda _: True) |
| self.mock(xcodebuild_runner.SimulatorParallelTestRunner, |
| '_create_xctest_run_enum_tests', |
| lambda _, include_disabled: 'fake-path') |
| |
| def tearDown(self): |
| super(XCodebuildRunnerTest, self).tearDown() |
| |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| def testLaunchCommand_restartCrashed1stAttempt(self, mock_collect_results): |
| egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES) |
| crashed_collection = ResultCollection() |
| crashed_collection.crashed = True |
| mock_collect_results.side_effect = [ |
| crashed_collection, |
| ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| ] |
| launch_command = xcodebuild_runner.LaunchCommand( |
| egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180) |
| overall_result = launch_command.launch() |
| self.assertFalse(overall_result.crashed) |
| self.assertEqual(len(overall_result.all_test_names()), 2) |
| self.assertEqual(overall_result.expected_tests(), |
| set(['Class1/passedTest1', 'Class1/passedTest2'])) |
| |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| def testLaunchCommand_notRestartPassedTest(self, mock_collect_results): |
| egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES) |
| collection = ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| mock_collect_results.side_effect = [collection] |
| launch_command = xcodebuild_runner.LaunchCommand( |
| egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180) |
| launch_command.launch() |
| xcodebuild_runner.LaunchCommand( |
| egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180) |
| self.assertEqual(1, len(mock_collect_results.mock_calls)) |
| |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| def test_launch_command_restart_failed_attempt(self, mock_collect_results): |
| egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES) |
| mock_collect_results.side_effect = [ |
| ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.FAIL), |
| TestResult('Class1/passedTest2', TestStatus.FAIL) |
| ]), |
| ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| ] |
| launch_command = xcodebuild_runner.LaunchCommand( |
| egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180) |
| overall_result = launch_command.launch() |
| self.assertEqual(len(overall_result.all_test_names()), 2) |
| self.assertEqual(overall_result.expected_tests(), |
| set(['Class1/passedTest1', 'Class1/passedTest2'])) |
| |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| def test_launch_command_not_restart_crashed_attempt(self, |
| mock_collect_results): |
| """Crashed first attempt of runtime select test suite won't be retried.""" |
| egtests = test_apps.EgtestsApp(_FLAKY_EGTEST_APP_PATH, _ALL_EG_TEST_NAMES) |
| crashed_collection = ResultCollection() |
| crashed_collection.crashed = True |
| mock_collect_results.return_value = crashed_collection |
| launch_command = xcodebuild_runner.LaunchCommand( |
| egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180) |
| overall_result = launch_command.launch() |
| self.assertEqual(len(overall_result.all_test_names()), 0) |
| self.assertEqual(overall_result.expected_tests(), set([])) |
| self.assertTrue(overall_result.crashed) |
| |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| def test_launch_command_reset_video_plugin_before_attempt( |
| self, mock_collect_results): |
| egtests = test_apps.EgtestsApp(_EGTESTS_APP_PATH, _ALL_EG_TEST_NAMES) |
| collection = ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| mock_collect_results.side_effect = [collection] |
| mock_plugin_service = mock.MagicMock() |
| launch_command = xcodebuild_runner.LaunchCommand( |
| egtests, |
| _DESTINATION, |
| clones=1, |
| retries=3, |
| readline_timeout=180, |
| test_plugin_service=mock_plugin_service) |
| launch_command.launch() |
| xcodebuild_runner.LaunchCommand( |
| egtests, _DESTINATION, clones=1, retries=3, readline_timeout=180) |
| self.assertEqual(1, len(mock_collect_results.mock_calls)) |
| mock_plugin_service.reset.assert_called_once_with() |
| |
| |
| class DeviceXcodeTestRunnerTest(test_runner_test.TestCase): |
| """Test case to test xcodebuild_runner.DeviceXcodeTestRunner.""" |
| |
| def setUp(self): |
| super(DeviceXcodeTestRunnerTest, self).setUp() |
| self.mock(os.path, 'exists', lambda _: True) |
| self.mock(test_runner, 'get_current_xcode_info', lambda: { |
| 'version': 'test version', 'build': 'test build', 'path': 'test/path'}) |
| self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path) |
| |
| self.mock(result_sink_util.ResultSinkClient, |
| 'post', lambda *args, **kwargs: None) |
| self.mock( |
| test_runner.subprocess, |
| 'check_output', |
| lambda _, stderr=None: b'fake-output') |
| self.mock(test_runner.subprocess, 'check_call', lambda _: b'fake-out') |
| self.mock(test_runner.subprocess, |
| 'Popen', lambda cmd, env, stdout, stderr: 'fake-out') |
| self.mock(test_runner.TestRunner, 'set_sigterm_handler', |
| lambda self, handler: 0) |
| self.mock(os, 'listdir', lambda _: []) |
| self.mock(xcodebuild_runner.subprocess, |
| 'Popen', lambda cmd, env, stdout, stderr: 'fake-out') |
| self.mock(test_runner, 'print_process_output', lambda _, timeout: []) |
| self.mock(test_runner.TestRunner, 'start_proc', lambda self, cmd: 0) |
| self.mock(test_runner.DeviceTestRunner, 'get_installed_packages', |
| lambda self: []) |
| self.mock(test_runner.DeviceTestRunner, 'wipe_derived_data', lambda _: None) |
| self.mock(test_runner.TestRunner, 'retrieve_derived_data', lambda _: None) |
| self.mock(test_runner.TestRunner, 'process_xcresult_dir', lambda _: None) |
| self.mock(test_apps.EgtestsApp, |
| 'fill_xctest_run', lambda _1, _2: 'xctestrun') |
| self.mock(test_apps.EgtestsApp, 'get_all_tests', |
| lambda _: ['Class1/passedTest1', 'Class1/passedTest2']) |
| self.mock(iossim_util, 'is_device_with_udid_simulator', lambda _: False) |
| self.mock(xcode_util, 'using_xcode_15_or_higher', lambda: True) |
| self.mock(mac_util, 'kill_usbmuxd', lambda: None) |
| self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path') |
| self.mock(os.path, 'isfile', lambda _: True) |
| self.mock(xcodebuild_runner.SimulatorParallelTestRunner, |
| '_create_xctest_run_enum_tests', |
| lambda _, include_disabled: 'fake-path') |
| |
| @mock.patch( |
| 'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT)) |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| @mock.patch('platform.system', return_value='Darwin') |
| def test_launch(self, _, mock_result): |
| """Tests launch method in DeviceXcodeTestRunner""" |
| tr = xcodebuild_runner.DeviceXcodeTestRunner("fake-app-path", |
| "fake-host-app-path", |
| "fake-out-dir") |
| mock_result.return_value = ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| self.assertTrue(tr.launch()) |
| self.assertEqual(len(tr.test_results['tests']), 2) |
| |
| @mock.patch( |
| 'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT)) |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| @mock.patch('platform.system', return_value='Darwin') |
| def test_unexpected_skipped_crash_reported(self, _, mock_result): |
| """Tests launch method in DeviceXcodeTestRunner""" |
| tr = xcodebuild_runner.DeviceXcodeTestRunner("fake-app-path", |
| "fake-host-app-path", |
| "fake-out-dir") |
| crashed_collection = ResultCollection( |
| test_results=[TestResult('Class1/passedTest1', TestStatus.PASS)]) |
| crashed_collection.crashed = True |
| mock_result.return_value = crashed_collection |
| self.assertFalse(tr.launch()) |
| self.assertEqual(len(tr.test_results['tests']), 2) |
| tests = tr.test_results['tests'] |
| self.assertEqual(tests['Class1/passedTest1']['actual'], 'PASS') |
| self.assertEqual(tests['Class1/passedTest2']['actual'], 'SKIP') |
| self.assertEqual(tests['Class1/passedTest2']['expected'], 'PASS') |
| |
| @mock.patch( |
| 'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT)) |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| @mock.patch('platform.system', return_value='Darwin') |
| def test_unexpected_skipped_not_reported(self, _, mock_result): |
| """Unexpected skip not reported for these selecting tests at runtime.""" |
| crashed_collection = ResultCollection( |
| test_results=[TestResult('Class1/passedTest1', TestStatus.PASS)]) |
| crashed_collection.crashed = True |
| mock_result.return_value = crashed_collection |
| tr = xcodebuild_runner.DeviceXcodeTestRunner(_FLAKY_EGTEST_APP_PATH, |
| "fake-host-app-path", |
| "fake-out-dir") |
| self.assertFalse(tr.launch()) |
| self.assertEqual(len(tr.test_results['tests']), 1) |
| tests = tr.test_results['tests'] |
| self.assertEqual(tests['Class1/passedTest1']['actual'], 'PASS') |
| # Class1/passedTest2 doesn't appear in test results. |
| |
| @mock.patch( |
| 'builtins.open', |
| new=mock.mock_open(read_data=_ENUMERATE_DISABLED_TESTS_OUTPUT)) |
| @mock.patch('xcodebuild_runner.isinstance', return_value=True) |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| @mock.patch('test_apps.EgtestsApp', autospec=True) |
| @mock.patch('platform.system', return_value='Darwin') |
| def test_disabled_reported(self, _, mock_test_app, mock_result, __): |
| """Tests launch method in DeviceXcodeTestRunner""" |
| test_app = mock_test_app.return_value |
| test_app.test_app_path = _EGTESTS_APP_PATH |
| test_app.get_all_tests.return_value = [ |
| 'Class1/passedTest1', 'Class1/passedTest2' |
| ] |
| mock_result.return_value = ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| tr = xcodebuild_runner.DeviceXcodeTestRunner( |
| "fake-app-path", |
| "fake-host-app-path", |
| "fake-out-dir", |
| output_disabled_tests=True) |
| self.assertTrue(tr.launch()) |
| self.assertEqual(len(tr.test_results['tests']), 3) |
| tests = tr.test_results['tests'] |
| self.assertEqual(tests['Class1/passedTest1']['actual'], 'PASS') |
| self.assertEqual(tests['Class1/passedTest2']['actual'], 'PASS') |
| self.assertEqual(tests['Class2/disabled_test3']['actual'], 'SKIP') |
| self.assertEqual(tests['Class2/disabled_test3']['expected'], 'SKIP') |
| |
| @mock.patch( |
| 'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT)) |
| def test_tear_down(self): |
| tr = xcodebuild_runner.DeviceXcodeTestRunner( |
| "fake-app-path", "fake-host-app-path", "fake-out-dir") |
| tr.tear_down() |
| |
| |
| class SimulatorParallelTestRunnerTest(test_runner_test.TestCase): |
| """Test case to test xcodebuild_runner.SimulatorParallelTestRunner""" |
| |
| def setUp(self): |
| super(SimulatorParallelTestRunnerTest, self).setUp() |
| self.mock(iossim_util, 'get_simulator', lambda _1, _2: 'sim-UUID') |
| |
| def set_up(self): |
| return |
| |
| self.mock(xcodebuild_runner.SimulatorParallelTestRunner, 'set_up', set_up) |
| self.mock(os.path, 'exists', lambda _: True) |
| self.mock( |
| test_runner, 'get_current_xcode_info', lambda: { |
| 'version': 'test version', |
| 'build': 'test build', |
| 'path': 'test/path' |
| }) |
| self.mock(os.path, 'abspath', lambda path: '/abs/path/to/%s' % path) |
| |
| self.mock(result_sink_util.ResultSinkClient, |
| 'post', lambda *args, **kwargs: None) |
| self.mock( |
| test_runner.subprocess, |
| 'check_output', |
| lambda _, stderr=None: b'fake-output') |
| self.mock(test_runner.subprocess, 'check_call', lambda _: b'fake-out') |
| self.mock(test_runner.subprocess, |
| 'Popen', lambda cmd, env, stdout, stderr: 'fake-out') |
| self.mock(test_runner.TestRunner, |
| 'set_sigterm_handler', lambda self, handler: 0) |
| self.mock(os, 'listdir', lambda _: []) |
| self.mock(xcodebuild_runner.subprocess, |
| 'Popen', lambda cmd, env, stdout, stderr: 'fake-out') |
| self.mock(test_runner, 'print_process_output', lambda _, timeout: []) |
| self.mock(test_runner.TestRunner, 'start_proc', lambda self, cmd: 0) |
| self.mock(test_runner.TestRunner, 'retrieve_derived_data', lambda _: None) |
| self.mock(test_runner.TestRunner, 'process_xcresult_dir', lambda _: None) |
| self.mock(test_apps.EgtestsApp, 'fill_xctest_run', |
| lambda _1, _2: 'xctestrun') |
| self.mock(test_apps.EgtestsApp, 'get_all_tests', |
| lambda _: ['Class1/passedTest1', 'Class1/passedTest2']) |
| self.mock(iossim_util, 'is_device_with_udid_simulator', lambda _: False) |
| self.mock(xcode_util, 'xctest_path', lambda _: 'fake-path') |
| self.mock(os.path, 'isfile', lambda _: True) |
| self.mock(xcodebuild_runner.SimulatorParallelTestRunner, |
| '_create_xctest_run_enum_tests', |
| lambda _, include_disabled: 'fake-path') |
| |
| @mock.patch( |
| 'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT)) |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| @mock.patch('platform.system', return_value='Darwin') |
| def test_launch_egtest(self, _, mock_result): |
| """Tests launch method in SimulatorParallelTestRunner""" |
| tr = xcodebuild_runner.SimulatorParallelTestRunner( |
| "fake-app-path", "fake-host-app-path", "fake-iossim_path", |
| "fake-version", "fake-platform", "fake-out-dir") |
| mock_result.return_value = ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| self.assertTrue(tr.launch()) |
| self.assertEqual(len(tr.test_results['tests']), 2) |
| |
| @mock.patch( |
| 'builtins.open', new=mock.mock_open(read_data=_ENUMERATE_TESTS_OUTPUT)) |
| @mock.patch('xcode_log_parser.XcodeLogParser.collect_test_results') |
| @mock.patch('xcodebuild_runner.TestPluginServicerWrapper') |
| @mock.patch('platform.system', return_value='Darwin') |
| def test_launch_egtest_with_plugin_service(self, _, mock_plugin_service, |
| mock_result): |
| """ Tests launch method in SimulatorParallelTestRunner |
| with plugin service running """ |
| tr = xcodebuild_runner.SimulatorParallelTestRunner( |
| "fake-app-path", |
| "fake-host-app-path", |
| "fake-iossim_path", |
| "fake-version", |
| "fake-platform", |
| "fake-out-dir", |
| video_plugin_option='failed_only') |
| self.assertTrue(tr.test_plugin_service != None) |
| tr.test_plugin_service = mock_plugin_service |
| mock_result.return_value = ResultCollection(test_results=[ |
| TestResult('Class1/passedTest1', TestStatus.PASS), |
| TestResult('Class1/passedTest2', TestStatus.PASS) |
| ]) |
| self.assertTrue(tr.launch()) |
| self.assertEqual(len(tr.test_results['tests']), 2) |
| mock_plugin_service.start_server.assert_called_once_with() |
| mock_plugin_service.reset.assert_called_once_with() |
| mock_plugin_service.tear_down.assert_called_once_with() |
| |
| |
| if __name__ == '__main__': |
| logging.basicConfig( |
| format='[%(asctime)s:%(levelname)s] %(message)s', |
| level=logging.DEBUG, |
| datefmt='%I:%M:%S') |
| unittest.main() |