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