1#!/usr/bin/python2.4 2# 3# 4# Copyright 2009, The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17 18"""Utility to find instrumentation test definitions from file system.""" 19 20# python imports 21import os 22import re 23 24# local imports 25import android_build 26import android_manifest 27import android_mk 28import instrumentation_test 29import logger 30 31 32class TestWalker(object): 33 """Finds instrumentation tests from filesystem.""" 34 35 def FindTests(self, path): 36 """Gets list of Android instrumentation tests found at given path. 37 38 Tests are created from the <instrumentation> tags found in 39 AndroidManifest.xml files relative to the given path. 40 41 FindTests will first scan sub-folders of path for tests. If none are found, 42 it will scan the file system upwards until a AndroidManifest.xml is found 43 or the Android build root is reached. 44 45 Some sample values for path: 46 - a parent directory containing many tests: 47 ie development/samples will return tests for instrumentation's in ApiDemos, 48 ApiDemos/tests, Notepad/tests etc 49 - a java test class file 50 ie ApiDemos/tests/src/../ApiDemosTest.java will return a test for 51 the instrumentation in ApiDemos/tests, with the class name filter set to 52 ApiDemosTest 53 - a java package directory 54 ie ApiDemos/tests/src/com/example/android/apis will return a test for 55 the instrumentation in ApiDemos/tests, with the java package filter set 56 to com.example.android.apis. 57 58 Args: 59 path: file system path to search 60 61 Returns: 62 list of test suites that support operations defined by 63 test_suite.AbstractTestSuite 64 """ 65 if not os.path.exists(path): 66 logger.Log('%s does not exist' % path) 67 return [] 68 abspath = os.path.abspath(path) 69 # ensure path is in ANDROID_BUILD_ROOT 70 self._build_top = android_build.GetTop() 71 if not self._IsPathInBuildTree(abspath): 72 logger.Log('%s is not a sub-directory of build root %s' % 73 (path, self._build_top)) 74 return [] 75 76 # first, assume path is a parent directory, which specifies to run all 77 # tests within this directory 78 tests = self._FindSubTests(abspath, []) 79 if not tests: 80 logger.SilentLog('No tests found within %s, searching upwards' % path) 81 tests = self._FindUpstreamTests(abspath) 82 return tests 83 84 def _IsPathInBuildTree(self, path): 85 """Return true if given path is within current Android build tree. 86 87 Args: 88 path: absolute file system path 89 90 Returns: 91 True if path is within Android build tree 92 """ 93 return os.path.commonprefix([self._build_top, path]) == self._build_top 94 95 def _MakePathRelativeToBuild(self, path): 96 """Convert given path to one relative to build tree root. 97 98 Args: 99 path: absolute file system path to convert. 100 101 Returns: 102 The converted path relative to build tree root. 103 104 Raises: 105 ValueError: if path is not within build tree 106 """ 107 if not self._IsPathInBuildTree(path): 108 raise ValueError 109 build_path_len = len(self._build_top) + 1 110 # return string with common build_path removed 111 return path[build_path_len:] 112 113 def _FindSubTests(self, path, tests, build_path=None): 114 """Recursively finds all tests within given path. 115 116 Args: 117 path: absolute file system path to check 118 tests: current list of found tests 119 build_path: the parent directory where Android.mk was found 120 121 Returns: 122 updated list of tests 123 """ 124 if not os.path.isdir(path): 125 return tests 126 filenames = os.listdir(path) 127 # Try to build as much of original path as possible, so 128 # keep track of upper-most parent directory where Android.mk was found 129 # this is also necessary in case of overlapping tests 130 # ie if a test exists at 'foo' directory and 'foo/sub', attempting to 131 # build both 'foo' and 'foo/sub' will fail. 132 if not build_path and filenames.count(android_mk.AndroidMK.FILENAME): 133 build_path = self._MakePathRelativeToBuild(path) 134 if filenames.count(android_manifest.AndroidManifest.FILENAME): 135 # found a manifest! now parse it to find the test definition(s) 136 manifest = android_manifest.AndroidManifest(app_path=path) 137 tests.extend(self._CreateSuitesFromManifest(manifest, build_path)) 138 for filename in filenames: 139 self._FindSubTests(os.path.join(path, filename), tests, build_path) 140 return tests 141 142 def _FindUpstreamTests(self, path): 143 """Find tests defined upward from given path. 144 145 Args: 146 path: the location to start searching. If it points to a java class file 147 or java package dir, the appropriate test suite filters will be set 148 149 Returns: 150 list of test_suite.AbstractTestSuite found, may be empty 151 """ 152 class_name_arg = None 153 package_name = None 154 # if path is java file, populate class name 155 if self._IsJavaFile(path): 156 class_name_arg = self._GetClassNameFromFile(path) 157 logger.SilentLog('Using java test class %s' % class_name_arg) 158 elif self._IsJavaPackage(path): 159 package_name = self._GetPackageNameFromDir(path) 160 logger.SilentLog('Using java package %s' % package_name) 161 manifest = self._FindUpstreamManifest(path) 162 if manifest: 163 logger.SilentLog('Found AndroidManifest at %s' % manifest.GetAppPath()) 164 build_path = self._MakePathRelativeToBuild(manifest.GetAppPath()) 165 return self._CreateSuitesFromManifest(manifest, 166 build_path, 167 class_name=class_name_arg, 168 java_package_name=package_name) 169 170 def _IsJavaFile(self, path): 171 """Returns true if given file system path is a java file.""" 172 return os.path.isfile(path) and self._IsJavaFileName(path) 173 174 def _IsJavaFileName(self, filename): 175 """Returns true if given file name is a java file name.""" 176 return os.path.splitext(filename)[1] == '.java' 177 178 def _IsJavaPackage(self, path): 179 """Returns true if given file path is a java package. 180 181 Currently assumes if any java file exists in this directory, than it 182 represents a java package. 183 184 Args: 185 path: file system path of directory to check 186 187 Returns: 188 True if path is a java package 189 """ 190 if not os.path.isdir(path): 191 return False 192 for file_name in os.listdir(path): 193 if self._IsJavaFileName(file_name): 194 return True 195 return False 196 197 def _GetClassNameFromFile(self, java_file_path): 198 """Gets the fully qualified java class name from path. 199 200 Args: 201 java_file_path: file system path of java file 202 203 Returns: 204 fully qualified java class name or None. 205 """ 206 package_name = self._GetPackageNameFromFile(java_file_path) 207 if package_name: 208 filename = os.path.basename(java_file_path) 209 class_name = os.path.splitext(filename)[0] 210 return '%s.%s' % (package_name, class_name) 211 return None 212 213 def _GetPackageNameFromDir(self, path): 214 """Gets the java package name associated with given directory path. 215 216 Caveat: currently just parses defined java package name from first java 217 file found in directory. 218 219 Args: 220 path: file system path of directory 221 222 Returns: 223 the java package name or None 224 """ 225 for filename in os.listdir(path): 226 if self._IsJavaFileName(filename): 227 return self._GetPackageNameFromFile(os.path.join(path, filename)) 228 229 def _GetPackageNameFromFile(self, java_file_path): 230 """Gets the java package name associated with given java file path. 231 232 Args: 233 java_file_path: file system path of java file 234 235 Returns: 236 the java package name or None 237 """ 238 logger.SilentLog('Looking for java package name in %s' % java_file_path) 239 re_package = re.compile(r'package\s+(.*);') 240 file_handle = open(java_file_path, 'r') 241 for line in file_handle: 242 match = re_package.match(line) 243 if match: 244 return match.group(1) 245 return None 246 247 def _FindUpstreamManifest(self, path): 248 """Recursively searches filesystem upwards for a AndroidManifest file. 249 250 Args: 251 path: file system path to search 252 253 Returns: 254 the AndroidManifest found or None 255 """ 256 if (os.path.isdir(path) and 257 os.listdir(path).count(android_manifest.AndroidManifest.FILENAME)): 258 return android_manifest.AndroidManifest(app_path=path) 259 dirpath = os.path.dirname(path) 260 if self._IsPathInBuildTree(path): 261 return self._FindUpstreamManifest(dirpath) 262 logger.Log('AndroidManifest.xml not found') 263 return None 264 265 def _CreateSuitesFromManifest(self, manifest, build_path, class_name=None, 266 java_package_name=None): 267 """Creates TestSuites from a AndroidManifest. 268 269 Args: 270 manifest: the AndroidManifest 271 build_path: the build path to use for test 272 class_name: optionally, the class filter for the suite 273 java_package_name: optionally, the java package filter for the suite 274 275 Returns: 276 the list of tests created 277 """ 278 tests = [] 279 for instr_name in manifest.GetInstrumentationNames(): 280 pkg_name = manifest.GetPackageName() 281 if instr_name.find(".") < 0: 282 instr_name = "." + instr_name 283 logger.SilentLog('Found instrumentation %s/%s' % (pkg_name, instr_name)) 284 suite = instrumentation_test.InstrumentationTestSuite() 285 suite.SetPackageName(pkg_name) 286 suite.SetBuildPath(build_path) 287 suite.SetRunnerName(instr_name) 288 suite.SetName(pkg_name) 289 suite.SetClassName(class_name) 290 suite.SetJavaPackageFilter(java_package_name) 291 # this is a bit of a hack, assume if 'com.android.cts' is in 292 # package name, this is a cts test 293 # this logic can be removed altogether when cts tests no longer require 294 # custom build steps 295 if suite.GetPackageName().startswith('com.android.cts'): 296 suite.SetSuite('cts') 297 tests.append(suite) 298 return tests 299