blob: ef8b63bca29b787f37447e2b96988d9dd837139a [file] [log] [blame] [edit]
#!/usr/bin/env python2
# Copyright 2019 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.
from __future__ import print_function
import argparse
import contextlib
import copy
import logging
import os
import re
import shutil
import stat
import subprocess
import tempfile
import textwrap
import zipfile
# Use 'sudo pip install jinja2' to install.
from jinja2 import Template
# TODO(ihf): Assign better TIME to control files. Scheduling uses this to run
# LENGTHY first, then LONG, MEDIUM etc. But we need LENGTHY for the collect
# job, downgrade all others. Make sure this still works in CQ/smoke suite.
_CONTROLFILE_TEMPLATE = Template(
textwrap.dedent("""\
# Copyright {{year}} 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.
# This file has been automatically generated. Do not edit!
{%- if servo_support_needed %}
from autotest_lib.server import utils
{%- endif %}
AUTHOR = 'ARC++ Team'
NAME = '{{name}}'
ATTRIBUTES = '{{attributes}}'
DEPENDENCIES = '{{dependencies}}'
JOB_RETRIES = {{job_retries}}
TEST_TYPE = 'server'
TIME = '{{test_length}}'
MAX_RESULT_SIZE_KB = {{max_result_size_kb}}
{%- if sync_count and sync_count > 1 %}
SYNC_COUNT = {{sync_count}}
{%- endif %}
{%- if priority %}
PRIORITY = {{priority}}
{%- endif %}
DOC = '{{DOC}}'
{%- if servo_support_needed %}
# For local debugging, if your test setup doesn't have servo, REMOVE these
# two lines.
args_dict = utils.args_to_dict(args)
servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
{%- endif %}
{% if sync_count and sync_count > 1 %}
from autotest_lib.server import utils as server_utils
def {{test_func_name}}(ntuples):
host_list = [hosts.create_host(machine) for machine in ntuples]
{% else %}
def {{test_func_name}}(machine):
{%- if servo_support_needed %}
# REMOVE 'servo_args=servo_args' arg for local debugging if your test
# setup doesn't have servo.
try:
host_list = [hosts.create_host(machine, servo_args=servo_args)]
except:
# Just ignore any servo setup flakiness.
host_list = [hosts.create_host(machine)]
{%- else %}
host_list = [hosts.create_host(machine)]
{%- endif %}
{%- endif %}
job.run_test(
'{{base_name}}',
{%- if camera_facing %}
camera_facing='{{camera_facing}}',
cmdline_args=args,
{%- endif %}
hosts=host_list,
iterations=1,
{%- if max_retries != None %}
max_retry={{max_retries}},
{%- endif %}
{%- if enable_default_apps %}
enable_default_apps=True,
{%- endif %}
{%- if needs_push_media %}
needs_push_media={{needs_push_media}},
{%- endif %}
tag='{{tag}}',
test_name='{{name}}',
{%- if authkey %}
authkey='{{authkey}}',
{%- endif %}
run_template={{run_template}},
retry_template={{retry_template}},
target_module={% if target_module %}'{{target_module}}'{% else %}None{%endif%},
target_plan={% if target_plan %}'{{target_plan}}'{% else %}None{% endif %},
{%- if abi %}
bundle='{{abi}}',
{%- endif %}
{%- if extra_artifacts %}
extra_artifacts={{extra_artifacts}},
{%- endif %}
{%- if extra_artifacts_host %}
extra_artifacts_host={{extra_artifacts_host}},
{%- endif %}
{%- if uri %}
uri='{{uri}}',
{%- endif %}
{%- for arg in extra_args %}
{{arg}},
{%- endfor %}
{%- if servo_support_needed %}
hard_reboot_on_failure=True,
{%- endif %}
{%- if camera_facing %}
load_waivers=False,
{%- endif %}
timeout={{timeout}})
{% if sync_count and sync_count > 1 -%}
ntuples, failures = server_utils.form_ntuples_from_machines(machines,
SYNC_COUNT)
# Use log=False in parallel_simple to avoid an exception in setting up
# the incremental parser when SYNC_COUNT > 1.
parallel_simple({{test_func_name}}, ntuples, log=False)
{% else -%}
parallel_simple({{test_func_name}}, machines)
{% endif %}
"""))
CONFIG = None
_COLLECT = 'tradefed-run-collect-tests-only-internal'
_PUBLIC_COLLECT = 'tradefed-run-collect-tests-only'
_TEST_LENGTH = {1: 'FAST', 2: 'SHORT', 3: 'MEDIUM', 4: 'LONG', 5: 'LENGTHY'}
_ALL = 'all'
def get_tradefed_build(line):
"""Gets the build of Android CTS from tradefed.
@param line Tradefed identification output on startup. Example:
Android Compatibility Test Suite 7.0 (3423912)
@return Tradefed CTS build. Example: 2813453.
"""
# Sample string:
# - Android Compatibility Test Suite 7.0 (3423912)
# - Android Compatibility Test Suite for Instant Apps 1.0 (4898911)
# - Android Google Mobile Services (GMS) Test Suite 6.0_r1 (4756896)
m = re.search(r' \((.*)\)', line)
if m:
return m.group(1)
logging.warning('Could not identify build in line "%s".', line)
return '<unknown>'
def get_tradefed_revision(line):
"""Gets the revision of Android CTS from tradefed.
@param line Tradefed identification output on startup.
Example:
Android Compatibility Test Suite 6.0_r6 (2813453)
Android Compatibility Test Suite for Instant Apps 1.0 (4898911)
@return Tradefed CTS revision. Example: 6.0_r6.
"""
tradefed_identifier_list = [
r'Android Google Mobile Services \(GMS\) Test Suite (.*) \(',
r'Android Compatibility Test Suite(?: for Instant Apps)? (.*) \(',
r'Android Vendor Test Suite (.*) \(',
r'Android Security Test Suite (.*) \('
]
for identifier in tradefed_identifier_list:
m = re.search(identifier, line)
if m:
return m.group(1)
logging.warning('Could not identify revision in line "%s".', line)
return None
def get_bundle_abi(filename):
"""Makes an educated guess about the ABI.
In this case we chose to guess by filename, but we could also parse the
xml files in the module. (Maybe this needs to be done in the future.)
"""
if filename.endswith('arm.zip'):
return 'arm'
if filename.endswith('x86.zip'):
return 'x86'
assert(CONFIG['TRADEFED_CTS_COMMAND'] =='gts'), 'Only GTS has empty ABI'
return ''
def get_extension(module, abi, revision, is_public=False, led_provision=None, camera_facing=None):
"""Defines a unique string.
Notice we chose module revision first, then abi, as the module revision
changes regularly. This ordering makes it simpler to add/remove modules.
@param module: CTS module which will be tested in the control file. If 'all'
is specified, the control file will runs all the tests.
@param public: boolean variable to specify whether or not the bundle is from
public source or not.
@param led_provision: string or None indicate whether the camerabox has led
light or not.
@param camera_facing: string or None indicate whether it's camerabox tests
for specific camera facing or not.
@return string: unique string for specific tests. If public=True then the
string is "<abi>.<module>", otherwise, the unique string is
"<revision>.<abi>.<module>". Note that if abi is empty, the
abi part is omitted.
"""
ext_parts = []
if not is_public:
ext_parts = [revision]
if abi:
ext_parts += [abi]
ext_parts += [module]
if led_provision:
ext_parts += [led_provision]
if camera_facing:
ext_parts += ['camerabox', camera_facing]
return '.'.join(ext_parts)
def get_doc(modules, abi, is_public):
"""Defines the control file DOC string."""
if modules.intersection(get_collect_modules(is_public)):
module_text = 'all'
else:
# Generate per-module DOC
module_text = 'module ' + ', '.join(sorted(list(modules)))
abi_text = (' using %s ABI' % abi) if abi else ''
doc = ('Run %s of the %s%s in the ARC++ container.'
% (module_text, CONFIG['DOC_TITLE'], abi_text))
return doc
def servo_support_needed(modules, is_public=True):
"""Determines if servo support is needed for a module."""
return not is_public and all(module in CONFIG['NEEDS_POWER_CYCLE']
for module in modules)
def get_controlfile_name(module,
abi,
revision,
is_public=False,
led_provision=None,
camera_facing=None):
"""Defines the control file name.
@param module: CTS module which will be tested in the control file. If 'all'
is specified, the control file will runs all the tests.
@param public: boolean variable to specify whether or not the bundle is from
public source or not.
@param camera_facing: string or None indicate whether it's camerabox tests
for specific camera facing or not.
@param led_provision: string or None indicate whether the camerabox has led
light or not.
@return string: control file for specific tests. If public=True or
module=all, then the name will be "control.<abi>.<module>",
otherwise, the name will be
"control.<revision>.<abi>.<module>".
"""
return 'control.%s' % get_extension(module, abi, revision, is_public, led_provision,
camera_facing)
def get_sync_count(_modules, _abi, _is_public):
return 1
def get_suites(modules, abi, is_public, camera_facing=None):
"""Defines the suites associated with a module.
@param module: CTS module which will be tested in the control file. If 'all'
is specified, the control file will runs all the tests.
# TODO(ihf): Make this work with the "all" and "collect" generation,
# which currently bypass this function.
"""
if is_public:
# On moblab everything runs in the same suite.
return [CONFIG['MOBLAB_SUITE_NAME']]
suites = set(CONFIG['INTERNAL_SUITE_NAMES'])
for module in modules:
if module in get_collect_modules(is_public):
# We collect all tests both in arc-gts and arc-gts-qual as both have
# a chance to be complete (and used for submission).
suites |= set(CONFIG['QUAL_SUITE_NAMES'])
if module in CONFIG['EXTRA_ATTRIBUTES']:
# Special cases come with their own suite definitions.
suites |= set(CONFIG['EXTRA_ATTRIBUTES'][module])
if module in CONFIG['SMOKE'] and (abi == 'arm' or abi == ''):
# Handle VMTest by adding a few jobs to suite:smoke.
suites.add('suite:smoke')
if module in CONFIG['HARDWARE_DEPENDENT_MODULES']:
# CTS modules to be run on all unibuild models.
suites.add('suite:arc-cts-unibuild-hw')
if abi == 'x86':
# Handle a special builder for running all of CTS in a betty VM.
# TODO(ihf): figure out if this builder is still alive/needed.
vm_suite = None
for suite in CONFIG['VMTEST_INFO_SUITES']:
if not vm_suite:
vm_suite = suite
if module in CONFIG['VMTEST_INFO_SUITES'][suite]:
vm_suite = suite
if vm_suite is not None:
suites.add('suite:%s' % vm_suite)
# One or two modules hould be in suite:bvt-arc to cover CQ/PFQ. A few
# spare/fast modules can run in suite:bvt-perbuild in case we need a
# replacement for the module in suite:bvt-arc (integration test for
# cheets_CTS only, not a correctness test for CTS content).
if module in CONFIG['BVT_ARC'] and (abi == 'arm' or abi == ''):
suites.add('suite:bvt-arc')
elif module in CONFIG['BVT_PERBUILD'] and (abi == 'arm' or abi == ''):
suites.add('suite:bvt-perbuild')
if camera_facing != None:
suites.add('suite:arc-cts-camera')
return sorted(list(suites))
def get_dependencies(modules, abi, is_public, led_provision, camera_facing):
"""Defines lab dependencies needed to schedule a module.
@param module: CTS module which will be tested in the control file. If 'all'
is specified, the control file will runs all the tests.
@param abi: string that specifies the application binary interface of the
current test.
@param is_public: boolean variable to specify whether or not the bundle is
from public source or not.
@param led_provision: specify if led is provisioned in the camerabox setup. 'noled' when
there is no led light in the box and 'led' otherwise.
@param camera_facing: specify requirement of camerabox setup with target
test camera facing. Set to None if it's not camerabox
related test.
"""
dependencies = ['arc']
if abi in CONFIG['LAB_DEPENDENCY']:
dependencies += CONFIG['LAB_DEPENDENCY'][abi]
if led_provision is not None:
dependencies.append('camerabox_light:'+led_provision)
if camera_facing is not None:
dependencies.append('camerabox_facing:'+camera_facing)
for module in modules:
if is_public and module in CONFIG['PUBLIC_DEPENDENCIES']:
dependencies.extend(CONFIG['PUBLIC_DEPENDENCIES'][module])
return ', '.join(dependencies)
def get_job_retries(modules, is_public, suites):
"""Define the number of job retries associated with a module.
@param module: CTS module which will be tested in the control file. If a
special module is specified, the control file will runs all
the tests without retry.
@param is_public: true if the control file is for moblab (public) use.
@param suites: the list of suites that the control file belongs to.
"""
# TODO(haddowk): remove this when cts p has stabalized.
if is_public:
return CONFIG['CTS_JOB_RETRIES_IN_PUBLIC']
# Presubmit check forces to set 2 or more retries for CQ tests.
if 'suite:bvt-arc' in suites:
return 2
retries = 1 # 0 is NO job retries, 1 is one retry etc.
for module in modules:
# We don't want job retries for module collection or special cases.
if (module in get_collect_modules(is_public) or module == _ALL or
('CtsDeqpTestCases' in CONFIG['EXTRA_MODULES'] and
module in CONFIG['EXTRA_MODULES']['CtsDeqpTestCases']['SUBMODULES']
)):
retries = 0
return retries
def get_max_retries(modules, abi, suites, is_public):
"""Partners experiance issues where some modules are flaky and require more
retries. Calculate the retry number per module on moblab.
@param module: CTS module which will be tested in the control file.
"""
retry = -1
if is_public:
if _ALL in CONFIG['PUBLIC_MODULE_RETRY_COUNT']:
retry = CONFIG['PUBLIC_MODULE_RETRY_COUNT'][_ALL]
# In moblab at partners we may need many more retries than in lab.
for module in modules:
if module in CONFIG['PUBLIC_MODULE_RETRY_COUNT']:
retry = max(retry, CONFIG['PUBLIC_MODULE_RETRY_COUNT'][module])
else:
# See if we have any special values for the module, chose the largest.
for module in modules:
if module in CONFIG['CTS_MAX_RETRIES']:
retry = max(retry, CONFIG['CTS_MAX_RETRIES'][module])
# Ugly overrides.
# In bvt we don't want to hold the CQ/PFQ too long.
if 'suite:bvt-arc' in suites:
retry = 3
# Not strict as CQ for bvt-perbuild. Let per-module config take priority.
if retry == -1 and 'suite:bvt-perbuild' in suites:
retry = 3
# During qualification we want at least 9 retries, possibly more.
# TODO(kinaba&yoshiki): do not abuse suite names
if CONFIG.get('QUAL_SUITE_NAMES') and \
set(CONFIG['QUAL_SUITE_NAMES']) & set(suites):
retry = max(retry, CONFIG['CTS_QUAL_RETRIES'])
# Collection should never have a retry. This needs to be last.
if modules.intersection(get_collect_modules(is_public)):
retry = 0
if retry >= 0:
return retry
# Default case omits the retries in the control file, so tradefed_test.py
# can chose its own value.
return None
def get_max_result_size_kb(modules, is_public):
"""Returns the maximum expected result size in kB for autotest.
@param modules: List of CTS modules to be tested by the control file.
"""
for module in modules:
if (module in get_collect_modules(is_public) or
module == 'CtsDeqpTestCases'):
# CTS tests and dump logs for android-cts.
return CONFIG['LARGE_MAX_RESULT_SIZE']
# Individual module normal produces less results than all modules.
return CONFIG['NORMAL_MAX_RESULT_SIZE']
def get_extra_args(modules, is_public):
"""Generate a list of extra arguments to pass to the test.
Some params are specific to a particular module, particular mode or
combination of both, generate a list of arguments to pass into the template.
@param modules: List of CTS modules to be tested by the control file.
"""
extra_args = set()
preconditions = []
login_preconditions = []
prerequisites = []
for module in modules:
# Remove this once JDK9 is the base JDK for lab.
if CONFIG.get('USE_JDK9', False):
extra_args.add('use_jdk9=True')
if is_public:
extra_args.add('warn_on_test_retry=False')
extra_args.add('retry_manual_tests=True')
preconditions.extend(CONFIG['PUBLIC_PRECONDITION'].get(module, []))
else:
preconditions.extend(CONFIG['PRECONDITION'].get(module, []))
login_preconditions.extend(
CONFIG['LOGIN_PRECONDITION'].get(module, []))
prerequisites.extend(CONFIG['PREREQUISITES'].get(module,[]))
# Notice: we are just squishing the preconditions for all modules together
# with duplicated command removed. This may not always be correct.
# In such a case one should split the bookmarks in a way that the modules
# with conflicting preconditions end up in separate control files.
def deduped(lst):
"""Keep only the first occurrence of each element."""
return [e for i, e in enumerate(lst) if e not in lst[0:i]]
if preconditions:
# To properly escape the public preconditions we need to format the list
# manually using join.
extra_args.add('precondition_commands=[%s]' % ', '.join(
deduped(preconditions)))
if login_preconditions:
extra_args.add('login_precondition_commands=[%s]' % ', '.join(
deduped(login_preconditions)))
if prerequisites:
extra_args.add("prerequisites=['%s']" % "', '".join(
deduped(prerequisites)))
return sorted(list(extra_args))
def get_test_length(modules):
""" Calculate the test length based on the module name.
To better optimize DUT's connected to moblab, it is better to run the
longest tests and tests that require limited resources. For these modules
override from the default test length.
@param module: CTS module which will be tested in the control file. If 'all'
is specified, the control file will runs all the tests.
@return string: one of the specified test lengths:
['FAST', 'SHORT', 'MEDIUM', 'LONG', 'LENGTHY']
"""
length = 3 # 'MEDIUM'
for module in modules:
if module in CONFIG['OVERRIDE_TEST_LENGTH']:
length = max(length, CONFIG['OVERRIDE_TEST_LENGTH'][module])
return _TEST_LENGTH[length]
def get_test_priority(modules, is_public):
""" Calculate the test priority based on the module name.
On moblab run all long running tests and tests that have some unique
characteristic at a higher priority (50).
This optimizes the total run time of the suite assuring the shortest
time between suite kick off and 100% complete.
@param module: CTS module which will be tested in the control file.
@return int: 0 if priority not to be overridden, or priority number otherwise.
"""
if not is_public:
return 0
priority = 0
overide_test_priority_dict = CONFIG.get('PUBLIC_OVERRIDE_TEST_PRIORITY', {})
for module in modules:
if module in overide_test_priority_dict:
priority = max(priority, overide_test_priority_dict[module])
elif (module in CONFIG['OVERRIDE_TEST_LENGTH'] or
module in CONFIG['PUBLIC_DEPENDENCIES'] or
module in CONFIG['PUBLIC_PRECONDITION'] or
module.split('.')[0] in CONFIG['OVERRIDE_TEST_LENGTH']):
priority = max(priority, 50)
return priority
def get_authkey(is_public):
if is_public or not CONFIG['AUTHKEY']:
return None
return CONFIG['AUTHKEY']
def _format_collect_cmd(is_public, abi_to_run, retry):
"""Returns a list specifying tokens for tradefed to list all tests."""
if retry:
return None
cmd = ['run', 'commandAndExit', 'collect-tests-only']
if CONFIG['TRADEFED_DISABLE_REBOOT_ON_COLLECTION']:
cmd += ['--disable-reboot']
for m in CONFIG['MEDIA_MODULES']:
cmd.append('--module-arg')
cmd.append('%s:skip-media-download:true' % m)
if (not is_public and
not CONFIG.get('NEEDS_DYNAMIC_CONFIG_ON_COLLECTION', True)):
cmd.append('--dynamic-config-url=')
if abi_to_run:
cmd += ['--abi', abi_to_run]
return cmd
def _get_special_command_line(modules, _is_public):
"""This function allows us to split a module like Deqp into segments."""
cmd = []
for module in sorted(modules):
cmd += CONFIG['EXTRA_COMMANDLINE'].get(module, [])
return cmd
def _format_modules_cmd(is_public,
abi_to_run,
modules=None,
retry=False,
whole_module_set=None):
"""Returns list of command tokens for tradefed."""
if retry:
assert(CONFIG['TRADEFED_RETRY_COMMAND'] == 'cts' or
CONFIG['TRADEFED_RETRY_COMMAND'] == 'retry')
cmd = ['run', 'commandAndExit', CONFIG['TRADEFED_RETRY_COMMAND'],
'--retry', '{session_id}']
else:
# For runs create a logcat file for each individual failure.
cmd = ['run', 'commandAndExit', CONFIG['TRADEFED_CTS_COMMAND']]
special_cmd = _get_special_command_line(modules, is_public)
if special_cmd:
cmd.extend(special_cmd)
elif _ALL in modules:
pass
elif len(modules) == 1:
cmd += ['--module', list(modules)[0]]
else:
if whole_module_set is None:
assert (CONFIG['TRADEFED_CTS_COMMAND'] != 'cts-instant'), \
'cts-instant cannot include multiple modules'
# We run each module with its own --include-filter option.
# https://source.android.com/compatibility/cts/run
for module in sorted(modules):
cmd += ['--include-filter', module]
else:
# CTS-Instant does not support --include-filter due to
# its implementation detail. Instead, exclude the complement.
for module in whole_module_set - set(modules):
cmd += ['--exclude-filter', module]
# For runs create a logcat file for each individual failure.
# Not needed on moblab, nobody is going to look at them.
if (not modules.intersection(CONFIG['DISABLE_LOGCAT_ON_FAILURE']) and
not is_public and
CONFIG['TRADEFED_CTS_COMMAND'] != 'gts'):
cmd.append('--logcat-on-failure')
if CONFIG['TRADEFED_IGNORE_BUSINESS_LOGIC_FAILURE']:
cmd.append('--ignore-business-logic-failure')
if CONFIG['TRADEFED_DISABLE_REBOOT']:
cmd.append('--disable-reboot')
if (CONFIG['TRADEFED_MAY_SKIP_DEVICE_INFO'] and
not (modules.intersection(CONFIG['BVT_ARC'] + CONFIG['SMOKE'] +
CONFIG['NEEDS_DEVICE_INFO']))):
cmd.append('--skip-device-info')
if abi_to_run:
cmd += ['--abi', abi_to_run]
# If NEEDS_DYNAMIC_CONFIG is set, disable the feature except on the modules
# that explicitly set as needed.
if (not is_public and CONFIG.get('NEEDS_DYNAMIC_CONFIG') and
not modules.intersection(CONFIG['NEEDS_DYNAMIC_CONFIG'])):
cmd.append('--dynamic-config-url=')
return cmd
def get_run_template(modules,
is_public,
retry=False,
abi_to_run=None,
whole_module_set=None):
"""Command to run the modules specified by a control file."""
no_intersection = not modules.intersection(get_collect_modules(is_public))
collect_present = (_COLLECT in modules or _PUBLIC_COLLECT in modules)
all_present = _ALL in modules
if no_intersection or (all_present and not collect_present):
return _format_modules_cmd(is_public,
abi_to_run,
modules,
retry=retry,
whole_module_set=whole_module_set)
elif collect_present:
return _format_collect_cmd(is_public, abi_to_run, retry=retry)
return None
def get_retry_template(modules, is_public):
"""Command to retry the failed modules as specified by a control file."""
return get_run_template(modules, is_public, retry=True)
def get_extra_modules_dict(is_public, abi):
if not is_public:
return CONFIG['EXTRA_MODULES']
extra_modules = copy.deepcopy(CONFIG['PUBLIC_EXTRA_MODULES'])
if abi in CONFIG['EXTRA_SUBMODULE_OVERRIDE']:
for _, submodules in extra_modules.items():
for old, news in CONFIG['EXTRA_SUBMODULE_OVERRIDE'][abi].items():
submodules.remove(old)
submodules.extend(news)
return {
module: {
'SUBMODULES': submodules,
'SUITES': [CONFIG['MOBLAB_SUITE_NAME']],
} for module, submodules in extra_modules.items()
}
def get_extra_artifacts(modules):
artifacts = []
for module in modules:
if module in CONFIG['EXTRA_ARTIFACTS']:
artifacts += CONFIG['EXTRA_ARTIFACTS'][module]
return artifacts
def get_extra_artifacts_host(modules):
if not 'EXTRA_ARTIFACTS_HOST' in CONFIG:
return
artifacts = []
for module in modules:
if module in CONFIG['EXTRA_ARTIFACTS_HOST']:
artifacts += CONFIG['EXTRA_ARTIFACTS_HOST'][module]
return artifacts
def calculate_timeout(modules, suites):
"""Calculation for timeout of tradefed run.
Timeout is at least one hour, except if part of BVT_ARC.
Notice these do get adjusted dynamically by number of ABIs on the DUT.
"""
if 'suite:bvt-arc' in suites:
return int(3600 * CONFIG['BVT_TIMEOUT'])
if CONFIG.get('QUAL_SUITE_NAMES') and \
CONFIG.get('QUAL_TIMEOUT') and \
((set(CONFIG['QUAL_SUITE_NAMES']) & set(suites)) and \
not (_COLLECT in modules or _PUBLIC_COLLECT in modules)):
return int(3600 * CONFIG['QUAL_TIMEOUT'])
timeout = 0
# First module gets 1h (standard), all other half hour extra (heuristic).
default_timeout = int(3600 * CONFIG['CTS_TIMEOUT_DEFAULT'])
delta = default_timeout
for module in modules:
if module in CONFIG['CTS_TIMEOUT']:
# Modules that run very long are encoded here.
timeout += int(3600 * CONFIG['CTS_TIMEOUT'][module])
elif module.startswith('CtsDeqpTestCases.dEQP-VK.'):
# TODO: Optimize this temporary hack by reducing this value or
# setting appropriate values for each test if possible.
timeout = max(timeout, int(3600 * 12))
elif 'Jvmti' in module:
# We have too many of these modules and they run fast.
timeout += 300
else:
timeout += delta
delta = default_timeout // 2
return timeout
def needs_push_media(modules):
"""Oracle to determine if to push several GB of media files to DUT."""
if modules.intersection(set(CONFIG['NEEDS_PUSH_MEDIA'])):
return True
return False
def enable_default_apps(modules):
"""Oracle to determine if to enable default apps (eg. Files.app)."""
if modules.intersection(set(CONFIG['ENABLE_DEFAULT_APPS'])):
return True
return False
def get_controlfile_content(combined,
modules,
abi,
revision,
build,
uri,
suites=None,
is_public=False,
is_latest=False,
led_provision=None,
camera_facing=None,
whole_module_set=None):
"""Returns the text inside of a control file.
@param combined: name to use for this combination of modules.
@param modules: set of CTS modules which will be tested in the control
file. If 'all' is specified, the control file will run
all the tests.
"""
# We tag results with full revision now to get result directories containing
# the revision. This fits stainless/ better.
tag = '%s' % get_extension(combined, abi, revision, is_public, led_provision,
camera_facing)
# For test_that the NAME should be the same as for the control file name.
# We could try some trickery here to get shorter extensions for a default
# suite/ARM. But with the monthly uprevs this will quickly get confusing.
name = '%s.%s' % (CONFIG['TEST_NAME'], tag)
if not suites:
suites = get_suites(modules, abi, is_public, camera_facing)
attributes = ', '.join(suites)
uri = 'LATEST' if is_latest else (None if is_public else uri)
target_module = None
if (combined not in get_collect_modules(is_public) and combined != _ALL):
target_module = combined
for target, config in get_extra_modules_dict(is_public, abi).items():
if combined in config['SUBMODULES']:
target_module = target
return _CONTROLFILE_TEMPLATE.render(
year=CONFIG['COPYRIGHT_YEAR'],
name=name,
base_name=CONFIG['TEST_NAME'],
test_func_name=CONFIG['CONTROLFILE_TEST_FUNCTION_NAME'],
attributes=attributes,
dependencies=get_dependencies(modules, abi, is_public,
led_provision, camera_facing),
extra_artifacts=get_extra_artifacts(modules),
extra_artifacts_host=get_extra_artifacts_host(modules),
job_retries=get_job_retries(modules, is_public, suites),
max_result_size_kb=get_max_result_size_kb(modules, is_public),
revision=revision,
build=build,
abi=abi,
needs_push_media=needs_push_media(modules),
enable_default_apps=enable_default_apps(modules),
tag=tag,
uri=uri,
DOC=get_doc(modules, abi, is_public),
servo_support_needed=servo_support_needed(modules, is_public),
max_retries=get_max_retries(modules, abi, suites, is_public),
timeout=calculate_timeout(modules, suites),
run_template=get_run_template(modules,
is_public,
abi_to_run=CONFIG.get(
'REPRESENTATIVE_ABI',
{}).get(abi, None),
whole_module_set=whole_module_set),
retry_template=get_retry_template(modules, is_public),
target_module=target_module,
target_plan=None,
test_length=get_test_length(modules),
priority=get_test_priority(modules, is_public),
extra_args=get_extra_args(modules, is_public),
authkey=get_authkey(is_public),
sync_count=get_sync_count(modules, abi, is_public),
camera_facing=camera_facing)
def get_tradefed_data(path, is_public, abi):
"""Queries tradefed to provide us with a list of modules.
Notice that the parsing gets broken at times with major new CTS drops.
"""
tradefed = os.path.join(path, CONFIG['TRADEFED_EXECUTABLE_PATH'])
# Forgive me for I have sinned. Same as: chmod +x tradefed.
os.chmod(tradefed, os.stat(tradefed).st_mode | stat.S_IEXEC)
cmd_list = [tradefed, 'list', 'modules']
logging.info('Calling tradefed for list of modules.')
with open(os.devnull, 'w') as devnull:
# tradefed terminates itself if stdin is not a tty.
tradefed_output = subprocess.check_output(cmd_list, stdin=devnull)
# TODO(ihf): Get a tradefed command which terminates then refactor.
p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
modules = set()
build = '<unknown>'
line = ''
revision = None
is_in_intaractive_mode = True
# The process does not terminate, but we know the last test is vm-tests-tf.
while True:
line = p.stdout.readline().strip()
# Android Compatibility Test Suite 7.0 (3423912)
if (line.startswith('Android Compatibility Test Suite ')
or line.startswith('Android Google ')
or line.startswith('Android Vendor Test Suite')
or line.startswith('Android Security Test Suite')):
logging.info('Unpacking: %s.', line)
build = get_tradefed_build(line)
revision = get_tradefed_revision(line)
elif line.startswith('Non-interactive mode: '):
is_in_intaractive_mode = False
elif line.startswith('arm') or line.startswith('x86'):
# Newer CTS shows ABI-module pairs like "arm64-v8a CtsNetTestCases"
line = line.split()[1]
if line not in CONFIG.get('EXCLUDE_MODULES', []):
modules.add(line)
elif line.startswith('Cts'):
modules.add(line)
elif line.startswith('Gts'):
# Older GTS plainly lists the module names
modules.add(line)
elif line.startswith('cts-'):
modules.add(line)
elif line.startswith('signed-Cts'):
modules.add(line)
elif line.startswith('vm-tests-tf'):
modules.add(line)
break # TODO(ihf): Fix using this as EOS.
elif not line:
exit_code = p.poll()
if exit_code is not None:
# The process has automatically exited.
if is_in_intaractive_mode or exit_code != 0:
# The process exited unexpectedly in interactive mode,
# or exited with error in non-interactive mode.
logging.warning(
'The process has exited unexpectedly (exit code: %d)',
exit_code)
modules = set()
break
elif line.isspace() or line.startswith('Use "help"'):
pass
else:
logging.warning('Ignoring "%s"', line)
if p.poll() is None:
# Kill the process if alive.
p.kill()
p.wait()
if not modules:
raise Exception("no modules found.")
return list(modules), build, revision
def download(uri, destination):
"""Download |uri| to local |destination|.
|destination| must be a file path (not a directory path)."""
if uri.startswith('http://') or uri.startswith('https://'):
subprocess.check_call(['wget', uri, '-O', destination])
elif uri.startswith('gs://'):
subprocess.check_call(['gsutil', 'cp', uri, destination])
else:
raise Exception
@contextlib.contextmanager
def pushd(d):
"""Defines pushd."""
current = os.getcwd()
os.chdir(d)
try:
yield
finally:
os.chdir(current)
def unzip(filename, destination):
"""Unzips a zip file to the destination directory."""
with pushd(destination):
# We are trusting Android to have a valid zip file for us.
with zipfile.ZipFile(filename) as zf:
zf.extractall()
def get_collect_modules(is_public):
if is_public:
return set([_PUBLIC_COLLECT])
return set([_COLLECT])
@contextlib.contextmanager
def TemporaryDirectory(prefix):
"""Poor man's python 3.2 import."""
tmp = tempfile.mkdtemp(prefix=prefix)
try:
yield tmp
finally:
shutil.rmtree(tmp)
def get_word_pattern(m, l=1):
"""Return the first few words of the CamelCase module name.
Break after l+1 CamelCase word.
Example: CtsDebugTestCases -> CtsDebug.
"""
s = re.findall('^[a-z-]+|[A-Z]*[^A-Z0-9]*', m)[0:l + 1]
# Ignore Test or TestCases at the end as they don't add anything.
if len(s) > l:
if s[l].startswith('Test') or s[l].startswith('['):
return ''.join(s[0:l])
if s[l - 1] == 'Test' and s[l].startswith('Cases'):
return ''.join(s[0:l - 1])
return ''.join(s[0:l + 1])
def combine_modules_by_common_word(modules):
"""Returns a dictionary of (combined name, set of module) pairs.
This gives a mild compaction of control files (from about 320 to 135).
Example:
'CtsVoice' -> ['CtsVoiceInteractionTestCases', 'CtsVoiceSettingsTestCases']
"""
d = dict()
# On first pass group modules with common first word together.
for module in modules:
pattern = get_word_pattern(module)
v = d.get(pattern, [])
v.append(module)
v.sort()
d[pattern] = v
# Second pass extend names to maximum common prefix. This keeps control file
# names identical if they contain only one module and less ambiguous if they
# contain multiple modules.
combined = dict()
for key in sorted(d):
# Instead if a one syllable prefix use longest common prefix of modules.
prefix = os.path.commonprefix(d[key])
# Beautification: strip Tests/TestCases from end of prefix, but only if
# there is more than one module in the control file. This avoids
# slightly strange combination of having CtsDpiTestCases1/2 inside of
# CtsDpiTestCases (now just CtsDpi to make it clearer there are several
# modules in this control file).
if len(d[key]) > 1:
prefix = re.sub('TestCases$', '', prefix)
prefix = re.sub('Tests$', '', prefix)
# Beautification: CtsMedia files run very long and are unstable. Give
# each module its own control file, even though this heuristic would
# lump them together.
if prefix.startswith('CtsMedia'):
# Separate each CtsMedia* modules, but group extra modules with
# optional parametrization (ex: secondary_user, instant) together.
prev = ' '
for media in sorted(d[key]):
if media.startswith(prev):
combined[prev].add(media)
else:
prev = media
combined[media] = set([media])
else:
combined[prefix] = set(d[key])
print('Reduced number of control files from %d to %d.' % (len(modules),
len(combined)))
return combined
def combine_modules_by_bookmark(modules):
"""Return a manually curated list of name, module pairs.
Ideally we split "all" into a dictionary of maybe 10-20 equal runtime parts.
(Say 2-5 hours each.) But it is ok to run problematic modules alone.
"""
d = dict()
# Figure out sets of modules between bookmarks. Not optimum time complexity.
for bookmark in CONFIG['QUAL_BOOKMARKS']:
if modules:
for module in sorted(modules):
if module < bookmark:
v = d.get(bookmark, set())
v.add(module)
d[bookmark] = v
# Remove processed modules.
if bookmark in d:
modules = modules - d[bookmark]
# Clean up names.
combined = dict()
for key in sorted(d):
v = sorted(d[key])
# New name is first element '_-_' last element.
# Notice there is a bug in $ADB_VENDOR_KEYS path name preventing
# arbitrary characters.
prefix = v[0] + '_-_' + v[-1]
combined[prefix] = set(v)
return combined
def write_controlfile(name,
modules,
abi,
revision,
build,
uri,
suites,
is_public,
is_latest=False,
whole_module_set=None):
"""Write a single control file."""
filename = get_controlfile_name(name, abi, revision, is_public)
content = get_controlfile_content(name,
modules,
abi,
revision,
build,
uri,
suites,
is_public,
is_latest,
whole_module_set=whole_module_set)
with open(filename, 'w') as f:
f.write(content)
def write_moblab_controlfiles(modules, abi, revision, build, uri, is_public):
"""Write all control files for moblab.
Nothing gets combined.
Moblab uses one module per job. In some cases like Deqp which can run super
long it even creates several jobs per module. Moblab can do this as it has
less relative overhead spinning up jobs than the lab.
"""
for module in modules:
# No need to generate control files with extra suffix, since --module
# option will cover variants with optional parameters.
if "[" in module:
continue
write_controlfile(module, set([module]), abi, revision, build, uri,
[CONFIG['MOBLAB_SUITE_NAME']], is_public)
def write_regression_controlfiles(modules, abi, revision, build, uri,
is_public, is_latest):
"""Write all control files for stainless/ToT regression lab coverage.
Regression coverage on tot currently relies heavily on watching stainless
dashboard and sponge. So instead of running everything in a single run
we split CTS into many jobs. It used to be one job per module, but that
became too much in P (more than 300 per ABI). Instead we combine modules
with similar names and run these in the same job (alphabetically).
"""
combined = combine_modules_by_common_word(set(modules))
for key in combined:
write_controlfile(key, combined[key], abi, revision, build, uri, None,
is_public, is_latest)
def write_qualification_controlfiles(modules, abi, revision, build, uri,
is_public):
"""Write all control files to run "all" tests for qualification.
Qualification was performed on N by running all tests using tradefed
sharding (specifying SYNC_COUNT=2) in the control files. In skylab
this is currently not implemented, so we fall back to autotest sharding
all CTS tests into 10-20 hand chosen shards.
"""
combined = combine_modules_by_bookmark(set(modules))
for key in combined:
write_controlfile('all.' + key, combined[key], abi, revision, build,
uri, CONFIG.get('QUAL_SUITE_NAMES'), is_public)
def write_qualification_and_regression_controlfile(modules, abi, revision,
build, uri, is_public):
"""Write a control file to run "all" tests for qualification and regression.
"""
# For cts-instant, qualication control files are expected to cover
# regressions as well. Hence the 'suite:arc-cts' is added.
suites = ['suite:arc-cts', 'suite:arc-cts-qual']
module_set = set(modules)
combined = combine_modules_by_bookmark(module_set)
for key in combined:
write_controlfile('all.' + key,
combined[key],
abi,
revision,
build,
uri,
suites,
is_public,
whole_module_set=module_set)
def write_collect_controlfiles(_modules, abi, revision, build, uri, is_public,
is_latest):
"""Write all control files for test collection used as reference to
compute completeness (missing tests) on the CTS dashboard.
"""
if is_public:
suites = [CONFIG['MOBLAB_SUITE_NAME']]
else:
suites = CONFIG['INTERNAL_SUITE_NAMES'] \
+ CONFIG.get('QUAL_SUITE_NAMES', [])
for module in get_collect_modules(is_public):
write_controlfile(module, set([module]), abi, revision, build, uri,
suites, is_public, is_latest)
def write_extra_controlfiles(_modules, abi, revision, build, uri, is_public,
is_latest):
"""Write all extra control files as specified in config.
This is used by moblab to load balance large modules like Deqp, as well as
making custom modules such as WM presubmit. A similar approach was also used
during bringup of grunt to split media tests.
"""
for module, config in get_extra_modules_dict(is_public, abi).items():
for submodule in config['SUBMODULES']:
write_controlfile(submodule, set([submodule]), abi, revision,
build, uri, config['SUITES'], is_public,
is_latest)
def write_extra_camera_controlfiles(abi, revision, build, uri, is_public):
"""Control files for CtsCameraTestCases.camerabox.*"""
module = 'CtsCameraTestCases'
for facing in ['back', 'front']:
for led_provision in ['led', 'noled']:
name = get_controlfile_name(module, abi,
revision, is_public, led_provision, facing)
content = get_controlfile_content(module,
set([module]),
abi,
revision,
build,
uri,
None,
is_public,
led_provision=led_provision,
camera_facing=facing)
with open(name, 'w') as f:
f.write(content)
def run(uris, is_public, is_latest, cache_dir):
"""Downloads each bundle in |uris| and generates control files for each
module as reported to us by tradefed.
"""
for uri in uris:
abi = get_bundle_abi(uri)
# Get tradefed data by downloading & unzipping the files
with TemporaryDirectory(prefix='cts-android_') as tmp:
if cache_dir is not None:
assert(os.path.isdir(cache_dir))
bundle = os.path.join(cache_dir, os.path.basename(uri))
if not os.path.exists(bundle):
logging.info('Downloading to %s.', cache_dir)
download(uri, bundle)
else:
bundle = os.path.join(tmp, os.path.basename(uri))
logging.info('Downloading to %s.', tmp)
download(uri, bundle)
logging.info('Extracting %s.', bundle)
unzip(bundle, tmp)
modules, build, revision = get_tradefed_data(tmp, is_public, abi)
if not revision:
raise Exception('Could not determine revision.')
logging.info('Writing all control files.')
if is_public:
write_moblab_controlfiles(modules, abi, revision, build, uri,
is_public)
else:
if CONFIG['CONTROLFILE_WRITE_SIMPLE_QUAL_AND_REGRESS']:
write_qualification_and_regression_controlfile(
modules, abi, revision, build, uri, is_public)
else:
write_regression_controlfiles(modules, abi, revision,
build, uri, is_public,
is_latest)
write_qualification_controlfiles(modules, abi, revision,
build, uri, is_public)
if CONFIG['CONTROLFILE_WRITE_CAMERA']:
write_extra_camera_controlfiles(abi, revision, build, uri,
is_public)
write_collect_controlfiles(modules, abi, revision, build, uri,
is_public, is_latest)
if CONFIG['CONTROLFILE_WRITE_EXTRA']:
write_extra_controlfiles(None, abi, revision, build, uri,
is_public, is_latest)
def main(config):
""" Entry method of generator """
global CONFIG
CONFIG = config
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser(
description='Create control files for a CTS bundle on GS.',
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
'uris',
nargs='+',
help='List of Google Storage URIs to CTS bundles. Example:\n'
'gs://chromeos-arc-images/cts/bundle/P/'
'android-cts-9.0_r9-linux_x86-x86.zip')
parser.add_argument(
'--is_public',
dest='is_public',
default=False,
action='store_true',
help='Generate the public control files for CTS, default generate'
' the internal control files')
parser.add_argument(
'--is_latest',
dest='is_latest',
default=False,
action='store_true',
help='Generate the control files for CTS from the latest CTS bundle'
' stored in the internal storage')
parser.add_argument(
'--cache_dir',
dest='cache_dir',
default=None,
action='store',
help='Cache directory for downloaded bundle file. Uses the cached '
'bundle file if exists, or caches a downloaded file to this '
'directory if not.')
args = parser.parse_args()
run(args.uris, args.is_public, args.is_latest, args.cache_dir)