From 59b4778dd6758436e7e301323b5eb6c10a3061c9 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Wed, 21 Oct 2009 17:23:01 -0700 Subject: [PATCH] Add runtest --path. This supports specifying a file system path to test(s) to run. The path can be a java test class file, a java package directory, or a parent directory containing many tests. This change allows users to run tests independently from test_defs.xml. BUG 2133198 --- testrunner/runtest.py | 44 +++-- testrunner/test_defs/__init__.py | 2 +- testrunner/test_defs/test_walker.py | 296 ++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 12 deletions(-) create mode 100755 testrunner/test_defs/test_walker.py diff --git a/testrunner/runtest.py b/testrunner/runtest.py index b6938cdbc..a4d723128 100755 --- a/testrunner/runtest.py +++ b/testrunner/runtest.py @@ -14,9 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Command line utility for running a pre-defined test. +"""Command line utility for running Android tests -Based on previous /development/tools/runtest shell script. +runtest helps automate the instructions for building and running tests +- It builds the corresponding test package for the code you want to test +- It pushes the test package to your device or emulator +- It launches InstrumentationTestRunner (or similar) to run the tests you +specify. + +runtest supports running tests whose attributes have been pre-defined in +_TEST_FILE_NAME files, (runtest ), or by specifying the file +system path to the test to run (runtest --path ). + +Do runtest --help to see full list of options. """ # Python imports @@ -34,6 +44,7 @@ import errors import logger import run_command from test_defs import test_defs +from test_defs import test_walker class TestRunner(object): @@ -57,7 +68,7 @@ class TestRunner(object): "for a list of tests, or you can launch one or more tests.") # default value for make -jX - _DEFAULT_JOBS=4 + _DEFAULT_JOBS = 4 def __init__(self): # disable logging of timestamp @@ -67,6 +78,7 @@ class TestRunner(object): self._known_tests = None self._options = None self._test_args = None + self._tests_to_run = None def _ProcessOptions(self): """Processes command-line options.""" @@ -114,6 +126,8 @@ class TestRunner(object): parser.add_option("-o", "--coverage", dest="coverage", default=False, action="store_true", help="Generate code coverage metrics for test(s)") + parser.add_option("-x", "--path", dest="test_path", + help="Run test(s) at given file system path") parser.add_option("-t", "--all-tests", dest="all_tests", default=False, action="store_true", help="Run all defined tests") @@ -145,6 +159,7 @@ class TestRunner(object): and not self._options.all_tests and not self._options.continuous_tests and not self._options.cts_tests + and not self._options.test_path and len(self._test_args) < 1): parser.print_help() logger.SilentLog("at least one test name must be specified") @@ -217,8 +232,8 @@ class TestRunner(object): # cts dependencies are removed if self._IsCtsTests(tests): # need to use make since these fail building with ONE_SHOT_MAKEFILE - cmd=('make -j%s CtsTestStubs android.core.tests.runner' % - self._options.make_jobs) + cmd = ('make -j%s CtsTestStubs android.core.tests.runner' % + self._options.make_jobs) logger.Log(cmd) if not self._options.preview: old_dir = os.getcwd() @@ -262,21 +277,28 @@ class TestRunner(object): def _GetTestsToRun(self): """Get a list of TestSuite objects to run, based on command line args.""" + if self._tests_to_run: + return self._tests_to_run + + self._tests_to_run = [] if self._options.all_tests: - return self._known_tests.GetTests() + self._tests_to_run = self._known_tests.GetTests() elif self._options.continuous_tests: - return self._known_tests.GetContinuousTests() + self._tests_to_run = self._known_tests.GetContinuousTests() elif self._options.cts_tests: - return self._known_tests.GetCtsTests() - tests = [] + self._tests_to_run = self._known_tests.GetCtsTests() + elif self._options.test_path: + walker = test_walker.TestWalker() + self._tests_to_run = walker.FindTests(self._options.test_path) + for name in self._test_args: test = self._known_tests.GetTest(name) if test is None: logger.Log("Error: Could not find test %s" % name) self._DumpTests() raise errors.AbortError - tests.append(test) - return tests + self._tests_to_run.append(test) + return self._tests_to_run def _IsCtsTests(self, test_list): """Check if any cts tests are included in given list of tests to run.""" diff --git a/testrunner/test_defs/__init__.py b/testrunner/test_defs/__init__.py index c205dcb8c..f397d2a74 100644 --- a/testrunner/test_defs/__init__.py +++ b/testrunner/test_defs/__init__.py @@ -1 +1 @@ -__all__ = ['test_defs'] +__all__ = ['test_defs', 'test_walker'] diff --git a/testrunner/test_defs/test_walker.py b/testrunner/test_defs/test_walker.py new file mode 100755 index 000000000..973cac16f --- /dev/null +++ b/testrunner/test_defs/test_walker.py @@ -0,0 +1,296 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2009, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility to find instrumentation test definitions from file system.""" + +# python imports +import os +import re + +# local imports +import android_build +import android_manifest +import android_mk +import instrumentation_test +import logger + + +class TestWalker(object): + """Finds instrumentation tests from filesystem.""" + + def FindTests(self, path): + """Gets list of Android instrumentation tests found at given path. + + Tests are created from the tags found in + AndroidManifest.xml files relative to the given path. + + FindTests will first scan sub-folders of path for tests. If none are found, + it will scan the file system upwards until a AndroidManifest.xml is found + or the Android build root is reached. + + Some sample values for path: + - a parent directory containing many tests: + ie development/samples will return tests for instrumentation's in ApiDemos, + ApiDemos/tests, Notepad/tests etc + - a java test class file + ie ApiDemos/tests/src/../ApiDemosTest.java will return a test for + the instrumentation in ApiDemos/tests, with the class name filter set to + ApiDemosTest + - a java package directory + ie ApiDemos/tests/src/com/example/android/apis will return a test for + the instrumentation in ApiDemos/tests, with the java package filter set + to com.example.android.apis. + + Args: + path: file system path to search + + Returns: + list of test suites that support operations defined by + test_suite.AbstractTestSuite + """ + if not os.path.exists(path): + logger.Log('%s does not exist' % path) + return [] + abspath = os.path.abspath(path) + # ensure path is in ANDROID_BUILD_ROOT + self._build_top = android_build.GetTop() + if not self._IsPathInBuildTree(abspath): + logger.Log('%s is not a sub-directory of build root %s' % + (path, self._build_top)) + return [] + + # first, assume path is a parent directory, which specifies to run all + # tests within this directory + tests = self._FindSubTests(abspath, []) + if not tests: + logger.SilentLog('No tests found within %s, searching upwards' % path) + tests = self._FindUpstreamTests(abspath) + return tests + + def _IsPathInBuildTree(self, path): + """Return true if given path is within current Android build tree. + + Args: + path: absolute file system path + + Returns: + True if path is within Android build tree + """ + return os.path.commonprefix([self._build_top, path]) == self._build_top + + def _MakePathRelativeToBuild(self, path): + """Convert given path to one relative to build tree root. + + Args: + path: absolute file system path to convert. + + Returns: + The converted path relative to build tree root. + + Raises: + ValueError: if path is not within build tree + """ + if not self._IsPathInBuildTree(path): + raise ValueError + build_path_len = len(self._build_top) + 1 + # return string with common build_path removed + return path[build_path_len:] + + def _FindSubTests(self, path, tests, build_path=None): + """Recursively finds all tests within given path. + + Args: + path: absolute file system path to check + tests: current list of found tests + build_path: the parent directory where Android.mk was found + + Returns: + updated list of tests + """ + if not os.path.isdir(path): + return tests + filenames = os.listdir(path) + # Try to build as much of original path as possible, so + # keep track of upper-most parent directory where Android.mk was found + # this is also necessary in case of overlapping tests + # ie if a test exists at 'foo' directory and 'foo/sub', attempting to + # build both 'foo' and 'foo/sub' will fail. + if not build_path and filenames.count(android_mk.AndroidMK.FILENAME): + build_path = self._MakePathRelativeToBuild(path) + if filenames.count(android_manifest.AndroidManifest.FILENAME): + # found a manifest! now parse it to find the test definition(s) + manifest = android_manifest.AndroidManifest(app_path=path) + tests.extend(self._CreateSuitesFromManifest(manifest, build_path)) + for filename in filenames: + self._FindSubTests(os.path.join(path, filename), tests, build_path) + return tests + + def _FindUpstreamTests(self, path): + """Find tests defined upward from given path. + + Args: + path: the location to start searching. If it points to a java class file + or java package dir, the appropriate test suite filters will be set + + Returns: + list of test_suite.AbstractTestSuite found, may be empty + """ + class_name_arg = None + package_name = None + # if path is java file, populate class name + if self._IsJavaFile(path): + class_name_arg = self._GetClassNameFromFile(path) + logger.SilentLog('Using java test class %s' % class_name_arg) + elif self._IsJavaPackage(path): + package_name = self._GetPackageNameFromDir(path) + logger.SilentLog('Using java package %s' % package_name) + manifest = self._FindUpstreamManifest(path) + if manifest: + logger.SilentLog('Found AndroidManifest at %s' % manifest.GetAppPath()) + build_path = self._MakePathRelativeToBuild(manifest.GetAppPath()) + return self._CreateSuitesFromManifest(manifest, + build_path, + class_name=class_name_arg, + java_package_name=package_name) + + def _IsJavaFile(self, path): + """Returns true if given file system path is a java file.""" + return os.path.isfile(path) and self._IsJavaFileName(path) + + def _IsJavaFileName(self, filename): + """Returns true if given file name is a java file name.""" + return os.path.splitext(filename)[1] == '.java' + + def _IsJavaPackage(self, path): + """Returns true if given file path is a java package. + + Currently assumes if any java file exists in this directory, than it + represents a java package. + + Args: + path: file system path of directory to check + + Returns: + True if path is a java package + """ + if not os.path.isdir(path): + return False + for file_name in os.listdir(path): + if self._IsJavaFileName(file_name): + return True + return False + + def _GetClassNameFromFile(self, java_file_path): + """Gets the fully qualified java class name from path. + + Args: + java_file_path: file system path of java file + + Returns: + fully qualified java class name or None. + """ + package_name = self._GetPackageNameFromFile(java_file_path) + if package_name: + filename = os.path.basename(java_file_path) + class_name = os.path.splitext(filename)[0] + return '%s.%s' % (package_name, class_name) + return None + + def _GetPackageNameFromDir(self, path): + """Gets the java package name associated with given directory path. + + Caveat: currently just parses defined java package name from first java + file found in directory. + + Args: + path: file system path of directory + + Returns: + the java package name or None + """ + for filename in os.listdir(path): + if self._IsJavaFileName(filename): + return self._GetPackageNameFromFile(os.path.join(path, filename)) + + def _GetPackageNameFromFile(self, java_file_path): + """Gets the java package name associated with given java file path. + + Args: + java_file_path: file system path of java file + + Returns: + the java package name or None + """ + logger.SilentLog('Looking for java package name in %s' % java_file_path) + re_package = re.compile(r'package\s+(.*);') + file_handle = open(java_file_path, 'r') + for line in file_handle: + match = re_package.match(line) + if match: + return match.group(1) + return None + + def _FindUpstreamManifest(self, path): + """Recursively searches filesystem upwards for a AndroidManifest file. + + Args: + path: file system path to search + + Returns: + the AndroidManifest found or None + """ + if (os.path.isdir(path) and + os.listdir(path).count(android_manifest.AndroidManifest.FILENAME)): + return android_manifest.AndroidManifest(app_path=path) + dirpath = os.path.dirname(path) + if self._IsPathInBuildTree(path): + return self._FindUpstreamManifest(dirpath) + logger.Log('AndroidManifest.xml not found') + return None + + def _CreateSuitesFromManifest(self, manifest, build_path, class_name=None, + java_package_name=None): + """Creates TestSuites from a AndroidManifest. + + Args: + manifest: the AndroidManifest + build_path: the build path to use for test + class_name: optionally, the class filter for the suite + java_package_name: optionally, the java package filter for the suite + + Returns: + the list of tests created + """ + tests = [] + for instr_name in manifest.GetInstrumentationNames(): + pkg_name = manifest.GetPackageName() + logger.SilentLog('Found instrumentation %s/%s' % (pkg_name, instr_name)) + suite = instrumentation_test.InstrumentationTestSuite() + suite.SetPackageName(pkg_name) + suite.SetBuildPath(build_path) + suite.SetRunnerName(instr_name) + suite.SetName(pkg_name) + suite.SetClassName(class_name) + suite.SetJavaPackageFilter(java_package_name) + # this is a bit of a hack, assume if 'com.android.cts' is in + # package name, this is a cts test + # this logic can be removed altogether when cts tests no longer require + # custom build steps + suite.SetCts(suite.GetPackageName().startswith('com.android.cts')) + tests.append(suite) + return tests +