1#!/usr/bin/env vpython3 2# Copyright 2020 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import json 7import os 8import shutil 9import sys 10import tempfile 11from textwrap import dedent 12import unittest 13 14# The following non-std imports are fetched via vpython. See the list at 15# //.vpython3 16import mock # pylint: disable=import-error 17from parameterized import parameterized # pylint: disable=import-error 18 19import test_runner 20 21_TAST_TEST_RESULTS_JSON = { 22 "name": "login.Chrome", 23 "errors": None, 24 "start": "2020-01-01T15:41:30.799228462-08:00", 25 "end": "2020-01-01T15:41:53.318914698-08:00", 26 "skipReason": "" 27} 28 29 30class TestRunnerTest(unittest.TestCase): 31 32 def setUp(self): 33 self._tmp_dir = tempfile.mkdtemp() 34 self.mock_rdb = mock.patch.object( 35 test_runner.result_sink, 'TryInitClient', return_value=None) 36 self.mock_rdb.start() 37 self.mock_env = mock.patch.dict( 38 os.environ, {'SWARMING_BOT_ID': 'cros-chrome-chromeos8-row29'}) 39 self.mock_env.start() 40 41 def tearDown(self): 42 shutil.rmtree(self._tmp_dir, ignore_errors=True) 43 self.mock_rdb.stop() 44 self.mock_env.stop() 45 46 def safeAssertItemsEqual(self, list1, list2): 47 """A Py3 safe version of assertItemsEqual. 48 49 See https://bugs.python.org/issue17866. 50 """ 51 self.assertSetEqual(set(list1), set(list2)) 52 53 54class TastTests(TestRunnerTest): 55 56 def get_common_tast_args(self, use_vm, fetch_cros_hostname): 57 return [ 58 'script_name', 59 'tast', 60 '--suite-name=chrome_all_tast_tests', 61 '--board=eve', 62 '--flash', 63 '--path-to-outdir=out_eve/Release', 64 '--logs-dir=%s' % self._tmp_dir, 65 '--use-vm' if use_vm else 66 ('--fetch-cros-hostname' 67 if fetch_cros_hostname else '--device=localhost:2222'), 68 ] 69 70 def get_common_tast_expectations(self, use_vm, fetch_cros_hostname): 71 expectation = [ 72 test_runner.CROS_RUN_TEST_PATH, 73 '--board', 74 'eve', 75 '--cache-dir', 76 test_runner.DEFAULT_CROS_CACHE, 77 '--results-dest-dir', 78 '%s/system_logs' % self._tmp_dir, 79 '--flash', 80 '--build-dir', 81 'out_eve/Release', 82 '--results-dir', 83 self._tmp_dir, 84 '--tast-total-shards=1', 85 '--tast-shard-index=0', 86 ] 87 expectation.extend(['--start', '--copy-on-write'] if use_vm else ( 88 ['--device', 'chrome-chromeos8-row29'] 89 if fetch_cros_hostname else ['--device', 'localhost:2222'])) 90 for p in test_runner.SYSTEM_LOG_LOCATIONS: 91 expectation.extend(['--results-src', p]) 92 93 expectation += [ 94 '--mount', 95 '--deploy', 96 '--nostrip', 97 ] 98 return expectation 99 100 def test_tast_gtest_filter(self): 101 """Tests running tast tests with a gtest-style filter.""" 102 with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: 103 json.dump(_TAST_TEST_RESULTS_JSON, f) 104 105 args = self.get_common_tast_args(False, False) + [ 106 '--attr-expr=( "group:mainline" && "dep:chrome" && !informational)', 107 '--gtest_filter=login.Chrome:ui.WindowControl', 108 ] 109 with mock.patch.object(sys, 'argv', args),\ 110 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: 111 mock_popen.return_value.returncode = 0 112 113 test_runner.main() 114 # The gtest filter should cause the Tast expr to be replaced with a list 115 # of the tests in the filter. 116 expected_cmd = self.get_common_tast_expectations(False, False) + [ 117 '--tast=("name:login.Chrome" || "name:ui.WindowControl")' 118 ] 119 120 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 121 122 @parameterized.expand([ 123 [True, False], 124 [False, True], 125 [False, False], 126 ]) 127 def test_tast_attr_expr(self, use_vm, fetch_cros_hostname): 128 """Tests running a tast tests specified by an attribute expression.""" 129 with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: 130 json.dump(_TAST_TEST_RESULTS_JSON, f) 131 132 args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ 133 '--attr-expr=( "group:mainline" && "dep:chrome" && !informational)', 134 ] 135 with mock.patch.object(sys, 'argv', args),\ 136 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: 137 mock_popen.return_value.returncode = 0 138 139 test_runner.main() 140 expected_cmd = self.get_common_tast_expectations( 141 use_vm, fetch_cros_hostname) + [ 142 '--tast=( "group:mainline" && "dep:chrome" && !informational)', 143 ] 144 145 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 146 147 @parameterized.expand([ 148 [True, False], 149 [False, True], 150 [False, False], 151 ]) 152 def test_tast_with_vars(self, use_vm, fetch_cros_hostname): 153 """Tests running a tast tests with runtime variables.""" 154 with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: 155 json.dump(_TAST_TEST_RESULTS_JSON, f) 156 157 args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ 158 '-t=login.Chrome', 159 '--tast-var=key=value', 160 ] 161 with mock.patch.object(sys, 'argv', args),\ 162 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: 163 mock_popen.return_value.returncode = 0 164 test_runner.main() 165 expected_cmd = self.get_common_tast_expectations( 166 use_vm, fetch_cros_hostname) + [ 167 '--tast', 'login.Chrome', '--tast-var', 'key=value' 168 ] 169 170 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 171 172 @parameterized.expand([ 173 [True, False], 174 [False, True], 175 [False, False], 176 ]) 177 def test_tast_retries(self, use_vm, fetch_cros_hostname): 178 """Tests running a tast tests with retries.""" 179 with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: 180 json.dump(_TAST_TEST_RESULTS_JSON, f) 181 182 args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ 183 '-t=login.Chrome', 184 '--tast-retries=1', 185 ] 186 with mock.patch.object(sys, 'argv', args),\ 187 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: 188 mock_popen.return_value.returncode = 0 189 test_runner.main() 190 expected_cmd = self.get_common_tast_expectations( 191 use_vm, 192 fetch_cros_hostname) + ['--tast', 'login.Chrome', '--tast-retries=1'] 193 194 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 195 196 @parameterized.expand([ 197 [True, False], 198 [False, True], 199 [False, False], 200 ]) 201 def test_tast(self, use_vm, fetch_cros_hostname): 202 """Tests running a tast tests.""" 203 with open(os.path.join(self._tmp_dir, 'streamed_results.jsonl'), 'w') as f: 204 json.dump(_TAST_TEST_RESULTS_JSON, f) 205 206 args = self.get_common_tast_args(use_vm, fetch_cros_hostname) + [ 207 '-t=login.Chrome', 208 ] 209 with mock.patch.object(sys, 'argv', args),\ 210 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: 211 mock_popen.return_value.returncode = 0 212 213 test_runner.main() 214 expected_cmd = self.get_common_tast_expectations( 215 use_vm, fetch_cros_hostname) + ['--tast', 'login.Chrome'] 216 217 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 218 219 220class GTestTest(TestRunnerTest): 221 222 @parameterized.expand([ 223 [True, True, True, False, True], 224 [True, False, False, False, False], 225 [False, True, True, True, True], 226 [False, False, False, True, False], 227 [False, True, True, False, True], 228 [False, False, False, False, False], 229 ]) 230 def test_gtest(self, use_vm, stop_ui, use_test_sudo_helper, 231 fetch_cros_hostname, use_deployed_dbus_configs): 232 """Tests running a gtest.""" 233 fd_mock = mock.mock_open() 234 235 args = [ 236 'script_name', 237 'gtest', 238 '--test-exe=out_eve/Release/base_unittests', 239 '--board=eve', 240 '--path-to-outdir=out_eve/Release', 241 '--use-vm' if use_vm else 242 ('--fetch-cros-hostname' 243 if fetch_cros_hostname else '--device=localhost:2222'), 244 ] 245 if stop_ui: 246 args.append('--stop-ui') 247 if use_test_sudo_helper: 248 args.append('--run-test-sudo-helper') 249 if use_deployed_dbus_configs: 250 args.append('--use-deployed-dbus-configs') 251 252 with mock.patch.object(sys, 'argv', args),\ 253 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen,\ 254 mock.patch.object(os, 'fdopen', fd_mock),\ 255 mock.patch.object(os, 'remove') as mock_remove,\ 256 mock.patch.object(tempfile, 'mkstemp', 257 side_effect=[(3, 'out_eve/Release/device_script.sh'),\ 258 (4, 'out_eve/Release/runtime_files.txt')]),\ 259 mock.patch.object(os, 'fchmod'): 260 mock_popen.return_value.returncode = 0 261 262 test_runner.main() 263 self.assertEqual(1, mock_popen.call_count) 264 expected_cmd = [ 265 'vpython3', test_runner.CROS_RUN_TEST_PATH, '--board', 'eve', 266 '--cache-dir', test_runner.DEFAULT_CROS_CACHE, '--remote-cmd', 267 '--cwd', 'out_eve/Release', '--files-from', 268 'out_eve/Release/runtime_files.txt' 269 ] 270 if not stop_ui: 271 expected_cmd.append('--as-chronos') 272 expected_cmd.extend(['--start', '--copy-on-write'] if use_vm else ( 273 ['--device', 'chrome-chromeos8-row29'] 274 if fetch_cros_hostname else ['--device', 'localhost:2222'])) 275 expected_cmd.extend(['--', './device_script.sh']) 276 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 277 278 expected_device_script = dedent("""\ 279 #!/bin/sh 280 export HOME=/usr/local/tmp 281 export TMPDIR=/usr/local/tmp 282 """) 283 284 core_cmd = 'LD_LIBRARY_PATH=./ ./out_eve/Release/base_unittests'\ 285 ' --test-launcher-shard-index=0 --test-launcher-total-shards=1' 286 287 if use_test_sudo_helper: 288 expected_device_script += dedent("""\ 289 TEST_SUDO_HELPER_PATH=$(mktemp) 290 ./test_sudo_helper.py --socket-path=${TEST_SUDO_HELPER_PATH} & 291 TEST_SUDO_HELPER_PID=$! 292 """) 293 core_cmd += ' --test-sudo-helper-socket-path=${TEST_SUDO_HELPER_PATH}' 294 295 if use_deployed_dbus_configs: 296 expected_device_script += dedent("""\ 297 mount --bind ./dbus /opt/google/chrome/dbus 298 kill -s HUP $(pgrep dbus) 299 """) 300 301 if stop_ui: 302 dbus_cmd = 'dbus-send --system --type=method_call'\ 303 ' --dest=org.chromium.PowerManager'\ 304 ' /org/chromium/PowerManager'\ 305 ' org.chromium.PowerManager.HandleUserActivity int32:0' 306 expected_device_script += dedent("""\ 307 stop ui 308 {0} 309 chown -R chronos: ../.. 310 sudo -E -u chronos -- /bin/bash -c \"{1}\" 311 TEST_RETURN_CODE=$? 312 start ui 313 """).format(dbus_cmd, core_cmd) 314 else: 315 expected_device_script += dedent("""\ 316 {0} 317 TEST_RETURN_CODE=$? 318 """).format(core_cmd) 319 320 if use_test_sudo_helper: 321 expected_device_script += dedent("""\ 322 pkill -P $TEST_SUDO_HELPER_PID 323 kill $TEST_SUDO_HELPER_PID 324 unlink ${TEST_SUDO_HELPER_PATH} 325 """) 326 327 if use_deployed_dbus_configs: 328 expected_device_script += dedent("""\ 329 umount /opt/google/chrome/dbus 330 kill -s HUP $(pgrep dbus) 331 """) 332 333 expected_device_script += dedent("""\ 334 exit $TEST_RETURN_CODE 335 """) 336 337 self.assertEqual(2, fd_mock().write.call_count) 338 write_calls = fd_mock().write.call_args_list 339 340 # Split the strings to make failure messages easier to read. 341 # Verify the first write of device script. 342 self.assertListEqual( 343 expected_device_script.split('\n'), 344 str(write_calls[0][0][0]).split('\n')) 345 346 # Verify the 2nd write of runtime files. 347 expected_runtime_files = ['out_eve/Release/device_script.sh'] 348 self.assertListEqual(expected_runtime_files, 349 str(write_calls[1][0][0]).strip().split('\n')) 350 351 mock_remove.assert_called_once_with('out_eve/Release/device_script.sh') 352 353 def test_gtest_with_vpython(self): 354 """Tests building a gtest with --vpython-dir.""" 355 args = mock.MagicMock() 356 args.test_exe = 'base_unittests' 357 args.test_launcher_summary_output = None 358 args.trace_dir = None 359 args.runtime_deps_path = None 360 args.path_to_outdir = self._tmp_dir 361 args.vpython_dir = self._tmp_dir 362 args.logs_dir = self._tmp_dir 363 364 # With vpython_dir initially empty, the test_runner should error out 365 # due to missing vpython binaries. 366 gtest = test_runner.GTestTest(args, None) 367 with self.assertRaises(test_runner.TestFormatError): 368 gtest.build_test_command() 369 370 # Create the two expected tools, and the test should be ready to run. 371 with open(os.path.join(args.vpython_dir, 'vpython3'), 'w'): 372 pass # Just touch the file. 373 os.mkdir(os.path.join(args.vpython_dir, 'bin')) 374 with open(os.path.join(args.vpython_dir, 'bin', 'python3'), 'w'): 375 pass 376 gtest = test_runner.GTestTest(args, None) 377 gtest.build_test_command() 378 379 380class HostCmdTests(TestRunnerTest): 381 382 @parameterized.expand([ 383 [False, False], 384 [False, True], 385 [True, False], 386 [True, True], 387 ]) 388 def test_host_cmd(self, deploy_chrome, strip_chrome): 389 args = [ 390 'script_name', 391 'host-cmd', 392 '--board=eve', 393 '--flash', 394 '--path-to-outdir=out/Release', 395 '--device=localhost:2222', 396 ] 397 if deploy_chrome: 398 args += ['--deploy-chrome'] 399 if strip_chrome: 400 args += ['--strip-chrome'] 401 args += [ 402 '--', 403 'fake_cmd', 404 ] 405 with mock.patch.object(sys, 'argv', args),\ 406 mock.patch.object(test_runner.subprocess, 'Popen') as mock_popen: 407 mock_popen.return_value.returncode = 0 408 409 test_runner.main() 410 expected_cmd = [ 411 test_runner.CROS_RUN_TEST_PATH, 412 '--board', 413 'eve', 414 '--cache-dir', 415 test_runner.DEFAULT_CROS_CACHE, 416 '--flash', 417 '--device', 418 'localhost:2222', 419 '--host-cmd', 420 ] 421 if deploy_chrome: 422 expected_cmd += [ 423 '--mount', 424 '--deploy', 425 '--build-dir', 426 os.path.join(test_runner.CHROMIUM_SRC_PATH, 'out/Release'), 427 ] 428 if not strip_chrome: 429 expected_cmd += ['--nostrip'] 430 431 expected_cmd += [ 432 '--', 433 'fake_cmd', 434 ] 435 436 self.safeAssertItemsEqual(expected_cmd, mock_popen.call_args[0][0]) 437 438 439if __name__ == '__main__': 440 unittest.main() 441