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