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 19import glob 20import os 21import re 22import subprocess 23import sys 24import xml.dom.minidom as dom 25from cts import tools 26 27 28def 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 33def 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 52class 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 __LogGenerateDescription(self, name): 76 print 'Generating test description for package %s' % name 77 78 def RunDescriptionGeneratorDoclet(self, source_root, output_file): 79 """Generate a test package description by running the DescriptionGenerator doclet. 80 81 Args: 82 source_root: Directory under which tests should be searched. 83 output_file: Name of the file where the description gets written. 84 85 Returns: 86 The exit code of the DescriptionGenerator doclet run. 87 """ 88 # Make sure sourceRoot is relative to self.android_root 89 source_root = self.RelPath(source_root, self.android_root) 90 91 # To determine whether a class is a JUnit test, the Doclet needs to have all intermediate 92 # subclasses of TestCase as well as the JUnit framework itself on the source path. 93 # Annotation classes are also required, since test annotations go into the description. 94 source_path = [ 95 'frameworks/base/core/java', # android test classes 96 'frameworks/base/test-runner', # test runner 97 'dalvik/libcore/junit/src/main/java', # junit classes 98 'development/tools/hosttestlib/src', # hosttestlib TestCase extensions 99 'dalvik/libcore/dalvik/src/main/java', # test annotations 100 'cts/tests/src', # cts test stubs 101 source_root # the source for this package 102 ] 103 source_path = [os.path.join(self.android_root, x) for x in source_path] 104 cmd = ('javadoc -o %s -J-Xmx512m -quiet -doclet DescriptionGenerator -docletpath %s' 105 ' -sourcepath %s ') % (output_file, self.doclet_path, ':'.join(source_path)) 106 sources = [] 107 108 def AddFile(sources, folder, names): 109 """Find *.java.""" 110 sources.extend([os.path.join(folder, name) for name in names if name.endswith('.java')]) 111 112 os.path.walk(os.path.join(self.android_root, source_root), AddFile, sources) 113 cmd += ' '.join(sources) 114 proc = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 115 # read and discard any output 116 proc.communicate() 117 # wait for process to terminate and return exit value 118 return proc.wait() 119 120 def GenerateSignatureCheckDescription(self): 121 """Generate the test description for the signature check.""" 122 self.__LogGenerateDescription('android.tests.sigtest') 123 package = tools.TestPackage('SignatureTest', 'android.tests.sigtest') 124 package.AddAttribute('signatureCheck', 'true') 125 package.AddAttribute('runner', '.InstrumentationRunner') 126 package.AddTest('android.tests.sigtest.SignatureTest.signatureTest') 127 description = open(os.path.join(self.test_repository, 'SignatureTest.xml'), 'w') 128 package.WriteDescription(description) 129 description.close() 130 131 def GenerateReferenceAppDescription(self): 132 """Generate the test description for the reference app tests.""" 133 self.__LogGenerateDescription('android.apidemos.cts') 134 package = tools.TestPackage('ApiDemosReferenceTest', 'android.apidemos.cts') 135 package.AddAttribute('packageToTest', 'com.example.android.apis') 136 package.AddAttribute('apkToTestName', 'ApiDemos') 137 package.AddAttribute('runner', 'android.test.InstrumentationTestRunner') 138 package.AddAttribute('referenceAppTest', 'true') 139 package.AddTest('android.apidemos.cts.ApiDemosTest.testNumberOfItemsInListView') 140 description = open(os.path.join(self.test_repository, 'ApiDemosReferenceTest.xml'), 'w') 141 package.WriteDescription(description) 142 description.close() 143 144 def GenerateAppSecurityDescription(self): 145 """Generate the test description for the application security tests.""" 146 test_root = 'cts/tests/appsecurity-tests' 147 makefile_name = os.path.join(test_root, 'Android.mk') 148 makefile_vars = GetMakeFileVars(makefile_name) 149 name = makefile_vars['LOCAL_MODULE'] 150 package_name = 'android.tests.appsecurity' 151 self.__LogGenerateDescription(package_name) 152 temp_desc = os.path.join(self.temp_dir, 'description.xml') 153 self.RunDescriptionGeneratorDoclet(os.path.join(test_root, 'src'), temp_desc) 154 doc = dom.parse(temp_desc) 155 test_description = doc.getElementsByTagName('TestPackage')[0] 156 test_description.setAttribute('name', package_name) 157 test_description.setAttribute('appPackageName', package_name) 158 test_description.setAttribute('hostSideOnly', 'true') 159 test_description.setAttribute('jarPath', name + '.jar') 160 description = open(os.path.join(self.test_repository, package_name + '.xml'), 'w') 161 doc.writexml(description, addindent=' ', encoding='UTF-8') 162 description.close() 163 164 @staticmethod 165 def RelPath(path, start=os.getcwd()): 166 """Get a relative version of a path. 167 168 This is equivalent to os.path.relpath, which is only available since Python 2.6. 169 170 Args: 171 path: The path to transform. 172 start: The base path. Defaults to the current working directory. 173 174 Returns: 175 A transformed path that is relative to start. 176 """ 177 path_dirs = os.path.abspath(path).split(os.path.sep) 178 start_dirs = os.path.abspath(start).split(os.path.sep) 179 180 num_common = len(os.path.commonprefix([start_dirs, path_dirs])) 181 182 result_dirs = ['..'] * (len(start_dirs) - num_common) + path_dirs[num_common:] 183 if result_dirs: 184 return os.path.join(*result_dirs) 185 return start 186 187 def GenerateTestDescriptions(self): 188 """Generate test descriptions for all packages.""" 189 # individually generate descriptions not following conventions 190 self.GenerateSignatureCheckDescription() 191 self.GenerateReferenceAppDescription() 192 self.GenerateAppSecurityDescription() 193 194 # generate test descriptions for android tests 195 android_packages = GetSubDirectories(self.test_root) 196 for package in android_packages: 197 app_package_name = 'android.' + package 198 package_root = os.path.join(self.test_root, package) 199 200 makefile_name = os.path.join(package_root, 'Android.mk') 201 if not os.path.exists(makefile_name): 202 print 'Skipping directory "%s" due to missing Android.mk' % package_root 203 continue 204 makefile_vars = GetMakeFileVars(makefile_name) 205 206 manifest_name = os.path.join(package_root, 'AndroidManifest.xml') 207 if not os.path.exists(manifest_name): 208 print 'Skipping directory "%s" due to missing AndroidManifest.xml' % package_root 209 continue 210 manifest = tools.XmlFile(manifest_name) 211 212 self.__LogGenerateDescription(app_package_name) 213 214 # Run the description generator doclet to get the test package structure 215 # TODO: The Doclet does not currently add all required attributes. Instead of rewriting 216 # the document below, additional attributes should be passed to the Doclet as arguments. 217 temp_desc = os.path.join(self.temp_dir, 'description.xml') 218 self.RunDescriptionGeneratorDoclet(package_root, temp_desc) 219 220 # obtain missing attribute values from the makefile and manifest 221 package_name = makefile_vars['LOCAL_PACKAGE_NAME'] 222 runner = manifest.GetAndroidAttr('instrumentation', 'name') 223 target_package = manifest.GetAndroidAttr('instrumentation', 'targetPackage') 224 target_binary_name = makefile_vars.get('LOCAL_INSTRUMENTATION_FOR') 225 226 # add them to the document 227 doc = dom.parse(temp_desc) 228 test_description = doc.getElementsByTagName('TestPackage')[0] 229 test_description.setAttribute('name', package_name) 230 test_description.setAttribute('runner', runner) 231 test_package = manifest.GetAttr('manifest', 'package') 232 test_description.setAttribute('appNameSpace', test_package) 233 test_description.setAttribute('appPackageName', app_package_name) 234 if not test_package == target_package: 235 test_description.setAttribute('targetNameSpace', target_package) 236 test_description.setAttribute('targetBinaryName', target_binary_name) 237 description = open(os.path.join(self.test_repository, package_name + '.xml'), 'w') 238 doc.writexml(description, addindent=' ', encoding='UTF-8') 239 description.close() 240 241 def __WritePlan(self, plan, plan_name): 242 print 'Generating test plan %s' % plan_name 243 plan.Write(os.path.join(self.plan_repository, plan_name + '.xml')) 244 245 def GenerateTestPlans(self): 246 """Generate default test plans.""" 247 # TODO: Instead of hard-coding the plans here, use a configuration file, 248 # such as test_defs.xml 249 packages = [] 250 descriptions = sorted(glob.glob(os.path.join(self.test_repository, '*.xml'))) 251 for description in descriptions: 252 doc = tools.XmlFile(description) 253 packages.append(doc.GetAttr('TestPackage', 'appPackageName')) 254 255 plan = tools.TestPlan(packages) 256 plan.Exclude('android\.performance.*') 257 self.__WritePlan(plan, 'CTS') 258 plan.Exclude(r'android\.tests\.sigtest') 259 plan.Exclude(r'android\.core.*') 260 self.__WritePlan(plan, 'Android') 261 262 plan = tools.TestPlan(packages) 263 plan.Include(r'android\.core\.tests.*') 264 self.__WritePlan(plan, 'Java') 265 266 plan = tools.TestPlan(packages) 267 plan.Include(r'android\.core\.vm-tests') 268 self.__WritePlan(plan, 'VM') 269 270 plan = tools.TestPlan(packages) 271 plan.Include(r'android\.tests\.sigtest') 272 self.__WritePlan(plan, 'Signature') 273 274 plan = tools.TestPlan(packages) 275 plan.Include(r'android\.apidemos\.cts') 276 self.__WritePlan(plan, 'RefApp') 277 278 plan = tools.TestPlan(packages) 279 plan.Include(r'android\.performance.*') 280 self.__WritePlan(plan, 'Performance') 281 282 plan = tools.TestPlan(packages) 283 plan.Include(r'android\.tests\.appsecurity') 284 self.__WritePlan(plan, 'AppSecurity') 285 286 287if __name__ == '__main__': 288 builder = CtsBuilder(sys.argv) 289 builder.GenerateTestDescriptions() 290 builder.GenerateTestPlans() 291