Home | History | Annotate | Download | only in aupt
      1 /*
      2  * Copyright (C) 2016 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 
     17 package android.support.test.aupt;
     18 
     19 import android.app.Instrumentation;
     20 import android.test.AndroidTestRunner;
     21 import android.util.Log;
     22 
     23 import dalvik.system.DexClassLoader;
     24 
     25 import junit.framework.AssertionFailedError;
     26 import junit.framework.Test;
     27 import junit.framework.TestCase;
     28 import junit.framework.TestListener;
     29 import junit.framework.TestResult;
     30 import junit.framework.TestSuite;
     31 
     32 import java.io.File;
     33 import java.util.ArrayList;
     34 import java.util.Enumeration;
     35 import java.util.List;
     36 import java.util.concurrent.Callable;
     37 import java.util.concurrent.ExecutionException;
     38 import java.util.concurrent.ExecutorService;
     39 import java.util.concurrent.Executors;
     40 import java.util.concurrent.Future;
     41 import java.util.concurrent.TimeUnit;
     42 import java.util.concurrent.TimeoutException;
     43 
     44 /**
     45  * A DexTestRunner runs tests by name from a given list of JARs,
     46  * with the following additional magic:
     47  *
     48  * - Custom ClassLoading from given dexed Jars
     49  * - Custom test scheduling (via Scheduler)
     50  *
     51  * In addition to the parameters in the constructor, be sure to run setTest or setTestClassName
     52  * before attempting to runTest.
     53  */
     54 class DexTestRunner extends AndroidTestRunner {
     55     private static final String LOG_TAG = DexTestRunner.class.getSimpleName();
     56 
     57     /* Constants */
     58     static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
     59     static final String DEX_OPT_PATH = "dex-test-opt";
     60 
     61     /* Private fields */
     62     private final List<TestListener> mTestListeners = new ArrayList<>();
     63     private final DexClassLoader mLoader;
     64     private final long mTestTimeoutMillis;
     65     private final long mSuiteTimeoutMillis;
     66 
     67     /* TestRunner State */
     68     protected TestResult mTestResult = new TestResult();
     69     protected List<TestCase> mTestCases = new ArrayList<>();
     70     protected String mTestClassName;
     71     protected Instrumentation mInstrumentation;
     72     protected Scheduler mScheduler;
     73     protected long mSuiteEndTime;
     74 
     75     /** A temporary ExecutorService to manage running the current test. */
     76     private ExecutorService mExecutorService;
     77 
     78     /** The current test. */
     79     private TestCase mTestCase;
     80 
     81     /* Field initialization */
     82     DexTestRunner(
     83             Instrumentation instrumentation,
     84             Scheduler scheduler,
     85             List<String> jars,
     86             long testTimeoutMillis,
     87             long suiteTimeoutMillis) {
     88         super();
     89 
     90         mInstrumentation = instrumentation;
     91         mScheduler = scheduler;
     92         mLoader = makeLoader(jars);
     93         mTestTimeoutMillis = testTimeoutMillis;
     94         mSuiteTimeoutMillis = suiteTimeoutMillis;
     95     }
     96 
     97     /* Main methods */
     98 
     99     @Override
    100     public void runTest() {
    101         runTest(newResult());
    102     }
    103 
    104     @Override
    105     public synchronized void runTest(final TestResult testResult) {
    106         mTestResult = testResult;
    107         mSuiteEndTime = System.currentTimeMillis() + mSuiteTimeoutMillis;
    108 
    109         for (final TestCase testCase : mScheduler.apply(mTestCases)) {
    110             // Timeout the suite if we've passed the end time.
    111             if (mSuiteTimeoutMillis != 0 && System.currentTimeMillis() > mSuiteEndTime) {
    112                 Log.w(LOG_TAG, String.format("Ending suite after %d mins running.",
    113                         TimeUnit.MILLISECONDS.toMinutes(mSuiteTimeoutMillis)));
    114                 break;
    115             }
    116 
    117             mExecutorService = Executors.newSingleThreadExecutor();
    118             mTestCase = testCase;
    119 
    120             // A Future that calls testCase::run. The reasoning behind using a thread here
    121             // is that AuptTestRunner should be able to interrupt it (via killTest) if it runs
    122             // too long; and interrupting the main thread here without actually exiting is tricky.
    123             Future<TestResult> result =
    124                     mExecutorService.submit(
    125                             new Callable<TestResult>() {
    126                                 @Override
    127                                 public TestResult call() throws Exception {
    128                                     testCase.run(testResult);
    129                                     return testResult;
    130                                 }
    131                             });
    132 
    133             try {
    134                 // Run our test-running thread and wait on it.
    135                 result.get(mTestTimeoutMillis, TimeUnit.MILLISECONDS);
    136             } catch (TimeoutException e) {
    137                 killTest(e);
    138             } catch (ExecutionException e) {
    139                 onError(testCase, e.getCause());
    140             } catch (InterruptedException e) {
    141                 Thread.currentThread().interrupt();
    142             } finally {
    143                 mExecutorService.shutdownNow();
    144                 mTestCase = null;
    145             }
    146         }
    147     }
    148 
    149     /** Interrupt the current test with the given exception. */
    150     void killTest(Exception e) {
    151         if (mTestCase != null) {
    152             // First, tell our listeners.
    153             onError(mTestCase, e);
    154 
    155             // Kill the test.
    156             mExecutorService.shutdownNow();
    157         }
    158     }
    159 
    160     /* TestCase Initialization */
    161 
    162     @Override
    163     public void setTestClassName(String className, String methodName) {
    164         mTestCases.clear();
    165         addTestClassByName(className, methodName);
    166     }
    167 
    168     void addTestClassByName(final String className, final String methodName) {
    169         try {
    170             final Class<?> testClass = mLoader.loadClass(className);
    171 
    172             if (Test.class.isAssignableFrom(testClass)) {
    173                 Test test = null;
    174 
    175                 try {
    176                     // Make sure it works
    177                     test = (Test) testClass.getConstructor().newInstance();
    178                 } catch (Exception e1) { /* If we fail, test will just stay null */ }
    179 
    180                 try {
    181                     test = (Test) testClass.getConstructor(String.class).newInstance(methodName);
    182                 } catch (Exception e2) { /* If we fail, test will just stay null */ }
    183 
    184                 addTest(test);
    185             } else {
    186                 throw new RuntimeException("Test class not found: " + className);
    187             }
    188         } catch (ClassNotFoundException ex) {
    189             throw new RuntimeException("Class not found: " + ex.getMessage());
    190         }
    191 
    192         if (mTestCases.isEmpty()) {
    193             throw new RuntimeException("No tests found in " + className + "#" + methodName);
    194         }
    195     }
    196 
    197     @Override
    198     public void setTest(Test test) {
    199         mTestCases.clear();
    200         addTest(test);
    201 
    202         // Update our test class name.
    203         if (TestSuite.class.isAssignableFrom(test.getClass())) {
    204             mTestClassName = ((TestSuite) test).getName();
    205         } else if (TestCase.class.isAssignableFrom(test.getClass())) {
    206             mTestClassName = ((TestCase) test).getName();
    207         } else {
    208             mTestClassName = test.getClass().getSimpleName();
    209         }
    210     }
    211 
    212     public void addTest(Test test) {
    213         if (test instanceof TestCase) {
    214 
    215             mTestCases.add((TestCase) test);
    216 
    217         } else if (test instanceof TestSuite) {
    218             Enumeration<Test> tests = ((TestSuite) test).tests();
    219 
    220             while (tests.hasMoreElements()) {
    221                 addTest(tests.nextElement());
    222             }
    223         } else {
    224             throw new RuntimeException("Tried to add invalid test: " + test.toString());
    225         }
    226     }
    227 
    228     /* State Manipulation Methods */
    229 
    230     @Override
    231     public void clearTestListeners() {
    232         mTestListeners.clear();
    233     }
    234 
    235     @Override
    236     public void addTestListener(TestListener testListener) {
    237         if (testListener != null) {
    238             mTestListeners.add(testListener);
    239             mTestResult.addListener(testListener);
    240         }
    241     }
    242 
    243     void addTestListenerIf(Boolean cond, TestListener testListener) {
    244         if (cond && testListener != null) {
    245             mTestListeners.add(testListener);
    246         }
    247     }
    248 
    249     @Override
    250     public List<TestCase> getTestCases() {
    251         return mTestCases;
    252     }
    253 
    254     @Override
    255     public void setInstrumentation(Instrumentation instrumentation) {
    256         mInstrumentation = instrumentation;
    257     }
    258 
    259     @Override
    260     public TestResult getTestResult() {
    261         return mTestResult;
    262     }
    263 
    264     @Override
    265     protected TestResult createTestResult() {
    266         return new TestResult();
    267     }
    268 
    269     @Override
    270     public String getTestClassName() {
    271         return mTestClassName;
    272     }
    273 
    274     /* Listener Exception Callback. */
    275 
    276     void onError(Test test, Throwable t) {
    277         if (t instanceof AssertionFailedError) {
    278             for (TestListener listener : mTestListeners) {
    279                 listener.addFailure(test, (AssertionFailedError) t);
    280             }
    281         } else {
    282             for (TestListener listener : mTestListeners) {
    283                 listener.addError(test, t);
    284             }
    285         }
    286     }
    287 
    288     /* Package-private Utilities */
    289 
    290     TestResult newResult() {
    291         TestResult result = new TestResult();
    292 
    293         for (TestListener listener: mTestListeners) {
    294             result.addListener(listener);
    295         }
    296 
    297         return result;
    298     }
    299 
    300     static List<String> parseDexedJarPaths(String jarString) {
    301         List<String> jars = new ArrayList<>();
    302 
    303         for (String jar : jarString.split(":")) {
    304             // Check that jar isn't empty, but don't fail because String::split will yield
    305             // spurious empty results if, for example, we don't specify any jars, accidentally
    306             // start with a leading colon, etc.
    307             if (!jar.trim().isEmpty()) {
    308                 File jarFile = jar.startsWith("/")
    309                         ? new File(jar)
    310                         : new File(DEFAULT_JAR_PATH + jar);
    311 
    312                 if (jarFile.exists()) {
    313                     jars.add(jarFile.getAbsolutePath());
    314                 } else {
    315                     throw new RuntimeException("Can't find jar file " + jarFile);
    316                 }
    317             }
    318         }
    319 
    320         return jars;
    321     }
    322 
    323     DexClassLoader getDexClassLoader() {
    324         return mLoader;
    325     }
    326 
    327     DexClassLoader makeLoader(List<String> jars) {
    328         StringBuilder jarFiles = new StringBuilder();
    329 
    330         for (String jar : jars) {
    331             if (new File(jar).exists() && new File(jar).canRead()) {
    332                 if (jarFiles.length() != 0) {
    333                     jarFiles.append(File.pathSeparator);
    334                 }
    335 
    336                 jarFiles.append(jar);
    337             } else {
    338                 throw new IllegalArgumentException(
    339                         "Jar file does not exist or not accessible: "  + jar);
    340             }
    341         }
    342 
    343         File optDir = new File(mInstrumentation.getTargetContext().getCacheDir(), DEX_OPT_PATH);
    344 
    345         if (optDir.exists() || optDir.mkdirs()) {
    346             return new DexClassLoader(
    347                     jarFiles.toString(),
    348                     optDir.getAbsolutePath(),
    349                     null,
    350                     DexTestRunner.class.getClassLoader());
    351         } else {
    352             throw new RuntimeException(
    353                     "Failed to create dex optimization directory: " + optDir.getAbsolutePath());
    354         }
    355     }
    356 }
    357