# Copyright 2018 The Chromium OS Authors. All rights reserved.
|
# Use of this source code is governed by a BSD-style license that can be
|
# found in the LICENSE file.
|
|
import contextlib
|
import logging
|
import os
|
import random
|
import re
|
|
from autotest_lib.client.bin import utils as client_utils
|
from autotest_lib.client.common_lib import utils as common_utils
|
from autotest_lib.client.common_lib import error
|
from autotest_lib.server import utils
|
from autotest_lib.server.cros import lockfile
|
|
|
@contextlib.contextmanager
|
def lock(filename):
|
"""Prevents other autotest/tradefed instances from accessing cache.
|
|
@param filename: The file to be locked.
|
"""
|
filelock = lockfile.FileLock(filename)
|
# It is tempting just to call filelock.acquire(3600). But the implementation
|
# has very poor temporal granularity (timeout/10), which is unsuitable for
|
# our needs. See /usr/lib64/python2.7/site-packages/lockfile/
|
attempts = 0
|
while not filelock.i_am_locking():
|
try:
|
attempts += 1
|
logging.info('Waiting for cache lock...')
|
# We must not use a random integer as the filelock implementations
|
# may underflow an integer division.
|
filelock.acquire(random.uniform(0.0, pow(2.0, attempts)))
|
except (lockfile.AlreadyLocked, lockfile.LockTimeout):
|
# Our goal is to wait long enough to be sure something very bad
|
# happened to the locking thread. 11 attempts is between 15 and
|
# 30 minutes.
|
if attempts > 11:
|
# Normally we should aqcuire the lock immediately. Once we
|
# wait on the order of 10 minutes either the dev server IO is
|
# overloaded or a lock didn't get cleaned up. Take one for the
|
# team, break the lock and report a failure. This should fix
|
# the lock for following tests. If the failure affects more than
|
# one job look for a deadlock or dev server overload.
|
logging.error('Permanent lock failure. Trying to break lock.')
|
# TODO(ihf): Think how to do this cleaner without having a
|
# recursive lock breaking problem. We may have to kill every
|
# job that is currently waiting. The main goal though really is
|
# to have a cache that does not corrupt. And cache updates
|
# only happen once a month or so, everything else are reads.
|
filelock.break_lock()
|
raise error.TestFail('Error: permanent cache lock failure.')
|
else:
|
logging.info('Acquired cache lock after %d attempts.', attempts)
|
try:
|
yield
|
finally:
|
filelock.release()
|
logging.info('Released cache lock.')
|
|
|
@contextlib.contextmanager
|
def adb_keepalive(targets, extra_paths):
|
"""A context manager that keeps the adb connection alive.
|
|
AdbKeepalive will spin off a new process that will continuously poll for
|
adb's connected state, and will attempt to reconnect if it ever goes down.
|
This is the only way we can currently recover safely from (intentional)
|
reboots.
|
|
@param target: the hostname and port of the DUT.
|
@param extra_paths: any additional components to the PATH environment
|
variable.
|
"""
|
from autotest_lib.client.common_lib.cros import adb_keepalive as module
|
# |__file__| returns the absolute path of the compiled bytecode of the
|
# module. We want to run the original .py file, so we need to change the
|
# extension back.
|
script_filename = module.__file__.replace('.pyc', '.py')
|
jobs = [common_utils.BgJob(
|
[script_filename, target],
|
nickname='adb_keepalive',
|
stderr_level=logging.DEBUG,
|
stdout_tee=common_utils.TEE_TO_LOGS,
|
stderr_tee=common_utils.TEE_TO_LOGS,
|
extra_paths=extra_paths) for target in targets]
|
|
try:
|
yield
|
finally:
|
# The adb_keepalive.py script runs forever until SIGTERM is sent.
|
for job in jobs:
|
common_utils.nuke_subprocess(job.sp)
|
common_utils.join_bg_jobs(jobs)
|
|
|
@contextlib.contextmanager
|
def pushd(d):
|
"""Defines pushd.
|
@param d: the directory to change to.
|
"""
|
current = os.getcwd()
|
os.chdir(d)
|
try:
|
yield
|
finally:
|
os.chdir(current)
|
|
|
def parse_tradefed_result(result, waivers=None):
|
"""Check the result from the tradefed output.
|
|
@param result: The result stdout string from the tradefed command.
|
@param waivers: a set() of tests which are permitted to fail.
|
@return List of the waived tests.
|
"""
|
# Regular expressions for start/end messages of each test-run chunk.
|
abi_re = r'arm\S*|x86\S*'
|
# TODO(kinaba): use the current running module name.
|
module_re = r'\S+'
|
start_re = re.compile(r'(?:Start|Continu)ing (%s) %s with'
|
r' (\d+(?:,\d+)?) test' % (abi_re, module_re))
|
end_re = re.compile(r'(%s) %s (?:complet|fail)ed in .*\.'
|
r' (\d+) passed, (\d+) failed, (\d+) not executed' %
|
(abi_re, module_re))
|
fail_re = re.compile(r'I/ConsoleReporter.* (\S+) fail:')
|
inaccurate_re = re.compile(r'IMPORTANT: Some modules failed to run to '
|
'completion, tests counts may be inaccurate')
|
abis = set()
|
waived_count = dict()
|
failed_tests = set()
|
accurate = True
|
for line in result.splitlines():
|
match = start_re.search(line)
|
if match:
|
abis = abis.union([match.group(1)])
|
continue
|
match = end_re.search(line)
|
if match:
|
abi = match.group(1)
|
if abi not in abis:
|
logging.error('Trunk end with %s abi but have not seen '
|
'any trunk start with this abi.(%s)', abi, line)
|
continue
|
match = fail_re.search(line)
|
if match:
|
testname = match.group(1)
|
if waivers and testname in waivers:
|
waived_count[testname] = waived_count.get(testname, 0) + 1
|
else:
|
failed_tests.add(testname)
|
continue
|
# b/66899135, tradefed may reported inaccuratly with `list results`.
|
# Add warning if summary section shows that the result is inacurrate.
|
match = inaccurate_re.search(line)
|
if match:
|
accurate = False
|
|
logging.info('Total ABIs: %s', abis)
|
if failed_tests:
|
logging.error('Failed (but not waived) tests:\n%s',
|
'\n'.join(sorted(failed_tests)))
|
|
# TODO(dhaddock): Find a more robust way to apply waivers.
|
waived = []
|
for testname, fail_count in waived_count.items():
|
if fail_count > len(abis):
|
# This should be an error.TestFail, but unfortunately
|
# tradefed has a bug that emits "fail" twice when a
|
# test failed during teardown. It will anyway causes
|
# a test count inconsistency and visible on the dashboard.
|
logging.error('Found %d failures for %s but there are only %d '
|
'abis: %s', fail_count, testname, len(abis), abis)
|
fail_count = len(abis)
|
waived += [testname] * fail_count
|
logging.info('Waived failure for %s %d time(s)', testname, fail_count)
|
logging.info('Total waived = %s', waived)
|
return waived, accurate
|
|
|
def select_32bit_java():
|
"""Switches to 32 bit java if installed (like in lab lxc images) to save
|
about 30-40% server/shard memory during the run."""
|
if utils.is_in_container() and not client_utils.is_moblab():
|
java = '/usr/lib/jvm/java-8-openjdk-i386'
|
if os.path.exists(java):
|
logging.info('Found 32 bit java, switching to use it.')
|
os.environ['JAVA_HOME'] = java
|
os.environ['PATH'] = (
|
os.path.join(java, 'bin') + os.pathsep + os.environ['PATH'])
|