Brett Chabot | 59b4778 | 2009-10-21 17:23:01 -0700 | [diff] [blame] | 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 |
| 21 | import os |
| 22 | import re |
| 23 | |
| 24 | # local imports |
| 25 | import android_build |
| 26 | import android_manifest |
| 27 | import android_mk |
| 28 | import instrumentation_test |
| 29 | import logger |
| 30 | |
| 31 | |
| 32 | class 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() |
Fred Quintana | 75a73dc | 2010-03-16 16:27:03 -0700 | [diff] [blame^] | 281 | if instr_name.find(".") < 0: |
| 282 | instr_name = "." + instr_name |
Brett Chabot | 59b4778 | 2009-10-21 17:23:01 -0700 | [diff] [blame] | 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 |
Brett Chabot | 4a5d9f1 | 2010-02-18 20:01:11 -0800 | [diff] [blame] | 295 | if suite.GetPackageName().startswith('com.android.cts'): |
| 296 | suite.SetSuite('cts') |
Brett Chabot | 59b4778 | 2009-10-21 17:23:01 -0700 | [diff] [blame] | 297 | tests.append(suite) |
| 298 | return tests |