| # 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']) |