blob: 4d4b94a2b7bb43f50fc136546e16d3ce40eddc73 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2016 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 argparse
import contextlib
import logging
import os
import re
import shutil
import stat
import subprocess
import tempfile
import zipfile
_CONTROLFILE_TEMPLATE = """\
# Copyright 2016 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!
AUTHOR = 'ARC Team'
NAME = '{name}'
ATTRIBUTES = '{attributes}'
DEPENDENCIES = '{dependencies}'
JOB_RETRIES = {retries}
TEST_TYPE = 'server'
TIME = 'LENGTHY'
DOC = ('Run package {package} of the '
'Android {revision} Compatibility Test Suite (CTS), build {build},'
'using {abi} ABI in the ARC container.')
def run_CTS(machine):
host = hosts.create_host(machine)
job.run_test(
'cheets_CTS',
host=host,
iterations=1,{retry}
tag='{tag}',
target_package='{package}',
{source},
timeout={timeout})
parallel_simple(run_CTS, machines)"""
_CTS_TIMEOUT = {
'android.core.tests.libcore.package.org': 90,
'android.core.vm-tests-tf': 90,
'android.host.security': 90,
'android.media': 360,
'android.mediastress': 480,
'android.print': 180,
'com.android.cts.filesystemperf': 180,
'com.drawelements.deqp.gles3': 240,
'com.drawelements.deqp.gles31': 90,
'com.drawelements.deqp.gles31.copy_image_mixed': 120,
'com.drawelements.deqp.gles31.copy_image_non_compressed': 240,
}
_SUITE_SMOKE = [
'android.core.tests.libcore.package.harmony_java_math'
]
# Any test in SMOKE (VMTest) should also be in CQ (HWTest).
_SUITE_BVT_ARC = _SUITE_SMOKE + [
'com.android.cts.dram'
]
_SUITE_BVT_CQ = _SUITE_BVT_ARC
_SUITE_BVT_PERBUILD = [
'android.signature',
'android.speech',
'android.systemui',
'android.telecom',
'android.telephony',
'android.theme',
'android.transition',
'android.tv',
'android.uiautomation',
'android.usb',
'android.voicesettings',
'com.android.cts.filesystemperf',
'com.android.cts.jank',
'com.android.cts.opengl',
'com.android.cts.simplecpu',
]
def get_tradefed_build(line):
"""Gets the build of Android CTS from tradefed.
@param line Tradefed identification output on startup. Example:
Android CTS 6.0_r6 build:2813453
@return Tradefed CTS build. Example: 2813453.
"""
# Sample string: Android CTS 6.0_r6 build:2813453.
m = re.search(r'(?<=build:)(.*)', line)
if m:
return m.group(0)
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 CTS 6.0_r6 build:2813453
@return Tradefed CTS revision. Example: 6.0_r6.
"""
m = re.search(r'(?<=Android CTS )(.*) build:', 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 package. (Maybe this needs to be done in the future.)
"""
if filename.endswith('_x86-arm.zip'):
return 'arm'
if filename.endswith('_x86-x86.zip'):
return 'x86'
raise Exception('Could not determine ABI from "%s".' % filename)
def get_bundle_revision(filename):
"""Makes an educated guess about the revision.
In this case we chose to guess by filename, but we could also parse the
xml files in the package.
"""
m = re.search(r'(?<=android-cts-)(.*)-linux', filename)
if m is not None:
return m.group(1)
return None
def get_extension(package, abi, revision):
"""Defines a unique string.
Notice we chose package revision first, then abi, as the package revision
changes at least on a monthly basis. This ordering makes it simpler to
add/remove packages.
"""
return '%s.%s.%s' % (revision, abi, package)
def get_controlfile_name(package, abi, revision):
"""Defines the control file name."""
return 'control.%s' % get_extension(package, abi, revision)
def get_public_extension(package, abi):
return '%s.%s' % (abi, package)
def get_public_controlfile_name(package, abi):
"""Defines the public control file name."""
return 'control.%s' % get_public_extension(package, abi)
def get_attribute_suites(package, abi):
"""Defines the suites associated with a package."""
attributes = 'suite:arc-cts'
# Get a minmum amount of coverage on cq (one quick CTS package only).
if package in _SUITE_SMOKE:
attributes += ', suite:smoke'
if package in _SUITE_BVT_CQ:
attributes += ', suite:bvt-cq'
if package in _SUITE_BVT_ARC:
attributes += ', suite:bvt-arc, suite:arc-bvt-cq'
if package in _SUITE_BVT_PERBUILD and abi == 'arm':
attributes += ', suite:bvt-perbuild'
# Adding arc-cts-stable runs all packages twice on stable.
return attributes
def get_dependencies(abi):
"""Defines lab dependencies needed to schedule a package.
Currently we only care about x86 ABI tests, which must run on Intel boards.
"""
# TODO(ihf): Enable this once crbug.com/618509 has labeled all lab DUTs.
if abi == 'x86':
return 'arc, cts_abi_x86'
return 'arc'
# suite_scheduler retries
_RETRIES = 2
def get_controlfile_content(package, abi, revision, build, uri):
"""Returns the text inside of a control file."""
# 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 = 'cheets_CTS.%s' % get_extension(package, abi, revision)
if is_public(uri):
name = 'cheets_CTS.%s' % get_public_extension(package, abi)
dependencies = get_dependencies(abi)
retries = _RETRIES
# We put all revisions and all abi in the same tag for wmatrix, otherwise
# the list at https://wmatrix.googleplex.com/tests would grow way too large.
# Example: for all values of |REVISION| and |ABI| we map control file
# cheets_CTS.REVISION.ABI.PACKAGE on wmatrix to cheets_CTS.PACKAGE.
# This way people can find results independent of |REVISION| and |ABI| at
# the same URL.
tag = package
timeout = 3600
if package in _CTS_TIMEOUT:
timeout = 60 * _CTS_TIMEOUT[package]
if is_public(uri):
source = "bundle='%s'" % abi
attributes = 'suite:cts'
else:
source = "uri='%s'" % uri
attributes = '%s, test_name:%s' % (get_attribute_suites(package, abi),
name)
# cheets_CTS internal retries limited due to time constraints on cq.
retry = ''
if (package in (_SUITE_SMOKE + _SUITE_BVT_CQ + _SUITE_BVT_ARC) or
package in _SUITE_BVT_PERBUILD and abi == 'arm'):
retry = '\n max_retry=3,'
return _CONTROLFILE_TEMPLATE.format(
name=name,
attributes=attributes,
dependencies=dependencies,
retries=retries,
package=package,
revision=revision,
build=build,
abi=abi,
tag=tag,
source=source,
retry=retry,
timeout=timeout)
def get_tradefed_data(path):
"""Queries tradefed to provide us with a list of packages."""
tradefed = os.path.join(path, 'android-cts/tools/cts-tradefed')
# 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', 'packages']
logging.info('Calling tradefed for list of packages.')
# TODO(ihf): Get a tradefed command which terminates then refactor.
p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
packages = []
build = '<unknown>'
line = ''
revision = None
# The process does not terminate, but we know the last test starts with zzz.
while not line.startswith('zzz'):
line = p.stdout.readline().strip()
if line:
print line
if line.startswith('Android CTS '):
logging.info('Unpacking: %s.', line)
build = get_tradefed_build(line)
revision = get_tradefed_revision(line)
elif re.search(r'^(android\.|com\.|zzz\.)', line):
packages.append(line)
p.kill()
p.wait()
return packages, build, revision
def is_public(uri):
if uri.startswith('gs:'):
return False
elif uri.startswith('https://dl.google.com'):
return True
raise
def download(uri, destination):
"""Download |uri| to local |destination|."""
if is_public(uri):
subprocess.check_call(['wget', uri, '-P', destination])
else:
subprocess.check_call(['gsutil', 'cp', uri, destination])
@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 sane zip file for us.
with zipfile.ZipFile(filename) as zf:
zf.extractall()
@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 main(uris):
"""Downloads each package in |uris| and generates control files."""
for uri in uris:
abi = get_bundle_abi(uri)
with TemporaryDirectory(prefix='cts-android_') as tmp:
logging.info('Downloading to %s.', tmp)
bundle = os.path.join(tmp, 'cts-android.zip')
download(uri, tmp)
bundle = os.path.join(tmp, os.path.basename(uri))
logging.info('Extracting %s.', bundle)
unzip(bundle, tmp)
packages, build, revision = get_tradefed_data(tmp)
if not revision:
raise Exception('Could not determine revision.')
# And write all control files.
logging.info('Writing all control files.')
for package in packages:
name = get_controlfile_name(package, abi, revision)
if is_public(uri):
name = get_public_controlfile_name(package, abi)
content = get_controlfile_content(package, abi, revision, build,
uri)
with open(name, 'w') as f:
f.write(content)
if __name__ == '__main__':
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/2016-06-02/'
'android-cts-6.0_r6-linux_x86-arm.zip')
args = parser.parse_args()
main(args.uris)