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