#!/usr/bin/env vpython3 # Copyright 2020 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import json import os import shutil import sys import tempfile from textwrap import dedent import unittest # The following non-std imports are fetched via vpython. See the list at # //.vpython3 import mock # pylint: disable=import-error from parameterized import parameterized # pylint: disable=import-error import test_runner _TAST_TEST_RESULTS_JSON = { "name": "login.Chrome", "errors": None, "start": "2020-01-01T15:41:30.799228462-08:00", "end": "2020-01-01T15:41:53.318914698-08:00", "skipReason": "" } class TestRunnerTest(unittest.TestCase): def setUp(self): self._tmp_dir = tempfile.mkdtemp() self.mock_rdb = mock.patch.object( test_runner.result_sink, 'TryInitClient', return_value=None) self.mock_rdb.start() self.mock_env = mock.patch.dict( os.environ, {'SWARMING_BOT_ID': 'cros-chrome-chromeos8-row29'}) self.mock_env.start() def tearDown(self): shutil.rmtree(self._tmp_dir, ignore_errors=True) self.mock_rdb.stop() self.mock_env.stop() def safeAssertItemsEqual(self, list1, list2): """A Py3 safe version of assertItemsEqual. See https://bugs.python.org/issue17866. """ self.assertSetEqual(set(list1), set(list2)) class TastTests(TestRunnerTest): def get_common_tast_args(self, use_vm, fetch_cros_hostname): return [ 'script_name', 'tast', '--suite-name=chrome_all_tast_tests', '--board=eve', '--flash', '--path-to-outdir=out_eve/Release', '--logs-dir=%s' % self._tmp_dir, '--use-vm' if use_vm else ('--fetch-cros-hostname' if fetch_cros_hostname else '--device=localhost:2222'), ] def get_common_tast_expectations(self, use_vm, fetch_cros_hostname): expectation = [ test_runner.CROS_RUN_TEST_PATH, '--board', 'eve', '--cache-dir', test_runner.DEFAULT_CROS_CACHE, '--results-dest-dir', '%s/system_logs' % self._tmp_dir, '--flash', '--build-dir', 'out_eve/Release', '--results-dir', self._tmp_dir, '--tast-total-shards=1', '--tast-shard-index=0', ] expectation.extend(['--start', '--copy-on-write'] if use_vm else ( ['--device', 'chrome-chromeos8-row29'] if fetch_cros_hostname else ['--device', 'localhost:2222'])) for p in test_runner.SYSTEM_LOG_LOCATIONS: expectation.extend(['--results-src', p]) expectation += [ '--mount', '--deploy', '--nostrip', ] return expectation def test_tast_gtest_filter(self): """Tests running tast tests with a gtest-style filter.""" with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: json.dump(_TAST_TEST_RESULTS_JSON, f) args = self.get_common_tast_args(False, False) + [ '--attr-expr=( "group:mainline" && "dep:chrome" && !informational)', '--gtest_filter=login.Chrome:ui.WindowControl', ] with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: mock_popen.return_value.returncode = 0 test_runner.main() # The gtest filter should cause the Tast expr to be replaced with a list # of the tests in the filter. expected_cmd = self.get_common_tast_expectations(False, False) + [ '--tast=("name:login.Chrome" || "name:ui.WindowControl")' ] self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) @parameterized.expand([ [True, False], [False, True], [False, False], ]) def test_tast_attr_expr(self, use_vm, fetch_cros_hostname): """Tests running a tast tests specified by an attribute expression.""" with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: json.dump(_TAST_TEST_RESULTS_JSON, f) args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ '--attr-expr=( "group:mainline" && "dep:chrome" && !informational)', ] with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: mock_popen.return_value.returncode = 0 test_runner.main() expected_cmd = self.get_common_tast_expectations( use_vm, fetch_cros_hostname) + [ '--tast=( "group:mainline" && "dep:chrome" && !informational)', ] self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) @parameterized.expand([ [True, False], [False, True], [False, False], ]) def test_tast_with_vars(self, use_vm, fetch_cros_hostname): """Tests running a tast tests with runtime variables.""" with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: json.dump(_TAST_TEST_RESULTS_JSON, f) args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ '-t=login.Chrome', '--tast-var=key=value', ] with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: mock_popen.return_value.returncode = 0 test_runner.main() expected_cmd = self.get_common_tast_expectations( use_vm, fetch_cros_hostname) + [ '--tast', 'login.Chrome', '--tast-var', 'key=value' ] self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) @parameterized.expand([ [True, False], [False, True], [False, False], ]) def test_tast_retries(self, use_vm, fetch_cros_hostname): """Tests running a tast tests with retries.""" with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: json.dump(_TAST_TEST_RESULTS_JSON, f) args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ '-t=login.Chrome', '--tast-retries=1', ] with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: mock_popen.return_value.returncode = 0 test_runner.main() expected_cmd = self.get_common_tast_expectations( use_vm, fetch_cros_hostname) + ['--tast', 'login.Chrome', '--tast-retries=1'] self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) @parameterized.expand([ [True, False], [False, True], [False, False], ]) def test_tast(self, use_vm, fetch_cros_hostname): """Tests running a tast tests.""" with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: json.dump(_TAST_TEST_RESULTS_JSON, f) args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ '-t=login.Chrome', ] with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: mock_popen.return_value.returncode = 0 test_runner.main() expected_cmd = self.get_common_tast_expectations( use_vm, fetch_cros_hostname) + ['--tast', 'login.Chrome'] self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) class GTestTest(TestRunnerTest): @parameterized.expand([ [True, True, True, False, True], [True, False, False, False, False], [False, True, True, True, True], [False, False, False, True, False], [False, True, True, False, True], [False, False, False, False, False], ]) def test_gtest(self, use_vm, stop_ui, use_test_sudo_helper, fetch_cros_hostname, use_deployed_dbus_configs): """Tests running a gtest.""" fd_mock = mock.mock_open() args = [ 'script_name', 'gtest', '--test-exe=out_eve/Release/base_unittests', '--board=eve', '--path-to-outdir=out_eve/Release', '--use-vm' if use_vm else ('--fetch-cros-hostname' if fetch_cros_hostname else '--device=localhost:2222'), ] if stop_ui: args.append('--stop-ui') if use_test_sudo_helper: args.append('--run-test-sudo-helper') if use_deployed_dbus_configs: args.append('--use-deployed-dbus-configs') with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen,\ mock.patch.object(os, 'fdopen', fd_mock),\ mock.patch.object(os, 'remove') as mock_remove,\ mock.patch.object(tempfile, 'mkstemp', side_effect=[(3, 'out_eve/Release/device_script.sh'),\ (4, 'out_eve/Release/runtime_files.txt')]),\ mock.patch.object(os, 'fchmod'): mock_popen.return_value.returncode = 0 test_runner.main() self.assertEqual(1, mock_popen.call_count) expected_cmd = [ 'vpython3', test_runner.CROS_RUN_TEST_PATH, '--board', 'eve', '--cache-dir', test_runner.DEFAULT_CROS_CACHE, '--remote-cmd', '--cwd', 'out_eve/Release', '--files-from', 'out_eve/Release/runtime_files.txt' ] if not stop_ui: expected_cmd.append('--as-chronos') expected_cmd.extend(['--start', '--copy-on-write'] if use_vm else ( ['--device', 'chrome-chromeos8-row29'] if fetch_cros_hostname else ['--device', 'localhost:2222'])) expected_cmd.extend(['--', './device_script.sh']) self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) expected_device_script = dedent("""\ #!/bin/sh export HOME=/usr/local/tmp export TMPDIR=/usr/local/tmp """) core_cmd = 'LD_LIBRARY_PATH=./ ./out_eve/Release/base_unittests'\ ' --test-launcher-shard-index=0 --test-launcher-total-shards=1' if use_test_sudo_helper: expected_device_script += dedent("""\ TEST_SUDO_HELPER_PATH=$(mktemp) ./test_sudo_helper.py --socket-path=${TEST_SUDO_HELPER_PATH} & TEST_SUDO_HELPER_PID=$! """) core_cmd += ' --test-sudo-helper-socket-path=${TEST_SUDO_HELPER_PATH}' if use_deployed_dbus_configs: expected_device_script += dedent("""\ mount --bind ./dbus /opt/google/chrome/dbus kill -s HUP $(pgrep dbus) """) if stop_ui: dbus_cmd = 'dbus-send --system --type=method_call'\ ' --dest=org.chromium.PowerManager'\ ' /org/chromium/PowerManager'\ ' org.chromium.PowerManager.HandleUserActivity int32:0' expected_device_script += dedent("""\ stop ui {0} chown -R chronos: ../.. sudo -E -u chronos -- /bin/bash -c \"{1}\" TEST_RETURN_CODE=$? start ui """).format(dbus_cmd, core_cmd) else: expected_device_script += dedent("""\ {0} TEST_RETURN_CODE=$? """).format(core_cmd) if use_test_sudo_helper: expected_device_script += dedent("""\ pkill -P $TEST_SUDO_HELPER_PID kill $TEST_SUDO_HELPER_PID unlink ${TEST_SUDO_HELPER_PATH} """) if use_deployed_dbus_configs: expected_device_script += dedent("""\ umount /opt/google/chrome/dbus kill -s HUP $(pgrep dbus) """) expected_device_script += dedent("""\ exit $TEST_RETURN_CODE """) self.assertEqual(2, fd_mock().write.call_count) write_calls = fd_mock().write.call_args_list # Split the strings to make failure messages easier to read. # Verify the first write of device script. self.assertListEqual( expected_device_script.split('\n'), str(write_calls[0][0][0]).split('\n')) # Verify the 2nd write of runtime files. expected_runtime_files = ['out_eve/Release/device_script.sh'] self.assertListEqual(expected_runtime_files, str(write_calls[1][0][0]).strip().split('\n')) mock_remove.assert_called_once_with('out_eve/Release/device_script.sh') def test_gtest_with_vpython(self): """Tests building a gtest with --vpython-dir.""" args = mock.MagicMock() args.test_exe = 'base_unittests' args.test_launcher_summary_output = None args.trace_dir = None args.runtime_deps_path = None args.path_to_outdir = self._tmp_dir args.vpython_dir = self._tmp_dir args.logs_dir = self._tmp_dir # With vpython_dir initially empty, the test_runner should error out # due to missing vpython binaries. gtest = test_runner.GTestTest(args, None) with self.assertRaises(test_runner.TestFormatError): gtest.build_test_command() # Create the two expected tools, and the test should be ready to run. with open(os.path.join(args.vpython_dir, 'vpython3'), 'w'): pass # Just touch the file. os.mkdir(os.path.join(args.vpython_dir, 'bin')) with open(os.path.join(args.vpython_dir, 'bin', 'python3'), 'w'): pass gtest = test_runner.GTestTest(args, None) gtest.build_test_command() class HostCmdTests(TestRunnerTest): @parameterized.expand([ [False, False], [False, True], [True, False], [True, True], ]) def test_host_cmd(self, deploy_chrome, strip_chrome): args = [ 'script_name', 'host-cmd', '--board=eve', '--flash', '--path-to-outdir=out/Release', '--device=localhost:2222', ] if deploy_chrome: args += ['--deploy-chrome'] if strip_chrome: args += ['--strip-chrome'] args += [ '--', 'fake_cmd', ] with mock.patch.object(sys, 'argv', args),\ mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: mock_popen.return_value.returncode = 0 test_runner.main() expected_cmd = [ test_runner.CROS_RUN_TEST_PATH, '--board', 'eve', '--cache-dir', test_runner.DEFAULT_CROS_CACHE, '--flash', '--device', 'localhost:2222', '--host-cmd', ] if deploy_chrome: expected_cmd += [ '--mount', '--deploy', '--build-dir', os.path.join(test_runner.CHROMIUM_SRC_PATH, 'out/Release'), ] if not strip_chrome: expected_cmd += ['--nostrip'] expected_cmd += [ '--', 'fake_cmd', ] self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) if __name__ == '__main__': unittest.main()