xref: /aosp_15_r20/external/angle/build/chromeos/test_runner_test.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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