blob: 8fa00db300145d93fead1917f73337b52a3d5160 [file] [log] [blame]
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
3# Copyright 2018 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"""Evaluate ChromeOS autotest.
7
8Note that by default 'test_that' will install dependency packages of autotest
9only once. For example, if you overwrote chrome's unittest binary, your new
Kuang-che Wu927231f2018-07-24 14:21:56 +080010binary will be persistent across autotest runs. Add --reinstall if you want
Kuang-che Wub9705bd2018-06-28 17:59:18 +080011clean autotest install.
12"""
13from __future__ import print_function
14import argparse
15import json
16import logging
17import os
18import re
19import subprocess
20import sys
21
22from bisect_kit import cli
23from bisect_kit import common
24from bisect_kit import configure
25from bisect_kit import cros_util
26from bisect_kit import util
27
28logger = logging.getLogger(__name__)
29
30OLD = 'old'
31NEW = 'new'
32SKIP = 'skip'
33FATAL = 'fatal'
34
35EXIT_CODE_MAP = {
36 OLD: 0,
37 NEW: 1,
38 SKIP: 125,
39 FATAL: 126,
40}
41
42
43def create_argument_parser():
44 parser = argparse.ArgumentParser(description=__doc__)
45 common.add_common_arguments(parser)
46 parser.add_argument(
47 'dut',
48 nargs='?',
49 type=cli.argtype_notempty,
50 metavar='DUT',
51 default=configure.get('DUT', ''))
52 parser.add_argument(
53 '--chromeos_root',
54 type=cli.argtype_dir_path,
55 metavar='CHROMEOS_ROOT',
56 default=configure.get('CHROMEOS_ROOT', ''),
57 help='ChromeOS tree root')
58 parser.add_argument(
Kuang-che Wud4603d72018-11-29 17:51:21 +080059 '--chrome_root',
60 metavar='CHROME_ROOT',
61 type=cli.argtype_dir_path,
62 default=configure.get('CHROME_ROOT'),
63 help='Chrome tree root; necessary for telemetry tests')
64 parser.add_argument(
Kuang-che Wub9705bd2018-06-28 17:59:18 +080065 '--test_name',
66 required=True,
67 help='Test name, like "video_VideoDecodeAccelerator.h264"')
68 parser.add_argument(
69 '--metric',
70 help=
71 'Metric name of performance test; example: "cheets_SystemRawImageSize"')
72 parser.add_argument(
73 '--old_value',
74 type=float,
75 help='For performance test, old value of given metric')
76 parser.add_argument(
77 '--new_value',
78 type=float,
79 help='For performance test, new value of given metric')
80 parser.add_argument(
81 '--prebuilt',
82 action='store_true',
83 help='Run autotest using existing prebuilt package if specified; '
84 'otherwise use the default one')
85 parser.add_argument(
86 '--reinstall',
87 action='store_true',
88 help='Remove existing autotest folder on the DUT first')
89 parser.add_argument(
90 '--args',
91 help='Extra args passed to "test_that --args"; Overrides the default')
92
93 return parser
94
95
96def parse_test_report_log(result_log, metric):
97 """Parses autotest result log.
98
99 Args:
100 result_log: content of test_report.log
101 metric: what metric to capture if not None
102
103 Returns:
104 passed, values:
105 passed: True if test run successfully
106 values: captured metric values; None if test failed or metric is None
107 """
108 m = re.search(r'Total PASS: (\d+)/(\d+)', result_log)
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800109 passed = (m and m.group(1) == m.group(2))
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800110
111 if not metric:
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800112 return passed, None
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800113
114 values = []
115 for line in result_log.splitlines():
116 m = re.match(r'^(\S+)\s+(\w+)(?:\{\d+\})?\s+(\d+\.\d+)$', line)
117 if not m:
118 continue
119 if m.group(2) == metric:
120 values.append(float(m.group(3)))
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800121 return passed, values
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800122
123
124def parse_test_result_chart(json_path, metric):
125 data = json.load(open(json_path))
Kuang-che Wu3331caf2018-09-06 19:47:02 +0800126
127 # format 1, telemetry
128 if 'charts' in data:
129 summary = data['charts'][metric]['summary']
130
131 # format 2, autotest without graph
132 elif metric in data:
133 summary = data[metric]['summary']
134
135 # format 3, autotest with graph
136 elif metric.count('.') == 1:
137 name, subname = metric.split('.')
138 summary = data[name][subname]
139
140 else:
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800141 logger.error('metric "%s" not in %s', metric, json_path)
Kuang-che Wudd802672018-08-10 19:40:14 +0800142 return []
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800143
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800144 if 'values' in summary:
145 return summary['values']
146 return [summary['value']]
147
148
149def get_additional_test_args(test_name):
150 """Gets extra arguments to specific test.
151
152 Some tests may require special arguments to run.
153
154 Args:
155 test_name: test name
156
157 Returns:
158 arguments (str)
159 """
160 if test_name.startswith('telemetry_'):
161 return 'local=True'
162 return ''
163
164
165def run_test(opts):
166 """Runs an autotest test.
167
168 Args:
169 opts: An argparse.Namespace to hold command line arguments.
170
171 Returns:
172 path of test result (outside chroot)
173 """
174 if opts.reinstall:
175 util.check_call('ssh', opts.dut, 'rm', '-rf', '/usr/local/autotest')
176
177 prebuilt_autotest_dir = os.path.join(cros_util.chromeos_root_inside_chroot,
178 cros_util.prebuilt_autotest_dir)
179 # Set results dir inside source tree, so it's easier to access them outside
180 # chroot.
181 results_dir = os.path.join(cros_util.chromeos_root_inside_chroot,
182 'tmp/autotest_results_tmp')
183 if opts.prebuilt:
184 test_that_bin = os.path.join(prebuilt_autotest_dir,
185 'site_utils/test_that.py')
186 else:
187 test_that_bin = '/usr/bin/test_that'
188 cmd = [test_that_bin, opts.dut, opts.test_name, '--results_dir', results_dir]
189 if opts.prebuilt:
190 cmd += ['--autotest_dir', prebuilt_autotest_dir]
191
192 args = get_additional_test_args(opts.test_name)
193 if opts.args:
194 if args:
Kuang-che Wu74768d32018-09-07 12:03:24 +0800195 logger.info(
196 'default test_that args `%s` is overridden by '
197 'command line option `%s`', args, opts.args)
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800198 cmd += ['--args', opts.args]
199 elif args:
200 cmd += ['--args', args]
201
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800202 try:
Kuang-che Wud4603d72018-11-29 17:51:21 +0800203 output = cros_util.cros_sdk(
204 opts.chromeos_root, *cmd, chrome_root=opts.chrome_root)
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800205 except subprocess.CalledProcessError as e:
206 output = e.output
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800207
208 m = re.search(r'Finished running tests. Results can be found in (\S+)',
209 output)
210 if not m:
211 logger.error('result dir is unknown')
212 return None
213 assert m.group(1) == results_dir
214 return results_dir.replace(cros_util.chromeos_root_inside_chroot,
215 opts.chromeos_root)
216
217
218def gather_test_result(opts, result_dir):
219 result_log_path = os.path.join(result_dir, 'test_report.log')
220 result_log = open(result_log_path).read()
221
222 passed, values = parse_test_report_log(result_log, opts.metric)
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800223 if opts.metric and not values:
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800224 values = []
225 for root, _, files in os.walk(result_dir):
226 for filename in files:
227 if filename != 'results-chart.json':
228 continue
229 full_path = os.path.join(root, filename)
230 values += parse_test_result_chart(full_path, opts.metric)
231
232 return passed, values
233
234
235def main(args=None):
236 common.init()
237 parser = create_argument_parser()
238 opts = parser.parse_args(args)
239 common.config_logging(opts)
240
241 if not cros_util.is_dut(opts.dut):
242 return FATAL
243
244 # Verify command line options.
245 if opts.metric:
246 if opts.old_value is None:
247 logger.error('--old_value is not provided')
248 return FATAL
249 if opts.new_value is None:
250 logger.error('--new_value is not provided')
251 return FATAL
252 else:
253 if opts.old_value is not None:
254 logger.error('--old_value is provided but --metric is not')
255 return FATAL
256 if opts.new_value is not None:
257 logger.error('--new_value is provided but --metric is not')
258 return FATAL
Kuang-che Wud4603d72018-11-29 17:51:21 +0800259 if opts.test_name.startswith('telemetry_'):
260 if not opts.chrome_root:
261 logger.error('--chrome_root is mandatory for telemetry tests')
262 return FATAL
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800263
Kuang-che Wue47162d2018-10-29 17:24:04 +0800264 # Some versions of ChromeOS SDK is broken and ship bad 'ssh' executable. This
265 # works around the issue before we fixed the issue.
266 # TODO(kcwu): fix crbug/899490
267 cros_util.cros_sdk(opts.chromeos_root, 'sudo', 'emerge', 'net-misc/openssh')
268
Kuang-che Wu171dcb62018-10-25 12:37:05 +0800269 result_dir = run_test(opts)
270 if not result_dir:
Kuang-che Wudd802672018-08-10 19:40:14 +0800271 return FATAL
272
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800273 passed, values = gather_test_result(opts, result_dir)
274
275 if opts.metric:
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800276 if not values:
277 logger.warning('no values found; SKIP')
278 return SKIP
279
280 print('BISECT_RESULT_VALUES=', ' '.join(map(str, values)))
281 average = float(sum(values)) / len(values)
Kuang-che Wu689f1542018-08-20 17:45:58 +0800282 if abs(average - opts.old_value) < abs(average - opts.new_value):
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800283 logger.info('values=%s, average=%s; OLD', values, average)
284 return OLD
285 logger.info('values=%s, average=%s; NEW', values, average)
286 return NEW
287 else:
288 if passed:
289 logger.info('passed')
290 return OLD
291 logger.info('failed')
292 return NEW
293
294
295if __name__ == '__main__':
296 sys.exit(EXIT_CODE_MAP[main()])