Home | History | Annotate | Download | only in cheets_CTS
      1 #!/usr/bin/env python
      2 # Copyright 2016 The Chromium OS Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 import argparse
      7 import contextlib
      8 import logging
      9 import os
     10 import re
     11 import shutil
     12 import stat
     13 import subprocess
     14 import tempfile
     15 import zipfile
     16 
     17 _CONTROLFILE_TEMPLATE = """\
     18 # Copyright 2016 The Chromium OS Authors. All rights reserved.
     19 # Use of this source code is governed by a BSD-style license that can be
     20 # found in the LICENSE file.
     21 
     22 # This file has been automatically generated. Do not edit!
     23 
     24 AUTHOR = 'ARC Team'
     25 NAME = '{name}'
     26 ATTRIBUTES = '{attributes}'
     27 DEPENDENCIES = '{dependencies}'
     28 JOB_RETRIES = {retries}
     29 TEST_TYPE = 'server'
     30 TIME = 'LENGTHY'
     31 
     32 DOC = ('Run package {package} of the '
     33        'Android {revision} Compatibility Test Suite (CTS), build {build},'
     34        'using {abi} ABI in the ARC container.')
     35 
     36 def run_CTS(machine):
     37     host = hosts.create_host(machine)
     38     job.run_test(
     39         'cheets_CTS',
     40         host=host,
     41         iterations=1,{retry}
     42         tag='{tag}',
     43         target_package='{package}',
     44         {source},
     45         timeout={timeout})
     46 
     47 parallel_simple(run_CTS, machines)"""
     48 
     49 _CTS_TIMEOUT = {
     50     'android.core.tests.libcore.package.org': 90,
     51     'android.core.vm-tests-tf': 90,
     52     'android.host.security': 90,
     53     'android.media': 360,
     54     'android.mediastress': 480,
     55     'android.print': 180,
     56     'com.android.cts.filesystemperf': 180,
     57     'com.drawelements.deqp.gles3': 240,
     58     'com.drawelements.deqp.gles31': 90,
     59     'com.drawelements.deqp.gles31.copy_image_mixed': 120,
     60     'com.drawelements.deqp.gles31.copy_image_non_compressed': 240,
     61 }
     62 
     63 _SUITE_SMOKE = [
     64     'android.core.tests.libcore.package.harmony_java_math'
     65 ]
     66 
     67 # Any test in SMOKE (VMTest) should also be in CQ (HWTest).
     68 _SUITE_BVT_ARC = _SUITE_SMOKE + [
     69     'com.android.cts.dram'
     70 ]
     71 
     72 _SUITE_BVT_PERBUILD = [
     73     'android.signature',
     74     'android.speech',
     75     'android.systemui',
     76     'android.telecom',
     77     'android.telephony',
     78     'android.theme',
     79     'android.transition',
     80     'android.tv',
     81     'android.uiautomation',
     82     'android.usb',
     83     'android.voicesettings',
     84     'com.android.cts.filesystemperf',
     85     'com.android.cts.jank',
     86     'com.android.cts.opengl',
     87     'com.android.cts.simplecpu',
     88 ]
     89 
     90 def get_tradefed_build(line):
     91     """Gets the build of Android CTS from tradefed.
     92 
     93     @param line Tradefed identification output on startup. Example:
     94                 Android CTS 6.0_r6 build:2813453
     95     @return Tradefed CTS build. Example: 2813453.
     96     """
     97     # Sample string: Android CTS 6.0_r6 build:2813453.
     98     m = re.search(r'(?<=build:)(.*)', line)
     99     if m:
    100         return m.group(0)
    101     logging.warning('Could not identify build in line "%s".', line)
    102     return '<unknown>'
    103 
    104 
    105 def get_tradefed_revision(line):
    106     """Gets the revision of Android CTS from tradefed.
    107 
    108     @param line Tradefed identification output on startup. Example:
    109                 Android CTS 6.0_r6 build:2813453
    110     @return Tradefed CTS revision. Example: 6.0_r6.
    111     """
    112     m = re.search(r'(?<=Android CTS )(.*) build:', line)
    113     if m:
    114         return m.group(1)
    115     logging.warning('Could not identify revision in line "%s".', line)
    116     return None
    117 
    118 
    119 def get_bundle_abi(filename):
    120     """Makes an educated guess about the ABI.
    121 
    122     In this case we chose to guess by filename, but we could also parse the
    123     xml files in the package. (Maybe this needs to be done in the future.)
    124     """
    125     if filename.endswith('_x86-arm.zip'):
    126         return 'arm'
    127     if filename.endswith('_x86-x86.zip'):
    128         return 'x86'
    129     raise Exception('Could not determine ABI from "%s".' % filename)
    130 
    131 
    132 def get_bundle_revision(filename):
    133     """Makes an educated guess about the revision.
    134 
    135     In this case we chose to guess by filename, but we could also parse the
    136     xml files in the package.
    137     """
    138     m = re.search(r'(?<=android-cts-)(.*)-linux', filename)
    139     if m is not None:
    140         return m.group(1)
    141     return None
    142 
    143 
    144 def get_extension(package, abi, revision):
    145     """Defines a unique string.
    146 
    147     Notice we chose package revision first, then abi, as the package revision
    148     changes at least on a monthly basis. This ordering makes it simpler to
    149     add/remove packages.
    150     """
    151     return '%s.%s.%s' % (revision, abi, package)
    152 
    153 
    154 def get_controlfile_name(package, abi, revision):
    155     """Defines the control file name."""
    156     return 'control.%s' % get_extension(package, abi, revision)
    157 
    158 
    159 def get_public_extension(package, abi):
    160     return '%s.%s' % (abi, package)
    161 
    162 
    163 def get_public_controlfile_name(package, abi):
    164     """Defines the public control file name."""
    165     return 'control.%s' % get_public_extension(package, abi)
    166 
    167 
    168 def get_attribute_suites(package, abi):
    169     """Defines the suites associated with a package."""
    170     attributes = 'suite:arc-cts'
    171     # Get a minmum amount of coverage on cq (one quick CTS package only).
    172     if package in _SUITE_SMOKE:
    173         attributes += ', suite:smoke'
    174     if package in _SUITE_BVT_ARC:
    175         attributes += ', suite:bvt-arc'
    176     if package in _SUITE_BVT_PERBUILD and abi == 'arm':
    177         attributes += ', suite:bvt-perbuild'
    178     # Adding arc-cts-stable runs all packages twice on stable.
    179     return attributes
    180 
    181 
    182 def get_dependencies(abi):
    183     """Defines lab dependencies needed to schedule a package.
    184 
    185     Currently we only care about x86 ABI tests, which must run on Intel boards.
    186     """
    187     # TODO(ihf): Enable this once crbug.com/618509 has labeled all lab DUTs.
    188     if abi == 'x86':
    189         return 'arc, cts_abi_x86'
    190     return 'arc'
    191 
    192 # suite_scheduler retries
    193 _RETRIES = 2
    194 
    195 
    196 def get_controlfile_content(package, abi, revision, build, uri):
    197     """Returns the text inside of a control file."""
    198     # For test_that the NAME should be the same as for the control file name.
    199     # We could try some trickery here to get shorter extensions for a default
    200     # suite/ARM. But with the monthly uprevs this will quickly get confusing.
    201     name = 'cheets_CTS.%s' % get_extension(package, abi, revision)
    202     if is_public(uri):
    203         name = 'cheets_CTS.%s' % get_public_extension(package, abi)
    204     dependencies = get_dependencies(abi)
    205     retries = _RETRIES
    206     # We put all revisions and all abi in the same tag for wmatrix, otherwise
    207     # the list at https://wmatrix.googleplex.com/tests would grow way too large.
    208     # Example: for all values of |REVISION| and |ABI| we map control file
    209     # cheets_CTS.REVISION.ABI.PACKAGE on wmatrix to cheets_CTS.PACKAGE.
    210     # This way people can find results independent of |REVISION| and |ABI| at
    211     # the same URL.
    212     tag = package
    213     timeout = 3600
    214     if package in _CTS_TIMEOUT:
    215         timeout = 60 * _CTS_TIMEOUT[package]
    216     if is_public(uri):
    217         source = "bundle='%s'" % abi
    218         attributes = 'suite:cts'
    219     else:
    220         source = "uri='%s'" % uri
    221         attributes = '%s, test_name:%s' % (get_attribute_suites(package, abi),
    222                                            name)
    223     # cheets_CTS internal retries limited due to time constraints on cq.
    224     retry = ''
    225     if (package in (_SUITE_SMOKE + _SUITE_BVT_ARC) or
    226         package in _SUITE_BVT_PERBUILD and abi == 'arm'):
    227         retry = '\n        max_retry=3,'
    228     return _CONTROLFILE_TEMPLATE.format(
    229         name=name,
    230         attributes=attributes,
    231         dependencies=dependencies,
    232         retries=retries,
    233         package=package,
    234         revision=revision,
    235         build=build,
    236         abi=abi,
    237         tag=tag,
    238         source=source,
    239         retry=retry,
    240         timeout=timeout)
    241 
    242 
    243 def get_tradefed_data(path):
    244     """Queries tradefed to provide us with a list of packages."""
    245     tradefed = os.path.join(path, 'android-cts/tools/cts-tradefed')
    246     # Forgive me for I have sinned. Same as: chmod +x tradefed.
    247     os.chmod(tradefed, os.stat(tradefed).st_mode | stat.S_IEXEC)
    248     cmd_list = [tradefed, 'list', 'packages']
    249     logging.info('Calling tradefed for list of packages.')
    250     # TODO(ihf): Get a tradefed command which terminates then refactor.
    251     p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
    252     packages = []
    253     build = '<unknown>'
    254     line = ''
    255     revision = None
    256     # The process does not terminate, but we know the last test starts with zzz.
    257     while not line.startswith('zzz'):
    258         line = p.stdout.readline().strip()
    259         if line:
    260             print line
    261         if line.startswith('Android CTS '):
    262             logging.info('Unpacking: %s.', line)
    263             build = get_tradefed_build(line)
    264             revision = get_tradefed_revision(line)
    265         elif re.search(r'^(android\.|com\.|zzz\.)', line):
    266             packages.append(line)
    267     p.kill()
    268     p.wait()
    269     return packages, build, revision
    270 
    271 
    272 def is_public(uri):
    273     if uri.startswith('gs:'):
    274         return False
    275     elif uri.startswith('https://dl.google.com'):
    276         return True
    277     raise
    278 
    279 
    280 def download(uri, destination):
    281     """Download |uri| to local |destination|."""
    282     if is_public(uri):
    283         subprocess.check_call(['wget', uri, '-P', destination])
    284     else:
    285         subprocess.check_call(['gsutil', 'cp', uri, destination])
    286 
    287 
    288 @contextlib.contextmanager
    289 def pushd(d):
    290     """Defines pushd."""
    291     current = os.getcwd()
    292     os.chdir(d)
    293     try:
    294         yield
    295     finally:
    296         os.chdir(current)
    297 
    298 
    299 def unzip(filename, destination):
    300     """Unzips a zip file to the destination directory."""
    301     with pushd(destination):
    302         # We are trusting Android to have a sane zip file for us.
    303         with zipfile.ZipFile(filename) as zf:
    304             zf.extractall()
    305 
    306 
    307 @contextlib.contextmanager
    308 def TemporaryDirectory(prefix):
    309     """Poor man's python 3.2 import."""
    310     tmp = tempfile.mkdtemp(prefix=prefix)
    311     try:
    312         yield tmp
    313     finally:
    314         shutil.rmtree(tmp)
    315 
    316 
    317 def main(uris):
    318     """Downloads each package in |uris| and generates control files."""
    319     for uri in uris:
    320         abi = get_bundle_abi(uri)
    321         with TemporaryDirectory(prefix='cts-android_') as tmp:
    322             logging.info('Downloading to %s.', tmp)
    323             bundle = os.path.join(tmp, 'cts-android.zip')
    324             download(uri, tmp)
    325             bundle = os.path.join(tmp, os.path.basename(uri))
    326             logging.info('Extracting %s.', bundle)
    327             unzip(bundle, tmp)
    328             packages, build, revision = get_tradefed_data(tmp)
    329             if not revision:
    330                 raise Exception('Could not determine revision.')
    331             # And write all control files.
    332             logging.info('Writing all control files.')
    333             for package in packages:
    334                 name = get_controlfile_name(package, abi, revision)
    335                 if is_public(uri):
    336                     name = get_public_controlfile_name(package, abi)
    337                 content = get_controlfile_content(package, abi, revision, build,
    338                                                   uri)
    339                 with open(name, 'w') as f:
    340                     f.write(content)
    341 
    342 
    343 if __name__ == '__main__':
    344     logging.basicConfig(level=logging.INFO)
    345     parser = argparse.ArgumentParser(
    346         description='Create control files for a CTS bundle on GS.',
    347         formatter_class=argparse.RawTextHelpFormatter)
    348     parser.add_argument(
    349         'uris',
    350         nargs='+',
    351         help='List of Google Storage URIs to CTS bundles. Example:\n'
    352         'gs://chromeos-arc-images/cts/bundle/2016-06-02/'
    353         'android-cts-6.0_r6-linux_x86-arm.zip')
    354     args = parser.parse_args()
    355     main(args.uris)
    356