Home | History | Annotate | Download | only in utils
      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