1 #!/usr/bin/python 2 3 # Copyright (C) 2009 The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 """Module for generating CTS test descriptions and test plans.""" 18 19 import glob 20 import os 21 import re 22 import subprocess 23 import sys 24 import xml.dom.minidom as dom 25 from cts import tools 26 from multiprocessing import Pool 27 28 def GetSubDirectories(root): 29 """Return all directories under the given root directory.""" 30 return [x for x in os.listdir(root) if os.path.isdir(os.path.join(root, x))] 31 32 33 def GetMakeFileVars(makefile_path): 34 """Extracts variable definitions from the given make file. 35 36 Args: 37 makefile_path: Path to the make file. 38 39 Returns: 40 A dictionary mapping variable names to their assigned value. 41 """ 42 result = {} 43 pattern = re.compile(r'^\s*([^:#=\s]+)\s*:=\s*(.*?[^\\])$', re.MULTILINE + re.DOTALL) 44 stream = open(makefile_path, 'r') 45 content = stream.read() 46 for match in pattern.finditer(content): 47 result[match.group(1)] = match.group(2) 48 stream.close() 49 return result 50 51 52 class CtsBuilder(object): 53 """Main class for generating test descriptions and test plans.""" 54 55 def __init__(self, argv): 56 """Initialize the CtsBuilder from command line arguments.""" 57 if not len(argv) == 6: 58 print 'Usage: %s <testRoot> <ctsOutputDir> <tempDir> <androidRootDir> <docletPath>' % argv[0] 59 print '' 60 print 'testRoot: Directory under which to search for CTS tests.' 61 print 'ctsOutputDir: Directory in which the CTS repository should be created.' 62 print 'tempDir: Directory to use for storing temporary files.' 63 print 'androidRootDir: Root directory of the Android source tree.' 64 print 'docletPath: Class path where the DescriptionGenerator doclet can be found.' 65 sys.exit(1) 66 self.test_root = sys.argv[1] 67 self.out_dir = sys.argv[2] 68 self.temp_dir = sys.argv[3] 69 self.android_root = sys.argv[4] 70 self.doclet_path = sys.argv[5] 71 72 self.test_repository = os.path.join(self.out_dir, 'repository/testcases') 73 self.plan_repository = os.path.join(self.out_dir, 'repository/plans') 74 75 def GenerateTestDescriptions(self): 76 """Generate test descriptions for all packages.""" 77 pool = Pool(processes=16) 78 79 # individually generate descriptions not following conventions 80 pool.apply_async(GenerateSignatureCheckDescription, [self.test_repository]) 81 pool.apply_async(GenerateReferenceAppDescription, [self.test_repository]) 82 pool.apply_async(GenerateAppSecurityDescription, [self.temp_dir, 83 self.test_repository, self.android_root, self.doclet_path]) 84 85 # generate test descriptions for android tests 86 android_packages = GetSubDirectories(self.test_root) 87 for package in android_packages: 88 pool.apply_async(GenerateTestDescription, [self.test_root, self.temp_dir, 89 self.test_repository, self.android_root, self.doclet_path, package]) 90 91 pool.close() 92 pool.join() 93 94 def __WritePlan(self, plan, plan_name): 95 print 'Generating test plan %s' % plan_name 96 plan.Write(os.path.join(self.plan_repository, plan_name + '.xml')) 97 98 def GenerateTestPlans(self): 99 """Generate default test plans.""" 100 # TODO: Instead of hard-coding the plans here, use a configuration file, 101 # such as test_defs.xml 102 packages = [] 103 descriptions = sorted(glob.glob(os.path.join(self.test_repository, '*.xml'))) 104 for description in descriptions: 105 doc = tools.XmlFile(description) 106 packages.append(doc.GetAttr('TestPackage', 'appPackageName')) 107 108 plan = tools.TestPlan(packages) 109 plan.Exclude(r'android\.core\.vm-tests-tf') 110 plan.Exclude('android\.performance.*') 111 self.__WritePlan(plan, 'CTS') 112 113 plan.Exclude('android\.core\.vm-tests') 114 plan.Exclude('android\.performance.*') 115 plan.Include(r'android\.core\.vm-tests-tf') 116 self.__WritePlan(plan, 'CTS-TF') 117 118 plan.Exclude(r'android\.tests\.sigtest') 119 plan.Exclude(r'android\.core.*') 120 self.__WritePlan(plan, 'Android') 121 122 plan = tools.TestPlan(packages) 123 plan.Include(r'android\.core\.tests.*') 124 self.__WritePlan(plan, 'Java') 125 126 plan = tools.TestPlan(packages) 127 plan.Include(r'android\.core\.vm-tests') 128 plan.Exclude(r'android\.core\.vm-tests-tf') 129 self.__WritePlan(plan, 'VM') 130 131 plan = tools.TestPlan(packages) 132 plan.Include(r'android\.core\.vm-tests-tf') 133 self.__WritePlan(plan, 'VM-TF') 134 135 plan = tools.TestPlan(packages) 136 plan.Include(r'android\.tests\.sigtest') 137 self.__WritePlan(plan, 'Signature') 138 139 plan = tools.TestPlan(packages) 140 plan.Include(r'android\.apidemos\.cts') 141 self.__WritePlan(plan, 'RefApp') 142 143 plan = tools.TestPlan(packages) 144 plan.Include(r'android\.tests\.appsecurity') 145 self.__WritePlan(plan, 'AppSecurity') 146 147 def LogGenerateDescription(name): 148 print 'Generating test description for package %s' % name 149 150 def GenerateSignatureCheckDescription(test_repository): 151 """Generate the test description for the signature check.""" 152 LogGenerateDescription('android.tests.sigtest') 153 package = tools.TestPackage('SignatureTest', 'android.tests.sigtest') 154 package.AddAttribute('appNameSpace', 'android.tests.sigtest') 155 package.AddAttribute('signatureCheck', 'true') 156 package.AddAttribute('runner', '.InstrumentationRunner') 157 package.AddTest('android.tests.sigtest.SignatureTest.signatureTest') 158 description = open(os.path.join(test_repository, 'SignatureTest.xml'), 'w') 159 package.WriteDescription(description) 160 description.close() 161 162 def GenerateReferenceAppDescription(test_repository): 163 """Generate the test description for the reference app tests.""" 164 LogGenerateDescription('android.apidemos.cts') 165 package = tools.TestPackage('ApiDemosReferenceTest', 'android.apidemos.cts') 166 package.AddAttribute('appNameSpace', 'android.apidemos.cts') 167 package.AddAttribute('packageToTest', 'com.example.android.apis') 168 package.AddAttribute('apkToTestName', 'ApiDemos') 169 package.AddAttribute('runner', 'android.test.InstrumentationTestRunner') 170 package.AddAttribute('referenceAppTest', 'true') 171 package.AddTest('android.apidemos.cts.ApiDemosTest.testNumberOfItemsInListView') 172 description = open(os.path.join(test_repository, 'ApiDemosReferenceTest.xml'), 'w') 173 package.WriteDescription(description) 174 description.close() 175 176 def GenerateAppSecurityDescription(temp_dir, test_repository, android_root, doclet_path): 177 """Generate the test description for the application security tests.""" 178 test_root = 'cts/tests/appsecurity-tests' 179 makefile_name = os.path.join(test_root, 'Android.mk') 180 makefile_vars = GetMakeFileVars(makefile_name) 181 name = makefile_vars['LOCAL_MODULE'] 182 package_name = 'android.tests.appsecurity' 183 LogGenerateDescription(package_name) 184 temp_desc = os.path.join(temp_dir, 'description.xml') 185 RunDescriptionGeneratorDoclet(android_root, doclet_path, 186 os.path.join(test_root, 'src'), temp_desc) 187 doc = dom.parse(temp_desc) 188 test_description = doc.getElementsByTagName('TestPackage')[0] 189 test_description.setAttribute('name', package_name) 190 test_description.setAttribute('appPackageName', package_name) 191 test_description.setAttribute('hostSideOnly', 'true') 192 test_description.setAttribute('jarPath', name + '.jar') 193 description = open(os.path.join(test_repository, package_name + '.xml'), 'w') 194 doc.writexml(description, addindent=' ', encoding='UTF-8') 195 description.close() 196 197 198 def GenerateTestDescription(test_root, temp_dir, test_repository, android_root, 199 doclet_path, package): 200 201 app_package_name = 'android.' + package 202 package_root = os.path.join(test_root, package) 203 204 makefile_name = os.path.join(package_root, 'Android.mk') 205 if not os.path.exists(makefile_name): 206 print 'Skipping directory "%s" due to missing Android.mk' % package_root 207 return 208 makefile_vars = GetMakeFileVars(makefile_name) 209 210 manifest_name = os.path.join(package_root, 'AndroidManifest.xml') 211 if not os.path.exists(manifest_name): 212 print 'Skipping directory "%s" due to missing AndroidManifest.xml' % package_root 213 return 214 manifest = tools.XmlFile(manifest_name) 215 216 LogGenerateDescription(app_package_name) 217 218 # Run the description generator doclet to get the test package structure 219 # TODO: The Doclet does not currently add all required attributes. Instead of rewriting 220 # the document below, additional attributes should be passed to the Doclet as arguments. 221 temp_desc = os.path.join(temp_dir, app_package_name + '-description.xml') 222 223 RunDescriptionGeneratorDoclet(android_root, doclet_path, package_root, temp_desc) 224 225 # obtain missing attribute values from the makefile and manifest 226 package_name = makefile_vars['LOCAL_PACKAGE_NAME'] 227 runner = manifest.GetAndroidAttr('instrumentation', 'name') 228 target_package = manifest.GetAndroidAttr('instrumentation', 'targetPackage') 229 target_binary_name = makefile_vars.get('LOCAL_INSTRUMENTATION_FOR') 230 231 # add them to the document 232 doc = dom.parse(temp_desc) 233 test_description = doc.getElementsByTagName('TestPackage')[0] 234 test_description.setAttribute('name', package_name) 235 test_description.setAttribute('runner', runner) 236 test_package = manifest.GetAttr('manifest', 'package') 237 test_description.setAttribute('appNameSpace', test_package) 238 test_description.setAttribute('appPackageName', app_package_name) 239 if not test_package == target_package: 240 test_description.setAttribute('targetNameSpace', target_package) 241 test_description.setAttribute('targetBinaryName', target_binary_name) 242 description = open(os.path.join(test_repository, package_name + '.xml'), 'w') 243 doc.writexml(description, addindent=' ', encoding='UTF-8') 244 description.close() 245 246 def RunDescriptionGeneratorDoclet(android_root, doclet_path, source_root, output_file): 247 """Generate a test package description by running the DescriptionGenerator doclet. 248 249 Args: 250 android_root: Root directory of the Android source tree. 251 doclet_path: Class path where the DescriptionGenerator doclet can be found. 252 source_root: Directory under which tests should be searched. 253 output_file: Name of the file where the description gets written. 254 255 Returns: 256 The exit code of the DescriptionGenerator doclet run. 257 """ 258 # Make sure sourceRoot is relative to self.android_root 259 source_root = RelPath(source_root, android_root) 260 261 # To determine whether a class is a JUnit test, the Doclet needs to have all intermediate 262 # subclasses of TestCase as well as the JUnit framework itself on the source path. 263 # Annotation classes are also required, since test annotations go into the description. 264 sourcepath = [ 265 'frameworks/base/core/java', # android test classes 266 'frameworks/base/test-runner/src', # test runner 267 'libcore/junit/src/main/java', # junit classes 268 'development/tools/hosttestlib/src', # hosttestlib TestCase extensions 269 'libcore/dalvik/src/main/java', # test annotations 270 'cts/tests/src', # cts test stubs 271 source_root # the source for this package 272 ] 273 sourcepath = [os.path.join(android_root, x) for x in sourcepath] 274 classpath = [ 275 'prebuilt/common/tradefed/tradefed-prebuilt.jar', 276 ] 277 classpath = [os.path.join(android_root, x) for x in classpath] 278 cmd = ('javadoc -o %s -J-Xmx512m -quiet -doclet DescriptionGenerator -docletpath %s' 279 ' -sourcepath %s -classpath %s ') % (output_file, doclet_path, ':'.join(sourcepath), 280 ':'.join(classpath)) 281 sources = [] 282 283 def AddFile(sources, folder, names): 284 """Find *.java.""" 285 sources.extend([os.path.join(folder, name) for name in names if name.endswith('.java')]) 286 287 os.path.walk(os.path.join(android_root, source_root), AddFile, sources) 288 cmd += ' '.join(sources) 289 proc = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 290 # read and discard any output 291 proc.communicate() 292 # wait for process to terminate and return exit value 293 return proc.wait() 294 295 def RelPath(path, start=os.getcwd()): 296 """Get a relative version of a path. 297 298 This is equivalent to os.path.relpath, which is only available since Python 2.6. 299 300 Args: 301 path: The path to transform. 302 start: The base path. Defaults to the current working directory. 303 304 Returns: 305 A transformed path that is relative to start. 306 """ 307 path_dirs = os.path.abspath(path).split(os.path.sep) 308 start_dirs = os.path.abspath(start).split(os.path.sep) 309 310 num_common = len(os.path.commonprefix([start_dirs, path_dirs])) 311 312 result_dirs = ['..'] * (len(start_dirs) - num_common) + path_dirs[num_common:] 313 if result_dirs: 314 return os.path.join(*result_dirs) 315 return start 316 317 if __name__ == '__main__': 318 builder = CtsBuilder(sys.argv) 319 builder.GenerateTestDescriptions() 320 builder.GenerateTestPlans() 321