blob: af32e77f0f5cdbedcbcf19e5198c029f66c7a413 [file] [log] [blame]
Ilja H. Friedelbee84a72016-09-28 15:57:06 -07001# Copyright 2016 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# repohooks/pre-upload.py currently does not run pylint. But for developers who
6# want to check their code manually we disable several harmless pylint warnings
7# which just distract from more serious remaining issues.
8#
9# The instance variables _host and _install_paths are not defined in __init__().
10# pylint: disable=attribute-defined-outside-init
11#
12# Many short variable names don't follow the naming convention.
13# pylint: disable=invalid-name
14#
15# _parse_result() and _dir_size() don't access self and could be functions.
16# pylint: disable=no-self-use
17#
18# _ChromeLogin and _TradefedLogCollector have no public methods.
19# pylint: disable=too-few-public-methods
20
21import contextlib
22import errno
David Haddockb9a362b2016-10-28 16:19:12 -070023import glob
Ilja H. Friedelbee84a72016-09-28 15:57:06 -070024import hashlib
25import logging
26import os
27import pipes
28import random
29import re
30import shutil
31import stat
32import tempfile
33import urlparse
34
35from autotest_lib.client.bin import utils as client_utils
36from autotest_lib.client.common_lib import error
37from autotest_lib.client.common_lib.cros import dev_server
38from autotest_lib.server import afe_utils
39from autotest_lib.server import autotest
40from autotest_lib.server import test
41from autotest_lib.server import utils
42from autotest_lib.site_utils import lxc
43
44try:
45 import lockfile
46except ImportError:
47 if utils.is_in_container():
48 # Ensure the container has the required packages installed.
49 lxc.install_packages(python_packages=['lockfile'])
50 import lockfile
51 else:
52 raise
53
54
55_SDK_TOOLS_DIR = ('gs://chromeos-arc-images/builds/'
Ilja H. Friedel5c46f522016-12-07 20:24:00 -080056 'git_mnc-dr-arc-dev-linux-static_sdk_tools/3554341')
Ilja H. Friedelbee84a72016-09-28 15:57:06 -070057_SDK_TOOLS_FILES = ['aapt']
58# To stabilize adb behavior, we use dynamically linked adb.
59_ADB_DIR = ('gs://chromeos-arc-images/builds/'
Ilja H. Friedel5c46f522016-12-07 20:24:00 -080060 'git_mnc-dr-arc-dev-linux-cheets_arm-user/3554341')
Ilja H. Friedel94639902017-01-18 00:42:44 -080061# TODO(ihf): Make this the path below as it seems to work locally.
62# 'git_mnc-dr-arc-dev-linux-static_sdk_tools/3554341')
Ilja H. Friedelbee84a72016-09-28 15:57:06 -070063_ADB_FILES = ['adb']
64
65_ADB_POLLING_INTERVAL_SECONDS = 1
66_ADB_READY_TIMEOUT_SECONDS = 60
67_ANDROID_ADB_KEYS_PATH = '/data/misc/adb/adb_keys'
68
69_ARC_POLLING_INTERVAL_SECONDS = 1
70_ARC_READY_TIMEOUT_SECONDS = 60
71
72_TRADEFED_PREFIX = 'autotest-tradefed-install_'
73_TRADEFED_CACHE_LOCAL = '/tmp/autotest-tradefed-cache'
74_TRADEFED_CACHE_CONTAINER = '/usr/local/autotest/results/shared/cache'
75_TRADEFED_CACHE_CONTAINER_LOCK = '/usr/local/autotest/results/shared/lock'
76
77# According to dshi a drone has 500GB of disk space. It is ok for now to use
78# 10GB of disk space, as no more than 10 tests should run in parallel.
79# TODO(ihf): Investigate tighter cache size.
80_TRADEFED_CACHE_MAX_SIZE = (10 * 1024 * 1024 * 1024)
81
82
83class _ChromeLogin(object):
84 """Context manager to handle Chrome login state."""
85
86 def __init__(self, host):
87 self._host = host
88
89 def __enter__(self):
90 """Logs in to the Chrome."""
91 logging.info('Ensure Android is running...')
92 autotest.Autotest(self._host).run_test('cheets_CTSHelper',
93 check_client_result=True)
94
95 def __exit__(self, exc_type, exc_value, traceback):
96 """On exit, to wipe out all the login state, reboot the machine.
97
98 @param exc_type: Exception type if an exception is raised from the
99 with-block.
100 @param exc_value: Exception instance if an exception is raised from
101 the with-block.
102 @param traceback: Stack trace info if an exception is raised from
103 the with-block.
104 @return None, indicating not to ignore an exception from the with-block
105 if raised.
106 """
107 logging.info('Rebooting...')
108 try:
109 self._host.reboot()
110 except Exception:
111 if exc_type is None:
112 raise
113 # If an exception is raise from the with-block, just record the
114 # exception for the rebooting to avoid ignoring the original
115 # exception.
116 logging.exception('Rebooting failed.')
117
118
119@contextlib.contextmanager
120def lock(filename):
121 """Prevents other autotest/tradefed instances from accessing cache."""
122 filelock = lockfile.FileLock(filename)
123 # It is tempting just to call filelock.acquire(3600). But the implementation
124 # has very poor temporal granularity (timeout/10), which is unsuitable for
125 # our needs. See /usr/lib64/python2.7/site-packages/lockfile/
Ilja H. Friedeld2410cc2016-10-27 11:38:45 -0700126 attempts = 0
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700127 while not filelock.i_am_locking():
128 try:
Ilja H. Friedeld2410cc2016-10-27 11:38:45 -0700129 attempts += 1
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700130 logging.info('Waiting for cache lock...')
131 filelock.acquire(random.randint(1, 5))
132 except (lockfile.AlreadyLocked, lockfile.LockTimeout):
Ilja H. Friedeld2410cc2016-10-27 11:38:45 -0700133 if attempts > 1000:
134 # Normally we should aqcuire the lock in a few seconds. Once we
135 # wait on the order of hours either the dev server IO is
136 # overloaded or a lock didn't get cleaned up. Take one for the
137 # team, break the lock and report a failure. This should fix
138 # the lock for following tests. If the failure affects more than
139 # one job look for a deadlock or dev server overload.
140 logging.error('Permanent lock failure. Trying to break lock.')
141 filelock.break_lock()
142 raise error.TestFail('Error: permanent cache lock failure.')
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700143 else:
Ilja H. Friedeld2410cc2016-10-27 11:38:45 -0700144 logging.info('Acquired cache lock after %d attempts.', attempts)
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700145 try:
146 yield
147 finally:
148 filelock.release()
149 logging.info('Released cache lock.')
150
151
152class TradefedTest(test.test):
153 """Base class to prepare DUT to run tests via tradefed."""
154 version = 1
155
156 def initialize(self, host=None):
157 """Sets up the tools and binary bundles for the test."""
158 logging.info('Hostname: %s', host.hostname)
159 self._host = host
160 self._install_paths = []
161 # Tests in the lab run within individual lxc container instances.
162 if utils.is_in_container():
163 # Ensure the container has the required packages installed.
164 lxc.install_packages(packages=['unzip', 'default-jre'])
165 cache_root = _TRADEFED_CACHE_CONTAINER
166 else:
167 cache_root = _TRADEFED_CACHE_LOCAL
Ilja H. Friedel94639902017-01-18 00:42:44 -0800168 # Quick sanity check and spew of java version installed on the server.
169 utils.run('java', args=('-version',), ignore_status=False, verbose=True,
170 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS)
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700171 # The content of the cache survives across jobs.
172 self._safe_makedirs(cache_root)
173 self._tradefed_cache = os.path.join(cache_root, 'cache')
174 self._tradefed_cache_lock = os.path.join(cache_root, 'lock')
175 # The content of the install location does not survive across jobs and
176 # is isolated (by using a unique path)_against other autotest instances.
177 # This is not needed for the lab, but if somebody wants to run multiple
178 # TradedefTest instance.
179 self._tradefed_install = tempfile.mkdtemp(prefix=_TRADEFED_PREFIX)
180 # Under lxc the cache is shared between multiple autotest/tradefed
181 # instances. We need to synchronize access to it. All binaries are
182 # installed through the (shared) cache into the local (unshared)
183 # lxc/autotest instance storage.
184 # If clearing the cache it must happen before all downloads.
185 self._clear_download_cache_if_needed()
186 # Set permissions (rwxr-xr-x) to the executable binaries.
187 permission = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH
188 | stat.S_IXOTH)
189 self._install_files(_ADB_DIR, _ADB_FILES, permission)
190 self._install_files(_SDK_TOOLS_DIR, _SDK_TOOLS_FILES, permission)
191
192 def cleanup(self):
193 """Cleans up any dirtied state."""
194 # Kill any lingering adb servers.
195 self._run('adb', verbose=True, args=('kill-server',))
196 logging.info('Cleaning up %s.', self._tradefed_install)
197 shutil.rmtree(self._tradefed_install)
198
199 def _login_chrome(self):
200 """Returns Chrome log-in context manager.
201
202 Please see also cheets_CTSHelper for details about how this works.
203 """
204 return _ChromeLogin(self._host)
205
206 def _try_adb_connect(self):
207 """Attempts to connect to adb on the DUT.
208
209 @return boolean indicating if adb connected successfully.
210 """
211 # This may fail return failure due to a race condition in adb connect
212 # (b/29370989). If adb is already connected, this command will
213 # immediately return success.
214 hostport = '{}:{}'.format(self._host.hostname, self._host.port)
215 result = self._run(
216 'adb',
217 args=('connect', hostport),
218 verbose=True,
219 ignore_status=True)
220 logging.info('adb connect {}:\n{}'.format(hostport, result.stdout))
221 if result.exit_status != 0:
222 return False
223
224 result = self._run('adb', args=('devices',))
225 logging.info('adb devices:\n' + result.stdout)
226 if not re.search(
227 r'{}\s+(device|unauthorized)'.format(re.escape(hostport)),
228 result.stdout):
229 return False
230
231 # Actually test the connection with an adb command as there can be
232 # a race between detecting the connected device and actually being
233 # able to run a commmand with authenticated adb.
234 result = self._run('adb', args=('shell', 'exit'), ignore_status=True)
235 return result.exit_status == 0
236
237 def _android_shell(self, command):
238 """Run a command remotely on the device in an android shell
239
240 This function is strictly for internal use only, as commands do not run
241 in a fully consistent Android environment. Prefer adb shell instead.
242 """
243 self._host.run('android-sh -c ' + pipes.quote(command))
244
245 def _write_android_file(self, filename, data):
246 """Writes a file to a location relative to the android container.
247
248 This is an internal function used to bootstrap adb.
249 Tests should use adb push to write files.
250 """
251 android_cmd = 'echo %s > %s' % (pipes.quote(data),
252 pipes.quote(filename))
253 self._android_shell(android_cmd)
254
255 def _connect_adb(self):
256 """Sets up ADB connection to the ARC container."""
257 logging.info('Setting up adb connection.')
258 # Generate and push keys for adb.
259 # TODO(elijahtaylor): Extract this code to arc_common and de-duplicate
260 # code in arc.py on the client side tests.
261 key_path = os.path.join(self.tmpdir, 'test_key')
262 pubkey_path = key_path + '.pub'
263 self._run('adb', verbose=True, args=('keygen', pipes.quote(key_path)))
264 with open(pubkey_path, 'r') as f:
265 self._write_android_file(_ANDROID_ADB_KEYS_PATH, f.read())
266 self._android_shell('restorecon ' + pipes.quote(_ANDROID_ADB_KEYS_PATH))
267 os.environ['ADB_VENDOR_KEYS'] = key_path
268
269 # Kill existing adb server to ensure that the env var is picked up.
270 self._run('adb', verbose=True, args=('kill-server',))
271
272 # This starts adbd.
273 self._android_shell('setprop sys.usb.config mtp,adb')
274
275 # adbd may take some time to come up. Repeatedly try to connect to adb.
276 utils.poll_for_condition(lambda: self._try_adb_connect(),
Ilja H. Friedel6d5ca8f2016-10-26 22:35:36 -0700277 exception=error.TestFail(
278 'Error: Failed to set up adb connection'),
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700279 timeout=_ADB_READY_TIMEOUT_SECONDS,
280 sleep_interval=_ADB_POLLING_INTERVAL_SECONDS)
281
282 logging.info('Successfully setup adb connection.')
283
284 def _wait_for_arc_boot(self):
285 """Wait until ARC is fully booted.
286
287 Tests for the presence of the intent helper app to determine whether ARC
288 has finished booting.
289 """
290 def intent_helper_running():
Kazuhiro Inabaf2c47052017-01-26 09:18:51 +0900291 result = self._run('adb', args=('shell', 'pgrep', '-f',
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700292 'org.chromium.arc.intent_helper'))
293 return bool(result.stdout)
294 utils.poll_for_condition(
295 intent_helper_running,
Ilja H. Friedel6d5ca8f2016-10-26 22:35:36 -0700296 exception=error.TestFail(
297 'Error: Timed out waiting for intent helper.'),
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700298 timeout=_ARC_READY_TIMEOUT_SECONDS,
299 sleep_interval=_ARC_POLLING_INTERVAL_SECONDS)
300
301 def _disable_adb_install_dialog(self):
302 """Disables a dialog shown on adb install execution.
303
304 By default, on adb install execution, "Allow Google to regularly check
305 device activity ... " dialog is shown. It requires manual user action
306 so that tests are blocked at the point.
307 This method disables it.
308 """
309 logging.info('Disabling the adb install dialog.')
310 result = self._run(
311 'adb',
312 verbose=True,
313 args=(
314 'shell',
315 'settings',
316 'put',
317 'global',
318 'verifier_verify_adb_installs',
319 '0'))
320 logging.info('Disable adb dialog: %s', result.stdout)
321
322 def _ready_arc(self):
323 """Ready ARC and adb for running tests via tradefed."""
324 self._connect_adb()
325 self._disable_adb_install_dialog()
326 self._wait_for_arc_boot()
327
328 def _safe_makedirs(self, path):
329 """Creates a directory at |path| and its ancestors.
330
331 Unlike os.makedirs(), ignore errors even if directories exist.
332 """
333 try:
334 os.makedirs(path)
335 except OSError as e:
336 if not (e.errno == errno.EEXIST and os.path.isdir(path)):
337 raise
338
339 def _unzip(self, filename):
340 """Unzip the file.
341
342 The destination directory name will be the stem of filename.
343 E.g., _unzip('foo/bar/baz.zip') will create directory at
344 'foo/bar/baz', and then will inflate zip's content under the directory.
345 If here is already a directory at the stem, that directory will be used.
346
347 @param filename: Path to the zip archive.
348 @return Path to the inflated directory.
349 """
350 destination = os.path.splitext(filename)[0]
351 if os.path.isdir(destination):
352 return destination
353 self._safe_makedirs(destination)
354 utils.run('unzip', args=('-d', destination, filename))
355 return destination
356
357 def _dir_size(self, directory):
358 """Compute recursive size in bytes of directory."""
359 size = 0
360 for root, _, files in os.walk(directory):
361 size += sum(os.path.getsize(os.path.join(root, name))
362 for name in files)
363 return size
364
365 def _clear_download_cache_if_needed(self):
366 """Invalidates cache to prevent it from growing too large."""
367 # If the cache is large enough to hold a working set, we can simply
368 # delete everything without thrashing.
369 # TODO(ihf): Investigate strategies like LRU.
370 with lock(self._tradefed_cache_lock):
371 size = self._dir_size(self._tradefed_cache)
372 if size > _TRADEFED_CACHE_MAX_SIZE:
373 logging.info('Current cache size=%d got too large. Clearing %s.'
374 , size, self._tradefed_cache)
375 shutil.rmtree(self._tradefed_cache)
376 self._safe_makedirs(self._tradefed_cache)
377 else:
378 logging.info('Current cache size=%d of %s.', size,
379 self._tradefed_cache)
380
381 def _download_to_cache(self, uri):
382 """Downloads the uri from the storage server.
383
384 It always checks the cache for available binaries first and skips
385 download if binaries are already in cache.
386
387 The caller of this function is responsible for holding the cache lock.
388
389 @param uri: The Google Storage or dl.google.com uri.
390 @return Path to the downloaded object, name.
391 """
392 # Split uri into 3 pieces for use by gsutil and also by wget.
393 parsed = urlparse.urlparse(uri)
394 filename = os.path.basename(parsed.path)
395 # We are hashing the uri instead of the binary. This is acceptable, as
396 # the uris are supposed to contain version information and an object is
397 # not supposed to be changed once created.
398 output_dir = os.path.join(self._tradefed_cache,
399 hashlib.md5(uri).hexdigest())
400 output = os.path.join(output_dir, filename)
401 # Check for existence of file.
402 if os.path.exists(output):
403 logging.info('Skipping download of %s, reusing %s.', uri, output)
404 return output
405 self._safe_makedirs(output_dir)
406
407 if parsed.scheme not in ['gs', 'http', 'https']:
Ilja H. Friedel6d5ca8f2016-10-26 22:35:36 -0700408 raise error.TestFail('Error: Unknown download scheme %s' %
409 parsed.scheme)
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700410 if parsed.scheme in ['http', 'https']:
411 logging.info('Using wget to download %s to %s.', uri, output_dir)
412 # We are downloading 1 file at a time, hence using -O over -P.
413 # We also limit the rate to 20MBytes/s
414 utils.run(
415 'wget',
416 args=(
417 '--report-speed=bits',
418 '--limit-rate=20M',
419 '-O',
420 output,
421 uri),
422 verbose=True)
423 return output
424
425 if not client_utils.is_moblab():
426 # If the machine can access to the storage server directly,
427 # defer to "gsutil" for downloading.
428 logging.info('Host %s not in lab. Downloading %s directly to %s.',
429 self._host.hostname, uri, output)
430 # b/17445576: gsutil rsync of individual files is not implemented.
431 utils.run('gsutil', args=('cp', uri, output), verbose=True)
432 return output
433
434 # We are in the moblab. Because the machine cannot access the storage
435 # server directly, use dev server to proxy.
436 logging.info('Host %s is in lab. Downloading %s by staging to %s.',
437 self._host.hostname, uri, output)
438
439 dirname = os.path.dirname(parsed.path)
440 archive_url = '%s://%s%s' % (parsed.scheme, parsed.netloc, dirname)
441
442 # First, request the devserver to download files into the lab network.
443 # TODO(ihf): Switch stage_artifacts to honor rsync. Then we don't have
444 # to shuffle files inside of tarballs.
445 build = afe_utils.get_build(self._host)
446 ds = dev_server.ImageServer.resolve(build)
447 ds.stage_artifacts(build, files=[filename], archive_url=archive_url)
448
449 # Then download files from the dev server.
450 # TODO(ihf): use rsync instead of wget. Are there 3 machines involved?
451 # Itself, dev_server plus DUT? Or is there just no rsync in moblab?
452 ds_src = '/'.join([ds.url(), 'static', dirname, filename])
453 logging.info('dev_server URL: %s', ds_src)
454 # Calls into DUT to pull uri from dev_server.
455 utils.run(
456 'wget',
457 args=(
458 '--report-speed=bits',
459 '--limit-rate=20M',
460 '-O',
Ilja H. Friedelb83646b2016-10-18 13:02:59 -0700461 output,
462 ds_src),
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700463 verbose=True)
464 return output
465
466 def _instance_copy(self, cache_path):
467 """Makes a copy of a file from the (shared) cache to a wholy owned
468 local instance. Also copies one level of cache directoy (MD5 named).
469 """
470 filename = os.path.basename(cache_path)
471 dirname = os.path.basename(os.path.dirname(cache_path))
472 instance_dir = os.path.join(self._tradefed_install, dirname)
473 # Make sure destination directory is named the same.
474 self._safe_makedirs(instance_dir)
475 instance_path = os.path.join(instance_dir, filename)
476 shutil.copyfile(cache_path, instance_path)
477 return instance_path
478
479 def _install_bundle(self, gs_uri):
480 """Downloads a zip file, installs it and returns the local path."""
481 if not gs_uri.endswith('.zip'):
Ilja H. Friedel6d5ca8f2016-10-26 22:35:36 -0700482 raise error.TestFail('Error: Not a .zip file %s.', gs_uri)
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700483 # Atomic write through of file.
484 with lock(self._tradefed_cache_lock):
485 cache_path = self._download_to_cache(gs_uri)
486 local = self._instance_copy(cache_path)
David Haddockb9a362b2016-10-28 16:19:12 -0700487
488 unzipped = self._unzip(local)
489 self._abi = 'x86' if 'x86-x86' in unzipped else 'arm'
490 return unzipped
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700491
492 def _install_files(self, gs_dir, files, permission):
493 """Installs binary tools."""
494 for filename in files:
495 gs_uri = os.path.join(gs_dir, filename)
496 # Atomic write through of file.
497 with lock(self._tradefed_cache_lock):
498 cache_path = self._download_to_cache(gs_uri)
499 local = self._instance_copy(cache_path)
500 os.chmod(local, permission)
501 # Keep track of PATH.
502 self._install_paths.append(os.path.dirname(local))
503
504 def _run(self, *args, **kwargs):
505 """Executes the given command line.
506
507 To support SDK tools, such as adb or aapt, this adds _install_paths
508 to the extra_paths. Before invoking this, ensure _install_files() has
509 been called.
510 """
511 kwargs['extra_paths'] = (
512 kwargs.get('extra_paths', []) + self._install_paths)
513 return utils.run(*args, **kwargs)
514
515 def _parse_tradefed_datetime(self, result, summary=None):
516 """Get the tradefed provided result ID consisting of a datetime stamp.
517
518 Unfortunately we are unable to tell tradefed where to store the results.
519 In the lab we have multiple instances of tradefed running in parallel
520 writing results and logs to the same base directory. This function
521 finds the identifier which tradefed used during the current run and
522 returns it for further processing of result files.
523
524 @param result: The result object from utils.run.
525 @param summary: Test result summary from runs so far.
526 @return datetime_id: The result ID chosen by tradefed.
527 Example: '2016.07.14_00.34.50'.
528 """
529 # This string is show for both 'run' and 'continue' after all tests.
530 match = re.search(r': XML test result file generated at (\S+). Passed',
531 result.stdout)
532 if not (match and match.group(1)):
533 # TODO(ihf): Find out if we ever recover something interesting in
534 # this case. Otherwise delete it.
535 # Try harder to find the remains. This string shows before all
536 # tests but only with 'run', not 'continue'.
537 logging.warning('XML test result file incomplete?')
538 match = re.search(r': Created result dir (\S+)', result.stdout)
539 if not (match and match.group(1)):
540 error_msg = 'Test did not complete due to Chrome or ARC crash.'
541 if summary:
542 error_msg += (' Test summary from previous runs: %s'
543 % summary)
Ilja H. Friedel6d5ca8f2016-10-26 22:35:36 -0700544 raise error.TestFail(error_msg)
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700545 datetime_id = match.group(1)
546 logging.info('Tradefed identified results and logs with %s.',
547 datetime_id)
548 return datetime_id
549
Rohit Makasana99116d32016-10-17 19:32:04 -0700550 def _parse_result(self, result, waivers=None):
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700551 """Check the result from the tradefed output.
552
553 This extracts the test pass/fail/executed list from the output of
554 tradefed. It is up to the caller to handle inconsistencies.
555
556 @param result: The result object from utils.run.
Rohit Makasana99116d32016-10-17 19:32:04 -0700557 @param waivers: a set() of tests which are permitted to fail.
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700558 """
559 # Parse the stdout to extract test status. In particular step over
560 # similar output for each ABI and just look at the final summary.
561 match = re.search(r'(XML test result file generated at (\S+). '
562 r'Passed (\d+), Failed (\d+), Not Executed (\d+))',
563 result.stdout)
564 if not match:
565 raise error.Test('Test log does not contain a summary.')
566
567 passed = int(match.group(3))
568 failed = int(match.group(4))
569 not_executed = int(match.group(5))
570 match = re.search(r'(Start test run of (\d+) packages, containing '
571 r'(\d+(?:,\d+)?) tests)', result.stdout)
572 if match and match.group(3):
573 tests = int(match.group(3).replace(',', ''))
574 else:
575 # Unfortunately this happens. Assume it made no other mistakes.
576 logging.warning('Tradefed forgot to print number of tests.')
577 tests = passed + failed + not_executed
Rohit Makasana99116d32016-10-17 19:32:04 -0700578 # TODO(rohitbm): make failure parsing more robust by extracting the list
579 # of failing tests instead of searching in the result blob. As well as
580 # only parse for waivers for the running ABI.
581 if waivers:
582 for testname in waivers:
David Haddock16712332016-11-03 14:35:23 -0700583 # TODO(dhaddock): Find a more robust way to apply waivers.
584 fail_count = result.stdout.count(testname + ' FAIL')
585 if fail_count:
586 if fail_count > 2:
587 raise error.TestFail('Error: There are too many '
588 'failures found in the output to '
589 'be valid for applying waivers. '
590 'Please check output.')
591 failed -= fail_count
Rohit Makasana99116d32016-10-17 19:32:04 -0700592 # To maintain total count consistency.
David Haddock16712332016-11-03 14:35:23 -0700593 passed += fail_count
594 logging.info('Waived failure for %s %d time(s)',
595 testname, fail_count)
Rohit Makasana99116d32016-10-17 19:32:04 -0700596 logging.info('tests=%d, passed=%d, failed=%d, not_executed=%d',
597 tests, passed, failed, not_executed)
David Haddock16712332016-11-03 14:35:23 -0700598 if failed < 0:
599 raise error.TestFail('Error: Internal waiver book keeping has '
600 'become inconsistent.')
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700601 return (tests, passed, failed, not_executed)
602
603 def _collect_logs(self, repository, datetime, destination):
604 """Collects the tradefed logs.
605
606 It is legal to collect the same logs multiple times. This is normal
607 after 'tradefed continue' updates existing logs with new results.
608
609 @param repository: Full path to tradefeds output on disk.
610 @param datetime: The identifier which tradefed assigned to the run.
611 Currently this looks like '2016.07.14_00.34.50'.
612 @param destination: Autotest result directory (destination of logs).
613 """
614 logging.info('Collecting tradefed testResult.xml and logs to %s.',
615 destination)
616 repository_results = os.path.join(repository, 'results')
617 repository_logs = os.path.join(repository, 'logs')
618 # Because other tools rely on the currently chosen Google storage paths
619 # we need to keep destination_results in
620 # cheets_CTS.*/results/android-cts/2016.mm.dd_hh.mm.ss(/|.zip)
621 # and destination_logs in
622 # cheets_CTS.*/results/android-cts/logs/2016.mm.dd_hh.mm.ss/
623 destination_results = destination
Ilja H. Friedelb83646b2016-10-18 13:02:59 -0700624 destination_results_datetime = os.path.join(destination_results,
625 datetime)
Ilja H. Friedelbee84a72016-09-28 15:57:06 -0700626 destination_results_datetime_zip = destination_results_datetime + '.zip'
627 destination_logs = os.path.join(destination, 'logs')
628 destination_logs_datetime = os.path.join(destination_logs, datetime)
629 # We may have collected the same logs before, clean old versions.
630 if os.path.exists(destination_results_datetime_zip):
631 os.remove(destination_results_datetime_zip)
632 if os.path.exists(destination_results_datetime):
633 shutil.rmtree(destination_results_datetime)
634 if os.path.exists(destination_logs_datetime):
635 shutil.rmtree(destination_logs_datetime)
636 shutil.copytree(
637 os.path.join(repository_results, datetime),
638 destination_results_datetime)
639 # Copying the zip file has to happen after the tree so the destination
640 # directory is available.
641 shutil.copy(
642 os.path.join(repository_results, datetime) + '.zip',
643 destination_results_datetime_zip)
644 shutil.copytree(
645 os.path.join(repository_logs, datetime),
646 destination_logs_datetime)
David Haddockb9a362b2016-10-28 16:19:12 -0700647
Rohit Makasana77566902016-11-01 15:34:27 -0700648 def _get_expected_failures(self, directory):
649 """Return a list of expected failures.
David Haddockb9a362b2016-10-28 16:19:12 -0700650
Rohit Makasana77566902016-11-01 15:34:27 -0700651 @return: a list of expected failures.
David Haddockb9a362b2016-10-28 16:19:12 -0700652 """
Rohit Makasana77566902016-11-01 15:34:27 -0700653 logging.info('Loading expected failures from %s.', directory)
654 expected_fail_dir = os.path.join(self.bindir, directory)
David Haddockb9a362b2016-10-28 16:19:12 -0700655 expected_fail_files = glob.glob(expected_fail_dir + '/*.' + self._abi)
Rohit Makasana77566902016-11-01 15:34:27 -0700656 expected_failures = set()
David Haddockb9a362b2016-10-28 16:19:12 -0700657 for expected_fail_file in expected_fail_files:
658 try:
659 file_path = os.path.join(expected_fail_dir, expected_fail_file)
660 with open(file_path) as f:
661 lines = set(f.read().splitlines())
662 logging.info('Loaded %d expected failures from %s',
663 len(lines), expected_fail_file)
Rohit Makasana77566902016-11-01 15:34:27 -0700664 expected_failures |= lines
David Haddockb9a362b2016-10-28 16:19:12 -0700665 except IOError as e:
666 logging.error('Error loading %s (%s).', file_path, e.strerror)
Rohit Makasana77566902016-11-01 15:34:27 -0700667 logging.info('Finished loading expected failures: %s', expected_failures)
668 return expected_failures