1#!/usr/bin/python2.4
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"""Utility classes for CTS."""
18
19import re
20import xml.dom.minidom as minidom
21
22
23class TestPackage(object):
24  """This class represents a test package.
25
26  Each test package consists of one or more suites, each containing one or more subsuites and/or
27  one or more test cases. Each test case contains one or more tests.
28
29  The package structure is currently stored using Python dictionaries and lists. Translation
30  to XML is done via a DOM tree that gets created on demand.
31
32  TODO: Instead of using an internal data structure, using a DOM tree directly would increase
33  the usability. For example, one could easily create an instance by parsing an existing XML.
34  """
35
36  class TestSuite(object):
37    """A test suite."""
38
39    def __init__(self, is_root=False):
40      self.is_root = is_root
41      self.test_cases = {}
42      self.test_suites = {}
43
44    def Add(self, names):
45      if len(names) == 2:
46        # names contains the names of the test case and the test
47        test_case = self.test_cases.setdefault(names[0], [])
48        test_case.append(names[1])
49      else:
50        sub_suite = self.test_suites.setdefault(names[0], TestPackage.TestSuite())
51        sub_suite.Add(names[1:])
52
53    def WriteDescription(self, doc, parent):
54      """Recursively append all suites and testcases to the parent tag."""
55      for (suite_name, suite) in self.test_suites.iteritems():
56        child = doc.createElement('TestSuite')
57        child.setAttribute('name', suite_name)
58        parent.appendChild(child)
59        # recurse into child suites
60        suite.WriteDescription(doc, child)
61      for (case_name, test_list) in self.test_cases.iteritems():
62        child = doc.createElement('TestCase')
63        child.setAttribute('name', case_name)
64        parent.appendChild(child)
65        for test_name in test_list:
66          test = doc.createElement('Test')
67          test.setAttribute('name', test_name)
68          child.appendChild(test)
69
70  def __init__(self, package_name, app_package_name=''):
71    self.encoding = 'UTF-8'
72    self.attributes = {'name': package_name, 'AndroidFramework': 'Android 1.0',
73                       'version': '1.0', 'targetNameSpace': '', 'targetBinaryName': '',
74                       'jarPath': '', 'appPackageName': app_package_name}
75    self.root_suite = self.TestSuite(is_root=True)
76
77  def AddTest(self, name):
78    """Add a test to the package.
79
80    Test names are given in the form "testSuiteName.testSuiteName.TestCaseName.testName".
81    Test suites can be nested to any depth.
82
83    Args:
84      name: The name of the test to add.
85    """
86    parts = name.split('.')
87    self.root_suite.Add(parts)
88
89  def AddAttribute(self, name, value):
90    """Add an attribute to the test package itself."""
91    self.attributes[name] = value
92
93  def GetDocument(self):
94    """Returns a minidom Document representing the test package structure."""
95    doc = minidom.Document()
96    package = doc.createElement('TestPackage')
97    for (attr, value) in self.attributes.iteritems():
98      package.setAttribute(attr, value)
99    self.root_suite.WriteDescription(doc, package)
100    doc.appendChild(package)
101    return doc
102
103  def WriteDescription(self, writer):
104    """Write the description as XML to the given writer."""
105    doc = self.GetDocument()
106    doc.writexml(writer, addindent='  ', newl='\n', encoding=self.encoding)
107    doc.unlink()
108
109
110class TestPlan(object):
111  """A CTS test plan generator."""
112
113  def __init__(self, all_packages):
114    """Instantiate a test plan with a list of available package names.
115
116    Args:
117      all_packages: The full list of available packages. Subsequent calls to Exclude() and
118          Include() select from the packages given here.
119    """
120    self.all_packages = all_packages
121    self.map = None
122
123    self.includedTestsMap = {}
124    self.excludedTestsMap = {}
125
126
127  def IncludeTests(self, package, test_list):
128    """Include only specific tests in this plan.
129
130    package The package that contains the tests. e.g. android.mypackage
131      This package should must be included via Include.
132    test_list A list of tests with methods to be included. e.g.
133      ['TestClass#testA', 'TestClass#testB']
134    """
135    packaged_test_list = []
136    for test in test_list:
137      packaged_test_list.append(test)
138
139    if package in self.includedTestsMap:
140      self.includedTestsMap[package] += packaged_test_list
141    else:
142      self.includedTestsMap[package] = packaged_test_list
143
144
145  def ExcludeTests(self, package, test_list):
146    """Exclude specific tests from this plan.
147
148    package The package that contains the tests. e.g. android.mypackage
149      This package should must be included via Include.
150    test_list A list of tests with methods to be excluded. e.g.
151      ['TestClass#testA', 'TestClass#textB']
152    """
153    packaged_test_list = []
154    for test in test_list:
155      packaged_test_list.append(test)
156    if package in self.excludedTestsMap:
157      self.excludedTestsMap[package] += packaged_test_list
158    else:
159      self.excludedTestsMap[package] = packaged_test_list
160
161
162  def Exclude(self, pattern):
163    """Exclude all packages matching the given regular expression from the plan.
164
165    If this is the first call to Exclude() or Include(), all packages are selected before applying
166    the exclusion.
167
168    Args:
169      pattern: A regular expression selecting the package names to exclude.
170    """
171    if not self.map:
172      self.Include('.*')
173    exp = re.compile(pattern)
174    for package in self.all_packages:
175      if exp.match(package):
176        self.map[package] = False
177
178  def Include(self, pattern):
179    """Include all packages matching the given regular expressions in the plan.
180
181    Args:
182      pattern: A regular expression selecting the package names to include.
183    """
184    if not self.map:
185      self.map = {}
186      for package in self.all_packages:
187        self.map[package] = False
188    exp = re.compile(pattern)
189    for package in self.all_packages:
190      if exp.match(package):
191        self.map[package] = True
192
193  def Write(self, file_name):
194    """Write the test plan to the given file.
195
196    Requires Include() or Exclude() to be called prior to calling this method.
197
198    Args:
199      file_name: The name of the file into which the test plan should be written.
200    """
201    doc = minidom.Document()
202    plan = doc.createElement('TestPlan')
203    plan.setAttribute('version', '1.0')
204    doc.appendChild(plan)
205    for package in self.all_packages:
206      if self.map[package]:
207        entry = doc.createElement('Entry')
208        entry.setAttribute('name', package)
209        if package in self.excludedTestsMap:
210          entry.setAttribute('exclude', ';'.join(self.excludedTestsMap[package]))
211        if package in self.includedTestsMap:
212          entry.setAttribute('include', ';'.join(self.includedTestsMap[package]))
213        plan.appendChild(entry)
214    stream = open(file_name, 'w')
215    doc.writexml(stream, addindent='  ', newl='\n', encoding='UTF-8')
216    stream.close()
217
218
219class XmlFile(object):
220  """This class parses Xml files and allows reading attribute values by tag and attribute name."""
221
222  def __init__(self, path):
223    """Instantiate the class using the manifest file denoted by path."""
224    self.doc = minidom.parse(path)
225
226  def GetAndroidAttr(self, tag, attr_name):
227    """Get the value of the given attribute in the first matching tag.
228
229    Args:
230      tag: The name of the tag to search.
231      attr_name: An attribute name in the android manifest namespace.
232
233    Returns:
234      The value of the given attribute in the first matching tag.
235    """
236    element = self.doc.getElementsByTagName(tag)[0]
237    return element.getAttributeNS('http://schemas.android.com/apk/res/android', attr_name)
238
239  def GetAttr(self, tag, attr_name):
240    """Return the value of the given attribute in the first matching tag.
241
242    Args:
243      tag: The name of the tag to search.
244      attr_name: An attribute name in the default namespace.
245
246    Returns:
247      The value of the given attribute in the first matching tag.
248    """
249    element = self.doc.getElementsByTagName(tag)[0]
250    return element.getAttribute(attr_name)
251