Home | History | Annotate | Download | only in runner
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.test.runner;
     17 
     18 import android.app.Instrumentation;
     19 import android.os.Bundle;
     20 import android.test.suitebuilder.annotation.LargeTest;
     21 import android.test.suitebuilder.annotation.MediumTest;
     22 import android.test.suitebuilder.annotation.SmallTest;
     23 import android.test.suitebuilder.annotation.Suppress;
     24 import android.util.Log;
     25 
     26 import com.android.test.runner.ClassPathScanner.ChainedClassNameFilter;
     27 import com.android.test.runner.ClassPathScanner.ExcludePackageNameFilter;
     28 import com.android.test.runner.ClassPathScanner.ExternalClassNameFilter;
     29 import com.android.test.runner.ClassPathScanner.InclusivePackageNameFilter;
     30 
     31 import org.junit.runner.Computer;
     32 import org.junit.runner.Description;
     33 import org.junit.runner.Request;
     34 import org.junit.runner.Runner;
     35 import org.junit.runner.manipulation.Filter;
     36 import org.junit.runners.model.InitializationError;
     37 
     38 import java.io.IOException;
     39 import java.io.PrintStream;
     40 import java.lang.annotation.Annotation;
     41 import java.util.Arrays;
     42 import java.util.Collection;
     43 import java.util.Collections;
     44 import java.util.regex.Pattern;
     45 
     46 /**
     47  * Builds a {@link Request} from test classes in given apk paths, filtered on provided set of
     48  * restrictions.
     49  */
     50 public class TestRequestBuilder {
     51 
     52     private static final String LOG_TAG = "TestRequestBuilder";
     53 
     54     public static final String LARGE_SIZE = "large";
     55     public static final String MEDIUM_SIZE = "medium";
     56     public static final String SMALL_SIZE = "small";
     57 
     58     private String[] mApkPaths;
     59     private TestLoader mTestLoader;
     60     private Filter mFilter = new AnnotationExclusionFilter(Suppress.class);
     61     private PrintStream mWriter;
     62     private boolean mSkipExecution = false;
     63     private String mTestPackageName = null;
     64 
     65     /**
     66      * Filter that only runs tests whose method or class has been annotated with given filter.
     67      */
     68     private static class AnnotationInclusionFilter extends Filter {
     69 
     70         private final Class<? extends Annotation> mAnnotationClass;
     71 
     72         AnnotationInclusionFilter(Class<? extends Annotation> annotation) {
     73             mAnnotationClass = annotation;
     74         }
     75 
     76         /**
     77          * {@inheritDoc}
     78          */
     79         @Override
     80         public boolean shouldRun(Description description) {
     81             if (description.isTest()) {
     82                 return description.getAnnotation(mAnnotationClass) != null ||
     83                         description.getTestClass().isAnnotationPresent(mAnnotationClass);
     84             } else {
     85                 // the entire test class/suite should be filtered out if all its methods are
     86                 // filtered
     87                 // TODO: This is not efficient since some children may end up being evaluated more
     88                 // than once. This logic seems to be only necessary for JUnit3 tests. Look into
     89                 // fixing in upstream
     90                 for (Description child : description.getChildren()) {
     91                     if (shouldRun(child)) {
     92                         return true;
     93                     }
     94                 }
     95                 // no children to run, filter this out
     96                 return false;
     97             }
     98         }
     99 
    100         /**
    101          * {@inheritDoc}
    102          */
    103         @Override
    104         public String describe() {
    105             return String.format("annotation %s", mAnnotationClass.getName());
    106         }
    107     }
    108 
    109     /**
    110      * Filter out tests whose method or class has been annotated with given filter.
    111      */
    112     private static class AnnotationExclusionFilter extends Filter {
    113 
    114         private final Class<? extends Annotation> mAnnotationClass;
    115 
    116         AnnotationExclusionFilter(Class<? extends Annotation> annotation) {
    117             mAnnotationClass = annotation;
    118         }
    119 
    120         /**
    121          * {@inheritDoc}
    122          */
    123         @Override
    124         public boolean shouldRun(Description description) {
    125             final Class<?> testClass = description.getTestClass();
    126 
    127             /* Parameterized tests have no test classes. */
    128             if (testClass == null) {
    129                 return true;
    130             }
    131 
    132             if (testClass.isAnnotationPresent(mAnnotationClass) ||
    133                     description.getAnnotation(mAnnotationClass) != null) {
    134                 return false;
    135             } else {
    136                 return true;
    137             }
    138         }
    139 
    140         /**
    141          * {@inheritDoc}
    142          */
    143         @Override
    144         public String describe() {
    145             return String.format("not annotation %s", mAnnotationClass.getName());
    146         }
    147     }
    148 
    149     public TestRequestBuilder(PrintStream writer, String... apkPaths) {
    150         mApkPaths = apkPaths;
    151         mTestLoader = new TestLoader(writer);
    152     }
    153 
    154     /**
    155      * Add a test class to be executed. All test methods in this class will be executed.
    156      *
    157      * @param className
    158      */
    159     public void addTestClass(String className) {
    160         mTestLoader.loadClass(className);
    161     }
    162 
    163     /**
    164      * Adds a test method to run.
    165      * <p/>
    166      * Currently only supports one test method to be run.
    167      */
    168     public void addTestMethod(String testClassName, String testMethodName) {
    169         Class<?> clazz = mTestLoader.loadClass(testClassName);
    170         if (clazz != null) {
    171             mFilter = mFilter.intersect(matchParameterizedMethod(
    172                     Description.createTestDescription(clazz, testMethodName)));
    173         }
    174     }
    175 
    176     /**
    177      * A filter to get around the fact that parameterized tests append "[#]" at
    178      * the end of the method names. For instance, "getFoo" would become
    179      * "getFoo[0]".
    180      */
    181     private static Filter matchParameterizedMethod(final Description target) {
    182         return new Filter() {
    183             Pattern pat = Pattern.compile(target.getMethodName() + "(\\[[0-9]+\\])?");
    184 
    185             @Override
    186             public boolean shouldRun(Description desc) {
    187                 if (desc.isTest()) {
    188                     return target.getClassName().equals(desc.getClassName())
    189                             && isMatch(desc.getMethodName());
    190                 }
    191 
    192                 for (Description child : desc.getChildren()) {
    193                     if (shouldRun(child)) {
    194                         return true;
    195                     }
    196                 }
    197                 return false;
    198             }
    199 
    200             private boolean isMatch(String first) {
    201                 return pat.matcher(first).matches();
    202             }
    203 
    204             @Override
    205             public String describe() {
    206                 return String.format("Method %s", target.getDisplayName());
    207             }
    208         };
    209     }
    210 
    211     /**
    212      * Run only tests within given java package
    213      * @param testPackage
    214      */
    215     public void addTestPackageFilter(String testPackage) {
    216         mTestPackageName = testPackage;
    217     }
    218 
    219     /**
    220      * Run only tests with given size
    221      * @param testSize
    222      */
    223     public void addTestSizeFilter(String testSize) {
    224         if (SMALL_SIZE.equals(testSize)) {
    225             mFilter = mFilter.intersect(new AnnotationInclusionFilter(SmallTest.class));
    226         } else if (MEDIUM_SIZE.equals(testSize)) {
    227             mFilter = mFilter.intersect(new AnnotationInclusionFilter(MediumTest.class));
    228         } else if (LARGE_SIZE.equals(testSize)) {
    229             mFilter = mFilter.intersect(new AnnotationInclusionFilter(LargeTest.class));
    230         } else {
    231             Log.e(LOG_TAG, String.format("Unrecognized test size '%s'", testSize));
    232         }
    233     }
    234 
    235     /**
    236      * Only run tests annotated with given annotation class.
    237      *
    238      * @param annotation the full class name of annotation
    239      */
    240     public void addAnnotationInclusionFilter(String annotation) {
    241         Class<? extends Annotation> annotationClass = loadAnnotationClass(annotation);
    242         if (annotationClass != null) {
    243             mFilter = mFilter.intersect(new AnnotationInclusionFilter(annotationClass));
    244         }
    245     }
    246 
    247     /**
    248      * Skip tests annotated with given annotation class.
    249      *
    250      * @param notAnnotation the full class name of annotation
    251      */
    252     public void addAnnotationExclusionFilter(String notAnnotation) {
    253         Class<? extends Annotation> annotationClass = loadAnnotationClass(notAnnotation);
    254         if (annotationClass != null) {
    255             mFilter = mFilter.intersect(new AnnotationExclusionFilter(annotationClass));
    256         }
    257     }
    258 
    259     /**
    260      * Build a request that will generate test started and test ended events, but will skip actual
    261      * test execution.
    262      */
    263     public void setSkipExecution(boolean b) {
    264         mSkipExecution = b;
    265     }
    266 
    267     /**
    268      * Builds the {@link TestRequest} based on current contents of added classes and methods.
    269      * <p/>
    270      * If no classes have been explicitly added, will scan the classpath for all tests.
    271      *
    272      */
    273     public TestRequest build(Instrumentation instr, Bundle bundle) {
    274         if (mTestLoader.isEmpty()) {
    275             // no class restrictions have been specified. Load all classes
    276             loadClassesFromClassPath();
    277         }
    278 
    279         Request request = classes(instr, bundle, mSkipExecution, new Computer(),
    280                 mTestLoader.getLoadedClasses().toArray(new Class[0]));
    281         return new TestRequest(mTestLoader.getLoadFailures(), request.filterWith(mFilter));
    282     }
    283 
    284     /**
    285      * Create a <code>Request</code> that, when processed, will run all the tests
    286      * in a set of classes.
    287      *
    288      * @param instr the {@link Instrumentation} to inject into any tests that require it
    289      * @param bundle the {@link Bundle} of command line args to inject into any tests that require
    290      *         it
    291      * @param computer Helps construct Runners from classes
    292      * @param classes the classes containing the tests
    293      * @return a <code>Request</code> that will cause all tests in the classes to be run
    294      */
    295     private static Request classes(Instrumentation instr, Bundle bundle, boolean skipExecution,
    296             Computer computer, Class<?>... classes) {
    297         try {
    298             AndroidRunnerBuilder builder = new AndroidRunnerBuilder(true, instr, bundle,
    299                     skipExecution);
    300             Runner suite = computer.getSuite(builder, classes);
    301             return Request.runner(suite);
    302         } catch (InitializationError e) {
    303             throw new RuntimeException(
    304                     "Suite constructor, called as above, should always complete");
    305         }
    306     }
    307 
    308     private void loadClassesFromClassPath() {
    309         Collection<String> classNames = getClassNamesFromClassPath();
    310         for (String className : classNames) {
    311             mTestLoader.loadIfTest(className);
    312         }
    313     }
    314 
    315     private Collection<String> getClassNamesFromClassPath() {
    316         Log.i(LOG_TAG, String.format("Scanning classpath to find tests in apks %s",
    317                 Arrays.toString(mApkPaths)));
    318         ClassPathScanner scanner = new ClassPathScanner(mApkPaths);
    319 
    320         ChainedClassNameFilter filter =   new ChainedClassNameFilter();
    321          // exclude inner classes
    322         filter.add(new ExternalClassNameFilter());
    323         if (mTestPackageName != null) {
    324             // request to run only a specific java package, honor that
    325             filter.add(new InclusivePackageNameFilter(mTestPackageName));
    326         } else {
    327             // scan all packages, but exclude junit packages
    328             filter.addAll(new ExcludePackageNameFilter("junit"),
    329                     new ExcludePackageNameFilter("org.junit"),
    330                     new ExcludePackageNameFilter("org.hamcrest"),
    331                     new ExcludePackageNameFilter("com.android.test.runner.junit3"));
    332         }
    333 
    334         try {
    335             return scanner.getClassPathEntries(filter);
    336         } catch (IOException e) {
    337             mWriter.println("failed to scan classes");
    338             Log.e(LOG_TAG, "Failed to scan classes", e);
    339         }
    340         return Collections.emptyList();
    341     }
    342 
    343     /**
    344      * Factory method for {@link ClassPathScanner}.
    345      * <p/>
    346      * Exposed so unit tests can mock.
    347      */
    348     ClassPathScanner createClassPathScanner(String... apkPaths) {
    349         return new ClassPathScanner(apkPaths);
    350     }
    351 
    352     @SuppressWarnings("unchecked")
    353     private Class<? extends Annotation> loadAnnotationClass(String className) {
    354         try {
    355             Class<?> clazz = Class.forName(className);
    356             return (Class<? extends Annotation>)clazz;
    357         } catch (ClassNotFoundException e) {
    358             Log.e(LOG_TAG, String.format("Could not find annotation class: %s", className));
    359         } catch (ClassCastException e) {
    360             Log.e(LOG_TAG, String.format("Class %s is not an annotation", className));
    361         }
    362         return null;
    363     }
    364 }
    365