xref: /aosp_15_r20/external/cronet/testing/scripts/run_variations_smoke_tests.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env vpython3
2# Copyright 2021 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"""A smoke test to verify Chrome doesn't crash and basic rendering is functional
6when parsing a newly given variations seed.
7"""
8
9import argparse
10import http
11import json
12import logging
13import os
14import shutil
15from struct import pack
16import subprocess
17import sys
18import tempfile
19import time
20from functools import partial
21from http.server import SimpleHTTPRequestHandler
22from pkg_resources import packaging
23from threading import Thread
24
25_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
26_CHROMIUM_SRC_DIR = os.path.realpath(os.path.join(_THIS_DIR, '..', '..'))
27# Needed for skia_gold_common import.
28sys.path.append(os.path.join(_CHROMIUM_SRC_DIR, 'build'))
29# Needed to import common without pylint errors.
30sys.path.append(os.path.join(_CHROMIUM_SRC_DIR, 'testing'))
31
32import pkg_resources
33from skia_gold_common.skia_gold_properties import SkiaGoldProperties
34from skia_gold_infra import finch_skia_gold_utils
35
36import variations_seed_access_helper as seed_helper
37
38_VARIATIONS_TEST_DATA = 'variations_smoke_test_data'
39_VERSION_STRING = 'PRODUCT_VERSION'
40_FLAG_RELEASE_VERSION = packaging.version.parse('105.0.5176.3')
41
42
43from scripts import common
44
45from selenium import webdriver
46from selenium.webdriver import ChromeOptions
47from selenium.common.exceptions import NoSuchElementException
48from selenium.common.exceptions import WebDriverException
49
50# Constants for the waiting for seed from finch server
51_MAX_ATTEMPTS = 2
52_WAIT_TIMEOUT_IN_SEC = 0.5
53
54# Test cases to verify web elements can be rendered correctly.
55_TEST_CASES = [
56    {
57        # data:text/html,<h1 id="success">Success</h1>
58        'url': 'data:text/html,%3Ch1%20id%3D%22success%22%3ESuccess%3C%2Fh1%3E',
59        'expected_id': 'success',
60        'expected_text': 'Success',
61    },
62    {
63        'url': 'http://localhost:8000',
64        'expected_id': 'sites-chrome-userheader-title',
65        'expected_text': 'The Chromium Projects',
66        'skia_gold_image': 'finch_smoke_render_chromium_org_html',
67    },
68]
69
70
71def _get_httpd():
72  """Returns a HTTPServer instance."""
73  hostname = "localhost"
74  port = 8000
75  directory = os.path.join(_THIS_DIR, _VARIATIONS_TEST_DATA, "http_server")
76  httpd = None
77  handler = partial(SimpleHTTPRequestHandler, directory=directory)
78  httpd = http.server.HTTPServer((hostname, port), handler)
79  httpd.timeout = 0.5
80  httpd.allow_reuse_address = True
81  return httpd
82
83
84def _get_platform():
85  """Returns the host platform.
86
87  Returns:
88    One of 'linux', 'win' and 'mac'.
89  """
90  if sys.platform == 'win32' or sys.platform == 'cygwin':
91    return 'win'
92  if sys.platform.startswith('linux'):
93    return 'linux'
94  if sys.platform == 'darwin':
95    return 'mac'
96
97  raise RuntimeError(
98    'Unsupported platform: %s. Only Linux (linux*) and Mac (darwin) and '
99    'Windows (win32 or cygwin) are supported' % sys.platform)
100
101
102def _find_chrome_binary(): #pylint: disable=inconsistent-return-statements
103  """Finds and returns the relative path to the Chrome binary.
104
105  This function assumes that the CWD is the build directory.
106
107  Returns:
108    A relative path to the Chrome binary.
109  """
110  platform = _get_platform()
111  if platform == 'linux':
112    return os.path.join('.', 'chrome')
113  if platform == 'mac':
114    chrome_name = 'Google Chrome'
115    return os.path.join('.', chrome_name + '.app', 'Contents', 'MacOS',
116                            chrome_name)
117  if platform == 'win':
118    return os.path.join('.', 'chrome.exe')
119
120
121def _confirm_new_seed_downloaded(user_data_dir,
122                                 path_chromedriver,
123                                 chrome_options,
124                                 old_seed=None,
125                                 old_signature=None):
126  """Confirms the new seed to be downloaded from finch server.
127
128  Note that Local State does not dump until Chrome has exited.
129
130  Args:
131    user_data_dir: the use directory used to store fetched seed.
132    path_chromedriver: the path of chromedriver binary.
133    chrome_options: the chrome option used to launch Chrome.
134    old_seed: the old seed serves as a baseline. New seed should be different.
135    old_signature: the old signature serves as a baseline. New signature should
136        be different.
137
138  Returns:
139    True if the new seed is downloaded, otherwise False.
140  """
141  driver = None
142  attempt = 0
143  wait_timeout_in_sec = _WAIT_TIMEOUT_IN_SEC
144  while attempt < _MAX_ATTEMPTS:
145    # Starts Chrome to allow it to download a seed or a seed delta.
146    driver = webdriver.Chrome(path_chromedriver, chrome_options=chrome_options)
147    time.sleep(5)
148    # Exits Chrome so that Local State could be serialized to disk.
149    driver.quit()
150    # Checks the seed and signature.
151    current_seed, current_signature = seed_helper.get_current_seed(
152        user_data_dir)
153    if current_seed != old_seed and current_signature != old_signature:
154      return True
155    attempt += 1
156    time.sleep(wait_timeout_in_sec)
157    wait_timeout_in_sec *= 2
158  return False
159
160def _check_chrome_version():
161  path_chrome = os.path.abspath(_find_chrome_binary())
162  OS = _get_platform()
163  #(crbug/158372)
164  if OS == 'win':
165    cmd = ('powershell -command "&{(Get-Item'
166            '\''+ path_chrome + '\').VersionInfo.ProductVersion}"')
167    version = subprocess.run(cmd, check=True,
168                          capture_output=True).stdout.decode('utf-8')
169  else:
170    cmd = [path_chrome, '--version']
171    version = subprocess.run(cmd, check=True,
172                          capture_output=True).stdout.decode('utf-8')
173    #only return the version number portion
174    version = version.strip().split(" ")[-1]
175  return packaging.version.parse(version)
176
177def _inject_seed(user_data_dir, path_chromedriver, chrome_options):
178  # Verify a production version of variations seed was fetched successfully.
179  if not _confirm_new_seed_downloaded(user_data_dir, path_chromedriver,
180                                      chrome_options):
181    logging.error('Failed to fetch variations seed on initial run')
182    # For MacOS, there is sometime the test fail to download seed on initial
183    # run (crbug/1312393)
184    if _get_platform() != 'mac':
185      return 1
186
187  # Inject the test seed.
188  # This is a path as fallback when |seed_helper.load_test_seed_from_file()|
189  # can't find one under src root.
190  hardcoded_seed_path = os.path.join(_THIS_DIR, _VARIATIONS_TEST_DATA,
191                          'variations_seed_beta_%s.json' % _get_platform())
192  seed, signature = seed_helper.load_test_seed_from_file(hardcoded_seed_path)
193  if not seed or not signature:
194    logging.error(
195        'Ill-formed test seed json file: "%s" and "%s" are required',
196        seed_helper.LOCAL_STATE_SEED_NAME,
197        seed_helper.LOCAL_STATE_SEED_SIGNATURE_NAME)
198    return 1
199
200  if not seed_helper.inject_test_seed(seed, signature, user_data_dir):
201    logging.error('Failed to inject the test seed')
202    return 1
203  return 0
204
205def _run_tests(work_dir, skia_util, *args):
206  """Runs the smoke tests.
207
208  Args:
209    work_dir: A working directory to store screenshots and other artifacts.
210    skia_util: A FinchSkiaGoldUtil used to do pixel test.
211    args: Arguments to be passed to the chrome binary.
212
213  Returns:
214    0 if tests passed, otherwise 1.
215  """
216  skia_gold_session = skia_util.SkiaGoldSession
217  path_chrome = _find_chrome_binary()
218  path_chromedriver = os.path.join('.', 'chromedriver')
219  hardcoded_seed_path = os.path.join(_THIS_DIR, _VARIATIONS_TEST_DATA,
220                             'variations_seed_beta_%s.json' % _get_platform())
221  path_seed = seed_helper.get_test_seed_file_path(hardcoded_seed_path)
222
223  user_data_dir = tempfile.mkdtemp()
224  crash_dump_dir = tempfile.mkdtemp()
225  _, log_file = tempfile.mkstemp()
226
227  # Crashpad is a separate process and its dump locations is set via env
228  # variable.
229  os.environ['BREAKPAD_DUMP_LOCATION'] = crash_dump_dir
230
231  chrome_options = ChromeOptions()
232  chrome_options.binary_location = path_chrome
233  chrome_options.add_argument('user-data-dir=' + user_data_dir)
234  chrome_options.add_argument('log-file=' + log_file)
235  chrome_options.add_argument('variations-test-seed-path=' + path_seed)
236  #TODO(crbug/1342057): Remove this line.
237  chrome_options.add_argument("disable-field-trial-config")
238
239  for arg in args:
240    chrome_options.add_argument(arg)
241
242  # By default, ChromeDriver passes in --disable-backgroud-networking, however,
243  # fetching variations seeds requires network connection, so override it.
244  chrome_options.add_experimental_option('excludeSwitches',
245                                         ['disable-background-networking'])
246
247  driver = None
248  try:
249    chrome_verison = _check_chrome_version()
250    # If --variations-test-seed-path flag was not implemented in this version
251    if chrome_verison <= _FLAG_RELEASE_VERSION:
252      if _inject_seed(user_data_dir, path_chromedriver, chrome_options) == 1:
253        return 1
254
255    # Starts Chrome with the test seed injected.
256    driver = webdriver.Chrome(path_chromedriver, chrome_options=chrome_options)
257
258    # Run test cases: visit urls and verify certain web elements are rendered
259    # correctly.
260    for t in _TEST_CASES:
261      driver.get(t['url'])
262      driver.set_window_size(1280, 1024)
263      element = driver.find_element_by_id(t['expected_id'])
264      if not element.is_displayed() or t['expected_text'] != element.text:
265        logging.error(
266            'Test failed because element: "%s" is not visibly found after '
267            'visiting url: "%s"', t['expected_text'], t['url'])
268        return 1
269      if 'skia_gold_image' in t:
270        image_name = t['skia_gold_image']
271        sc_file = os.path.join(work_dir, image_name + '.png')
272        driver.find_element_by_id('body').screenshot(sc_file)
273        force_dryrun = False
274        if skia_util.IsTryjobRun and skia_util.IsRetryWithoutPatch:
275          force_dryrun = True
276        status, error = skia_gold_session.RunComparison(
277            name=image_name, png_file=sc_file, force_dryrun=force_dryrun)
278        if status:
279          finch_skia_gold_utils.log_skia_gold_status_code(
280              skia_gold_session, image_name, status, error)
281          return status
282
283    driver.quit()
284
285  except NoSuchElementException as e:
286    logging.error('Failed to find the expected web element.\n%s', e)
287    return 1
288  except WebDriverException as e:
289    if os.listdir(crash_dump_dir):
290      logging.error('Chrome crashed and exited abnormally.\n%s', e)
291    else:
292      logging.error('Uncaught WebDriver exception thrown.\n%s', e)
293    return 1
294  finally:
295    shutil.rmtree(user_data_dir, ignore_errors=True)
296    shutil.rmtree(crash_dump_dir, ignore_errors=True)
297
298    # Print logs for debugging purpose.
299    with open(log_file) as f:
300      logging.info('Chrome logs for debugging:\n%s', f.read())
301
302    shutil.rmtree(log_file, ignore_errors=True)
303    if driver:
304      driver.quit()
305
306  return 0
307
308
309def _start_local_http_server():
310  """Starts a local http server.
311
312  Returns:
313    A local http.server.HTTPServer.
314  """
315  httpd = _get_httpd()
316  thread = None
317  address = "http://{}:{}".format(httpd.server_name, httpd.server_port)
318  logging.info("%s is used as local http server.", address)
319  thread = Thread(target=httpd.serve_forever)
320  thread.setDaemon(True)
321  thread.start()
322  return httpd
323
324
325def main_run(args):
326  """Runs the variations smoke tests."""
327  logging.basicConfig(level=logging.INFO)
328  parser = argparse.ArgumentParser()
329  parser.add_argument('--isolated-script-test-output', type=str)
330  parser.add_argument('--isolated-script-test-filter', type=str)
331  SkiaGoldProperties.AddCommandLineArguments(parser)
332  args, rest = parser.parse_known_args()
333
334  temp_dir = tempfile.mkdtemp()
335  httpd = _start_local_http_server()
336  skia_util = finch_skia_gold_utils.FinchSkiaGoldUtil(
337      temp_dir, args)
338  try:
339    rc = _run_tests(temp_dir, skia_util, *rest)
340    if args.isolated_script_test_output:
341      with open(args.isolated_script_test_output, 'w') as f:
342        common.record_local_script_results('run_variations_smoke_tests', f, [],
343                                           rc == 0)
344  finally:
345    httpd.shutdown()
346    shutil.rmtree(temp_dir, ignore_errors=True)
347
348  return rc
349
350
351def main_compile_targets(args):
352  """Returns the list of targets to compile in order to run this test."""
353  json.dump(['chrome', 'chromedriver'], args.output)
354  return 0
355
356
357if __name__ == '__main__':
358  if 'compile_targets' in sys.argv:
359    funcs = {
360        'run': None,
361        'compile_targets': main_compile_targets,
362    }
363    sys.exit(common.run_script(sys.argv[1:], funcs))
364  sys.exit(main_run(sys.argv[1:]))
365