Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # |
| 3 | # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Tool to validate code in prod branch before pushing to lab. |
| 8 | |
| 9 | The script runs push_to_prod suite to verify code in prod branch is ready to be |
| 10 | pushed. Link to design document: |
| 11 | https://docs.google.com/a/google.com/document/d/1JMz0xS3fZRSHMpFkkKAL_rxsdbNZomhHbC3B8L71uuI/edit |
| 12 | |
| 13 | To verify if prod branch can be pushed to lab, run following command in |
| 14 | chromeos-autotest.cbf server: |
Michael Liang | 52d9f1f | 2014-06-17 15:01:24 -0700 | [diff] [blame] | 15 | /usr/local/autotest/site_utils/test_push.py -e someone@company.com |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 16 | |
Dan Shi | 8df9c00 | 2016-03-08 15:37:39 -0800 | [diff] [blame] | 17 | The script uses latest gandof canary build as test build by default. |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 18 | |
| 19 | """ |
| 20 | |
| 21 | import argparse |
| 22 | import getpass |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 23 | import multiprocessing |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 24 | import os |
| 25 | import re |
| 26 | import subprocess |
| 27 | import sys |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 28 | import time |
| 29 | import traceback |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 30 | import urllib2 |
| 31 | |
| 32 | import common |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 33 | try: |
| 34 | from autotest_lib.frontend import setup_django_environment |
| 35 | from autotest_lib.frontend.afe import models |
| 36 | except ImportError: |
| 37 | # Unittest may not have Django database configured and will fail to import. |
| 38 | pass |
Dan Shi | 5fa602c | 2015-03-26 17:54:13 -0700 | [diff] [blame] | 39 | from autotest_lib.client.common_lib import global_config |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 40 | from autotest_lib.server import site_utils |
Dan Shi | 47d3288 | 2014-12-22 16:25:05 -0800 | [diff] [blame] | 41 | from autotest_lib.server.cros import provision |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 42 | from autotest_lib.server.cros.dynamic_suite import frontend_wrappers |
Kevin Cheng | 6e4c264 | 2015-12-11 09:45:57 -0800 | [diff] [blame] | 43 | from autotest_lib.server.hosts import factory |
Dan Shi | 5fa602c | 2015-03-26 17:54:13 -0700 | [diff] [blame] | 44 | from autotest_lib.site_utils import gmail_lib |
Dan Shi | 47d3288 | 2014-12-22 16:25:05 -0800 | [diff] [blame] | 45 | from autotest_lib.site_utils.suite_scheduler import constants |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 46 | |
| 47 | CONFIG = global_config.global_config |
| 48 | |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 49 | AFE = frontend_wrappers.RetryingAFE(timeout_min=0.5, delay_sec=2) |
| 50 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 51 | MAIL_FROM = 'chromeos-test@google.com' |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 52 | DEVSERVERS = CONFIG.get_config_value('CROS', 'dev_server', type=list, |
| 53 | default=[]) |
| 54 | BUILD_REGEX = '^R[\d]+-[\d]+\.[\d]+\.[\d]+$' |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 55 | RUN_SUITE_COMMAND = 'run_suite.py' |
| 56 | PUSH_TO_PROD_SUITE = 'push_to_prod' |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 57 | DUMMY_SUITE = 'dummy' |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 58 | AU_SUITE = 'paygen_au_canary' |
| 59 | |
Fang Deng | 6dddf60 | 2014-04-17 17:01:47 -0700 | [diff] [blame] | 60 | SUITE_JOB_START_INFO_REGEX = ('^.*Created suite job:.*' |
| 61 | 'tab_id=view_job&object_id=(\d+)$') |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 62 | |
| 63 | # Dictionary of test results keyed by test name regular expression. |
| 64 | EXPECTED_TEST_RESULTS = {'^SERVER_JOB$': 'GOOD', |
| 65 | # This is related to dummy_Fail/control.dependency. |
| 66 | 'dummy_Fail.dependency$': 'TEST_NA', |
Dan Shi | dc9eb17 | 2014-12-09 16:05:02 -0800 | [diff] [blame] | 67 | 'login_LoginSuccess.*': 'GOOD', |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 68 | 'platform_InstallTestImage_SERVER_JOB$': 'GOOD', |
Dan Shi | 47d3288 | 2014-12-22 16:25:05 -0800 | [diff] [blame] | 69 | 'provision_AutoUpdate.double': 'GOOD', |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 70 | 'dummy_Pass.*': 'GOOD', |
| 71 | 'dummy_Fail.Fail$': 'FAIL', |
| 72 | 'dummy_Fail.RetryFail$': 'FAIL', |
| 73 | 'dummy_Fail.RetrySuccess': 'GOOD', |
| 74 | 'dummy_Fail.Error$': 'ERROR', |
| 75 | 'dummy_Fail.Warn$': 'WARN', |
| 76 | 'dummy_Fail.NAError$': 'TEST_NA', |
| 77 | 'dummy_Fail.Crash$': 'GOOD', |
| 78 | } |
| 79 | |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 80 | EXPECTED_TEST_RESULTS_DUMMY = {'^SERVER_JOB$': 'GOOD', |
| 81 | 'dummy_Pass.*': 'GOOD', |
| 82 | 'dummy_Fail.Fail': 'FAIL', |
| 83 | 'dummy_Fail.Warn': 'WARN', |
| 84 | 'dummy_Fail.Crash': 'GOOD', |
| 85 | 'dummy_Fail.Error': 'ERROR', |
| 86 | 'dummy_Fail.NAError': 'TEST_NA',} |
| 87 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 88 | EXPECTED_TEST_RESULTS_AU = {'SERVER_JOB$': 'GOOD', |
Dan Shi | 15e6d72 | 2015-09-29 14:20:47 -0700 | [diff] [blame] | 89 | 'autoupdate_EndToEndTest.paygen_au_canary_delta.*': 'GOOD', |
| 90 | 'autoupdate_EndToEndTest.paygen_au_canary_full.*': 'GOOD', |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 91 | } |
| 92 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 93 | URL_HOST = CONFIG.get_config_value('SERVER', 'hostname', type=str) |
| 94 | URL_PATTERN = CONFIG.get_config_value('CROS', 'log_url_pattern', type=str) |
| 95 | |
Dan Shi | dc9eb17 | 2014-12-09 16:05:02 -0800 | [diff] [blame] | 96 | # Some test could be missing from the test results for various reasons. Add |
| 97 | # such test in this list and explain the reason. |
| 98 | IGNORE_MISSING_TESTS = [ |
| 99 | # For latest build, npo_test_delta does not exist. |
| 100 | 'autoupdate_EndToEndTest.npo_test_delta.*', |
| 101 | # For trybot build, nmo_test_delta does not exist. |
| 102 | 'autoupdate_EndToEndTest.nmo_test_delta.*', |
| 103 | # Older build does not have login_LoginSuccess test in push_to_prod suite. |
| 104 | # TODO(dshi): Remove following lines after R41 is stable. |
| 105 | 'login_LoginSuccess'] |
| 106 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 107 | # Save all run_suite command output. |
| 108 | run_suite_output = [] |
| 109 | |
| 110 | class TestPushException(Exception): |
| 111 | """Exception to be raised when the test to push to prod failed.""" |
| 112 | pass |
| 113 | |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 114 | |
Kevin Cheng | 6e4c264 | 2015-12-11 09:45:57 -0800 | [diff] [blame] | 115 | def powerwash_dut(hostname): |
| 116 | """Powerwash the dut with the given hostname. |
| 117 | |
| 118 | @param hostname: hostname of the dut. |
| 119 | """ |
| 120 | host = factory.create_host(hostname) |
| 121 | host.run('echo "fast safe" > ' |
| 122 | '/mnt/stateful_partition/factory_install_reset') |
| 123 | host.run('reboot') |
| 124 | host.close() |
| 125 | |
| 126 | |
Dan Shi | 8df9c00 | 2016-03-08 15:37:39 -0800 | [diff] [blame] | 127 | def get_default_build(devserver=None, board='gandof'): |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 128 | """Get the default build to be used for test. |
| 129 | |
| 130 | @param devserver: devserver used to look for latest staged build. If value |
| 131 | is None, all devservers in config will be tried. |
Dan Shi | 8df9c00 | 2016-03-08 15:37:39 -0800 | [diff] [blame] | 132 | @param board: Name of board to be tested, default is gandof. |
| 133 | @return: Build to be tested, e.g., gandof-release/R36-5881.0.0 |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 134 | """ |
| 135 | LATEST_BUILD_URL_PATTERN = '%s/latestbuild?target=%s-release' |
| 136 | build = None |
| 137 | if not devserver: |
| 138 | for server in DEVSERVERS: |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 139 | url = LATEST_BUILD_URL_PATTERN % (server, board) |
| 140 | build = urllib2.urlopen(url).read() |
| 141 | if build and re.match(BUILD_REGEX, build): |
| 142 | return '%s-release/%s' % (board, build) |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 143 | |
| 144 | # If no devserver has any build staged for the given board, use the stable |
| 145 | # build in config. |
| 146 | build = CONFIG.get_config_value('CROS', 'stable_cros_version') |
| 147 | return '%s-release/%s' % (board, build) |
| 148 | |
| 149 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 150 | def parse_arguments(): |
| 151 | """Parse arguments for test_push tool. |
| 152 | |
| 153 | @return: Parsed arguments. |
| 154 | |
| 155 | """ |
| 156 | parser = argparse.ArgumentParser() |
Dan Shi | 8df9c00 | 2016-03-08 15:37:39 -0800 | [diff] [blame] | 157 | parser.add_argument('-b', '--board', dest='board', default='gandof', |
| 158 | help='Default is gandof.') |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 159 | parser.add_argument('-sb', '--shard_board', dest='shard_board', |
| 160 | default='quawks', |
| 161 | help='Default is quawks.') |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 162 | parser.add_argument('-i', '--build', dest='build', default=None, |
| 163 | help='Default is the latest canary build of given ' |
| 164 | 'board. Must be a canary build, otherwise AU test ' |
Don Garrett | 10c7361 | 2016-06-01 12:42:18 -0700 | [diff] [blame] | 165 | 'will fail. (ex: gandolf-release/R53-8397.0.0)') |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 166 | parser.add_argument('-si', '--shard_build', dest='shard_build', default=None, |
| 167 | help='Default is the latest canary build of given ' |
| 168 | 'board. Must be a canary build, otherwise AU test ' |
| 169 | 'will fail.') |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 170 | parser.add_argument('-p', '--pool', dest='pool', default='bvt') |
| 171 | parser.add_argument('-u', '--num', dest='num', type=int, default=3, |
| 172 | help='Run on at most NUM machines.') |
| 173 | parser.add_argument('-f', '--file_bugs', dest='file_bugs', default='True', |
| 174 | help='File bugs on test failures. Must pass "True" or ' |
| 175 | '"False" if used.') |
| 176 | parser.add_argument('-e', '--email', dest='email', default=None, |
| 177 | help='Email address for the notification to be sent to ' |
| 178 | 'after the script finished running.') |
| 179 | parser.add_argument('-d', '--devserver', dest='devserver', |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 180 | default=None, |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 181 | help='devserver to find what\'s the latest build.') |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 182 | parser.add_argument('-t', '--timeout_min', dest='timeout_min', type=int, |
| 183 | default=24, |
| 184 | help='Time in mins to wait before abort the jobs we ' |
| 185 | 'are waiting on. Only for the asynchronous suites ' |
| 186 | 'triggered by create_and_return flag.') |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 187 | |
| 188 | arguments = parser.parse_args(sys.argv[1:]) |
| 189 | |
| 190 | # Get latest canary build as default build. |
| 191 | if not arguments.build: |
Dan Shi | 5ba5d2e | 2014-05-09 13:47:00 -0700 | [diff] [blame] | 192 | arguments.build = get_default_build(arguments.devserver, |
| 193 | arguments.board) |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 194 | if not arguments.shard_build: |
| 195 | arguments.shard_build = get_default_build(arguments.devserver, |
| 196 | arguments.shard_board) |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 197 | |
| 198 | return arguments |
| 199 | |
| 200 | |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 201 | def do_run_suite(suite_name, arguments, use_shard=False, |
| 202 | create_and_return=False): |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 203 | """Call run_suite to run a suite job, and return the suite job id. |
| 204 | |
| 205 | The script waits the suite job to finish before returning the suite job id. |
| 206 | Also it will echo the run_suite output to stdout. |
| 207 | |
| 208 | @param suite_name: Name of a suite, e.g., dummy. |
| 209 | @param arguments: Arguments for run_suite command. |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 210 | @param use_shard: If true, suite is scheduled for shard board. |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 211 | @param create_and_return: If True, run_suite just creates the suite, print |
| 212 | the job id, then finish immediately. |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 213 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 214 | @return: Suite job ID. |
| 215 | |
| 216 | """ |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 217 | if not use_shard: |
| 218 | board = arguments.board |
| 219 | build = arguments.build |
| 220 | else: |
| 221 | board = arguments.shard_board |
| 222 | build = arguments.shard_build |
| 223 | |
Dan Shi | 47d3288 | 2014-12-22 16:25:05 -0800 | [diff] [blame] | 224 | # Remove cros-version label to force provision. |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 225 | hosts = AFE.get_hosts(label=constants.Labels.BOARD_PREFIX+board) |
Dan Shi | 47d3288 | 2014-12-22 16:25:05 -0800 | [diff] [blame] | 226 | for host in hosts: |
| 227 | for label in [l for l in host.labels |
| 228 | if l.startswith(provision.CROS_VERSION_PREFIX)]: |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 229 | AFE.run('host_remove_labels', id=host.id, labels=[label]) |
Dan Shi | 47d3288 | 2014-12-22 16:25:05 -0800 | [diff] [blame] | 230 | |
Kevin Cheng | 6e4c264 | 2015-12-11 09:45:57 -0800 | [diff] [blame] | 231 | if use_shard and not create_and_return: |
| 232 | # Let's verify the repair flow and powerwash the duts. We can |
| 233 | # assume they're all cros hosts (valid assumption?) so powerwash |
| 234 | # will work. |
| 235 | try: |
| 236 | powerwash_dut(host.hostname) |
| 237 | except Exception as e: |
| 238 | raise TestPushException('Failed to powerwash dut %s. Make ' |
| 239 | 'sure the dut is working first. ' |
| 240 | 'Error: %s' % (host.hostname, e)) |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 241 | AFE.reverify_hosts(hostnames=[host.hostname]) |
Kevin Cheng | 6e4c264 | 2015-12-11 09:45:57 -0800 | [diff] [blame] | 242 | |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 243 | current_dir = os.path.dirname(os.path.realpath(__file__)) |
| 244 | cmd = [os.path.join(current_dir, RUN_SUITE_COMMAND), |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 245 | '-s', suite_name, |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 246 | '-b', board, |
| 247 | '-i', build, |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 248 | '-p', arguments.pool, |
| 249 | '-u', str(arguments.num), |
| 250 | '-f', arguments.file_bugs] |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 251 | if create_and_return: |
| 252 | cmd += ['-c'] |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 253 | |
| 254 | suite_job_id = None |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 255 | |
| 256 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
| 257 | stderr=subprocess.STDOUT) |
| 258 | |
| 259 | while True: |
| 260 | line = proc.stdout.readline() |
| 261 | |
| 262 | # Break when run_suite process completed. |
| 263 | if not line and proc.poll() != None: |
| 264 | break |
| 265 | print line.rstrip() |
| 266 | run_suite_output.append(line.rstrip()) |
| 267 | |
| 268 | if not suite_job_id: |
| 269 | m = re.match(SUITE_JOB_START_INFO_REGEX, line) |
| 270 | if m and m.group(1): |
| 271 | suite_job_id = int(m.group(1)) |
| 272 | |
| 273 | if not suite_job_id: |
| 274 | raise TestPushException('Failed to retrieve suite job ID.') |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 275 | |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 276 | # If create_and_return specified, wait for the suite to finish. |
| 277 | if create_and_return: |
| 278 | end = time.time() + arguments.timeout_min * 60 |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 279 | while not AFE.get_jobs(id=suite_job_id, finished=True): |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 280 | if time.time() < end: |
| 281 | time.sleep(10) |
| 282 | else: |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 283 | AFE.run('abort_host_queue_entries', job=suite_job_id) |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 284 | raise TestPushException( |
| 285 | 'Asynchronous suite triggered by create_and_return ' |
| 286 | 'flag has timed out after %d mins. Aborting it.' % |
| 287 | arguments.timeout_min) |
| 288 | |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 289 | print 'Suite job %s is completed.' % suite_job_id |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 290 | return suite_job_id |
| 291 | |
| 292 | |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 293 | def check_dut_image(build, suite_job_id): |
| 294 | """Confirm all DUTs used for the suite are imaged to expected build. |
| 295 | |
| 296 | @param build: Expected build to be imaged. |
| 297 | @param suite_job_id: job ID of the suite job. |
| 298 | @raise TestPushException: If a DUT does not have expected build imaged. |
| 299 | """ |
| 300 | print 'Checking image installed in DUTs...' |
| 301 | job_ids = [job.id for job in |
| 302 | models.Job.objects.filter(parent_job_id=suite_job_id)] |
| 303 | hqes = [models.HostQueueEntry.objects.filter(job_id=job_id)[0] |
| 304 | for job_id in job_ids] |
| 305 | hostnames = set([hqe.host.hostname for hqe in hqes]) |
| 306 | for hostname in hostnames: |
Dan Shi | efd403e | 2016-02-03 11:37:02 -0800 | [diff] [blame] | 307 | found_build = site_utils.get_build_from_afe(hostname, AFE) |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 308 | if found_build != build: |
| 309 | raise TestPushException('DUT is not imaged properly. Host %s has ' |
| 310 | 'build %s, while build %s is expected.' % |
| 311 | (hostname, found_build, build)) |
| 312 | |
| 313 | |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 314 | def test_suite(suite_name, expected_results, arguments, use_shard=False, |
| 315 | create_and_return=False): |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 316 | """Call run_suite to start a suite job and verify results. |
| 317 | |
| 318 | @param suite_name: Name of a suite, e.g., dummy |
| 319 | @param expected_results: A dictionary of test name to test result. |
| 320 | @param arguments: Arguments for run_suite command. |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 321 | @param use_shard: If true, suite is scheduled for shard board. |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 322 | @param create_and_return: If True, run_suite just creates the suite, print |
| 323 | the job id, then finish immediately. |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 324 | """ |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 325 | suite_job_id = do_run_suite(suite_name, arguments, use_shard, |
| 326 | create_and_return) |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 327 | |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 328 | # Confirm all DUTs used for the suite are imaged to expected build. |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 329 | # hqe.host_id for jobs running in shard is not synced back to master db, |
| 330 | # therefore, skip verifying dut build for jobs running in shard. |
| 331 | if suite_name != AU_SUITE and not use_shard: |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 332 | check_dut_image(arguments.build, suite_job_id) |
| 333 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 334 | # Find all tests and their status |
Dan Shi | a8da760 | 2014-05-09 15:18:15 -0700 | [diff] [blame] | 335 | print 'Comparing test results...' |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 336 | TKO = frontend_wrappers.RetryingTKO(timeout_min=0.1, delay_sec=10) |
| 337 | test_views = site_utils.get_test_views_from_tko(suite_job_id, TKO) |
| 338 | |
| 339 | mismatch_errors = [] |
| 340 | extra_test_errors = [] |
| 341 | |
| 342 | found_keys = set() |
| 343 | for test_name,test_status in test_views.items(): |
| 344 | print "%s%s" % (test_name.ljust(30), test_status) |
| 345 | test_found = False |
| 346 | for key,val in expected_results.items(): |
| 347 | if re.search(key, test_name): |
| 348 | test_found = True |
| 349 | found_keys.add(key) |
| 350 | # TODO(dshi): result for this test is ignored until servo is |
| 351 | # added to a host accessible by cbf server (crbug.com/277109). |
| 352 | if key == 'platform_InstallTestImage_SERVER_JOB$': |
| 353 | continue |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 354 | if val != test_status: |
| 355 | error = ('%s Expected: [%s], Actual: [%s]' % |
| 356 | (test_name, val, test_status)) |
| 357 | mismatch_errors.append(error) |
| 358 | if not test_found: |
| 359 | extra_test_errors.append(test_name) |
| 360 | |
| 361 | missing_test_errors = set(expected_results.keys()) - found_keys |
Dan Shi | dc9eb17 | 2014-12-09 16:05:02 -0800 | [diff] [blame] | 362 | for exception in IGNORE_MISSING_TESTS: |
| 363 | try: |
| 364 | missing_test_errors.remove(exception) |
| 365 | except KeyError: |
| 366 | pass |
| 367 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 368 | summary = [] |
| 369 | if mismatch_errors: |
| 370 | summary.append(('Results of %d test(s) do not match expected ' |
| 371 | 'values:') % len(mismatch_errors)) |
| 372 | summary.extend(mismatch_errors) |
| 373 | summary.append('\n') |
| 374 | |
| 375 | if extra_test_errors: |
| 376 | summary.append('%d test(s) are not expected to be run:' % |
| 377 | len(extra_test_errors)) |
| 378 | summary.extend(extra_test_errors) |
| 379 | summary.append('\n') |
| 380 | |
| 381 | if missing_test_errors: |
| 382 | summary.append('%d test(s) are missing from the results:' % |
| 383 | len(missing_test_errors)) |
| 384 | summary.extend(missing_test_errors) |
| 385 | summary.append('\n') |
| 386 | |
| 387 | # Test link to log can be loaded. |
| 388 | job_name = '%s-%s' % (suite_job_id, getpass.getuser()) |
| 389 | log_link = URL_PATTERN % (URL_HOST, job_name) |
| 390 | try: |
| 391 | urllib2.urlopen(log_link).read() |
| 392 | except urllib2.URLError: |
| 393 | summary.append('Failed to load page for link to log: %s.' % log_link) |
| 394 | |
| 395 | if summary: |
| 396 | raise TestPushException('\n'.join(summary)) |
| 397 | |
| 398 | |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 399 | def test_suite_wrapper(queue, suite_name, expected_results, arguments, |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 400 | use_shard=False, create_and_return=False): |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 401 | """Wrapper to call test_suite. Handle exception and pipe it to parent |
| 402 | process. |
| 403 | |
| 404 | @param queue: Queue to save exception to be accessed by parent process. |
| 405 | @param suite_name: Name of a suite, e.g., dummy |
| 406 | @param expected_results: A dictionary of test name to test result. |
| 407 | @param arguments: Arguments for run_suite command. |
| 408 | @param use_shard: If true, suite is scheduled for shard board. |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 409 | @param create_and_return: If True, run_suite just creates the suite, print |
| 410 | the job id, then finish immediately. |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 411 | """ |
| 412 | try: |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 413 | test_suite(suite_name, expected_results, arguments, use_shard, |
| 414 | create_and_return) |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 415 | except: |
| 416 | # Store the whole exc_info leads to a PicklingError. |
| 417 | except_type, except_value, tb = sys.exc_info() |
| 418 | queue.put((except_type, except_value, traceback.extract_tb(tb))) |
| 419 | |
| 420 | |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 421 | def check_queue(queue): |
| 422 | """Check the queue for any exception being raised. |
| 423 | |
| 424 | @param queue: Queue used to store exception for parent process to access. |
| 425 | @raise: Any exception found in the queue. |
| 426 | """ |
| 427 | if queue.empty(): |
| 428 | return |
| 429 | exc_info = queue.get() |
| 430 | # Raise the exception with original backtrace. |
| 431 | print 'Original stack trace of the exception:\n%s' % exc_info[2] |
| 432 | raise exc_info[0](exc_info[1]) |
| 433 | |
| 434 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 435 | def main(): |
| 436 | """Entry point for test_push script.""" |
| 437 | arguments = parse_arguments() |
| 438 | |
| 439 | try: |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 440 | queue = multiprocessing.Queue() |
| 441 | |
| 442 | push_to_prod_suite = multiprocessing.Process( |
| 443 | target=test_suite_wrapper, |
| 444 | args=(queue, PUSH_TO_PROD_SUITE, EXPECTED_TEST_RESULTS, |
| 445 | arguments)) |
| 446 | push_to_prod_suite.start() |
Jakob Juelich | 8f14391 | 2014-10-10 14:08:05 -0700 | [diff] [blame] | 447 | |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 448 | # TODO(dshi): Remove following line after crbug.com/267644 is fixed. |
| 449 | # Also, merge EXPECTED_TEST_RESULTS_AU to EXPECTED_TEST_RESULTS |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 450 | au_suite = multiprocessing.Process( |
| 451 | target=test_suite_wrapper, |
| 452 | args=(queue, AU_SUITE, EXPECTED_TEST_RESULTS_AU, |
| 453 | arguments)) |
| 454 | au_suite.start() |
| 455 | |
| 456 | shard_suite = multiprocessing.Process( |
| 457 | target=test_suite_wrapper, |
| 458 | args=(queue, DUMMY_SUITE, EXPECTED_TEST_RESULTS_DUMMY, |
| 459 | arguments, True)) |
| 460 | shard_suite.start() |
| 461 | |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 462 | # suite test with --create_and_return flag |
| 463 | asynchronous_suite = multiprocessing.Process( |
| 464 | target=test_suite_wrapper, |
| 465 | args=(queue, DUMMY_SUITE, EXPECTED_TEST_RESULTS_DUMMY, |
| 466 | arguments, True, True)) |
| 467 | asynchronous_suite.start() |
| 468 | |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 469 | while (push_to_prod_suite.is_alive() or au_suite.is_alive() or |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 470 | shard_suite.is_alive() or asynchronous_suite.is_alive()): |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 471 | check_queue(queue) |
Dan Shi | ef1a5c0 | 2015-04-07 17:37:09 -0700 | [diff] [blame] | 472 | time.sleep(5) |
| 473 | |
| 474 | check_queue(queue) |
| 475 | |
| 476 | push_to_prod_suite.join() |
| 477 | au_suite.join() |
| 478 | shard_suite.join() |
Shuqian Zhao | d486477 | 2015-08-06 09:46:22 -0700 | [diff] [blame] | 479 | asynchronous_suite.join() |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 480 | except Exception as e: |
| 481 | print 'Test for pushing to prod failed:\n' |
| 482 | print str(e) |
| 483 | # Send out email about the test failure. |
| 484 | if arguments.email: |
Dan Shi | 5fa602c | 2015-03-26 17:54:13 -0700 | [diff] [blame] | 485 | gmail_lib.send_email( |
| 486 | arguments.email, |
| 487 | 'Test for pushing to prod failed. Do NOT push!', |
| 488 | ('Errors occurred during the test:\n\n%s\n\n' % str(e) + |
| 489 | 'run_suite output:\n\n%s' % '\n'.join(run_suite_output))) |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 490 | raise |
| 491 | |
| 492 | message = ('\nAll tests are completed successfully, prod branch is ready to' |
| 493 | ' be pushed.') |
| 494 | print message |
| 495 | # Send out email about test completed successfully. |
| 496 | if arguments.email: |
Dan Shi | 5fa602c | 2015-03-26 17:54:13 -0700 | [diff] [blame] | 497 | gmail_lib.send_email( |
| 498 | arguments.email, |
| 499 | 'Test for pushing to prod completed successfully', |
| 500 | message) |
Dan Shi | 7e04fa8 | 2013-07-25 15:08:48 -0700 | [diff] [blame] | 501 | |
| 502 | |
| 503 | if __name__ == '__main__': |
| 504 | sys.exit(main()) |