Home | History | Annotate | Download | only in target
      1 /*
      2  * Copyright (C) 2009 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 vogar.target;
     18 
     19 import com.google.common.annotations.VisibleForTesting;
     20 import java.io.File;
     21 import java.io.IOException;
     22 import java.io.InputStream;
     23 import java.io.PrintStream;
     24 import java.util.ArrayList;
     25 import java.util.Arrays;
     26 import java.util.HashSet;
     27 import java.util.Iterator;
     28 import java.util.List;
     29 import java.util.Properties;
     30 import java.util.Set;
     31 import java.util.concurrent.atomic.AtomicReference;
     32 import javax.annotation.Nullable;
     33 import vogar.Result;
     34 import vogar.RunnerType;
     35 import vogar.TestProperties;
     36 import vogar.monitor.TargetMonitor;
     37 import vogar.target.junit.JUnitRunnerFactory;
     38 
     39 /**
     40  * Runs an action, in process on the target.
     41  */
     42 public final class TestRunner {
     43 
     44     private final String qualifiedClassOrPackageName;
     45 
     46     /** the monitor port if a monitor is expected, or null for no monitor */
     47     @VisibleForTesting final Integer monitorPort;
     48 
     49     /** use an atomic reference so the runner can null it out when it is encountered. */
     50     private final AtomicReference<String> skipPastReference;
     51     private final int timeoutSeconds;
     52 
     53     private final RunnerFactory runnerFactory;
     54     private final String[] args;
     55     private boolean useSocketMonitor;
     56 
     57     public TestRunner(Properties properties, List<String> argsList) {
     58         qualifiedClassOrPackageName = properties.getProperty(TestProperties.TEST_CLASS_OR_PACKAGE);
     59         timeoutSeconds = Integer.parseInt(properties.getProperty(TestProperties.TIMEOUT));
     60 
     61         int monitorPort = Integer.parseInt(properties.getProperty(TestProperties.MONITOR_PORT));
     62         String skipPast = null;
     63 
     64         for (Iterator<String> i = argsList.iterator(); i.hasNext(); ) {
     65             String arg = i.next();
     66             if (arg.equals("--monitorPort")) {
     67                 i.remove();
     68                 monitorPort = Integer.parseInt(i.next());
     69                 i.remove();
     70             }
     71             if (arg.equals("--skipPast")) {
     72                 i.remove();
     73                 skipPast = i.next();
     74                 i.remove();
     75             }
     76         }
     77 
     78         // Select the RunnerFactory instances to use based on the selected runner type.
     79         RunnerType runnerType =
     80                 RunnerType.valueOf(properties.getProperty(TestProperties.RUNNER_TYPE));
     81         List<RunnerFactory> runnerFactories = new ArrayList<>();
     82         if (runnerType.supportsCaliper()) {
     83             runnerFactories.add(new CaliperRunnerFactory(argsList));
     84         }
     85         if (runnerType.supportsJUnit()) {
     86             runnerFactories.add(new JUnitRunnerFactory());
     87         }
     88         if (runnerType.supportsMain()) {
     89             runnerFactories.add(new MainRunnerFactory());
     90         }
     91         runnerFactory = new CompositeRunnerFactory(runnerFactories);
     92 
     93         this.monitorPort = monitorPort;
     94         this.skipPastReference = new AtomicReference<>(skipPast);
     95         this.args = argsList.toArray(new String[argsList.size()]);
     96     }
     97 
     98     /**
     99      * Load the properties that were either encapsulated in the APK (if using
    100      * {@link vogar.android.ActivityMode}), or encapsulated in the JAR compiled by Vogar (in other
    101      * modes).
    102      *
    103      * @return The {@link Properties} that were loaded.
    104      */
    105     public static Properties loadProperties() {
    106         try {
    107             InputStream in = getPropertiesStream();
    108             Properties properties = new Properties();
    109             properties.load(in);
    110             in.close();
    111             return properties;
    112         } catch (IOException e) {
    113             throw new RuntimeException(e);
    114         }
    115     }
    116 
    117     /**
    118      * Configure this test runner to await an incoming socket connection when
    119      * writing test results. Otherwise all communication happens over
    120      * System.out.
    121      */
    122     public void useSocketMonitor() {
    123         this.useSocketMonitor = true;
    124     }
    125 
    126     /**
    127      * Attempt to load the test properties file from both the application and system classloader.
    128      * This is necessary because sometimes we run tests from the boot classpath.
    129      */
    130     private static InputStream getPropertiesStream() throws IOException {
    131         for (Class<?> classToLoadFrom : new Class<?>[] { TestRunner.class, Object.class }) {
    132             InputStream propertiesStream = classToLoadFrom.getResourceAsStream(
    133                     "/" + TestProperties.FILE);
    134             if (propertiesStream != null) {
    135                 return propertiesStream;
    136             }
    137         }
    138         throw new IOException(TestProperties.FILE + " missing!");
    139     }
    140 
    141     public void run() throws IOException {
    142         final TargetMonitor monitor = useSocketMonitor
    143                 ? TargetMonitor.await(monitorPort)
    144                 : TargetMonitor.forPrintStream(System.out);
    145 
    146         PrintStream monitorPrintStream = new PrintStreamDecorator(System.out) {
    147             @Override public void print(String str) {
    148                 monitor.output(str != null ? str : "null");
    149             }
    150         };
    151         System.setOut(monitorPrintStream);
    152         System.setErr(monitorPrintStream);
    153 
    154         try {
    155             run(monitor);
    156         } catch (Throwable internalError) {
    157             internalError.printStackTrace(monitorPrintStream);
    158         } finally {
    159             monitor.close();
    160         }
    161     }
    162 
    163     private void run(final TargetMonitor monitor) {
    164         TestEnvironment testEnvironment = new TestEnvironment();
    165         testEnvironment.reset();
    166 
    167         String classOrPackageName;
    168         String qualification;
    169 
    170         // Check whether the class or package is qualified and, if so, strip it off and pass it
    171         // separately to the runners. For instance, may qualify a junit class by appending
    172         // #method_name, where method_name is the name of a single test of the class to run.
    173         int hash_position = qualifiedClassOrPackageName.indexOf("#");
    174         if (hash_position != -1) {
    175             classOrPackageName = qualifiedClassOrPackageName.substring(0, hash_position);
    176             qualification = qualifiedClassOrPackageName.substring(hash_position + 1);
    177         } else {
    178             classOrPackageName = qualifiedClassOrPackageName;
    179             qualification = null;
    180         }
    181 
    182         Set<Class<?>> classes = new ClassFinder().find(classOrPackageName);
    183 
    184         // if there is more than one class in the set, this must be a package. Since we're
    185         // running everything in the package already, remove any class called AllTests.
    186         if (classes.size() > 1) {
    187             Set<Class<?>> toRemove = new HashSet<>();
    188             for (Class<?> klass : classes) {
    189                 if (klass.getName().endsWith(".AllTests")) {
    190                     toRemove.add(klass);
    191                 }
    192             }
    193             classes.removeAll(toRemove);
    194         }
    195 
    196 
    197         for (Class<?> klass : classes) {
    198             TargetRunner targetRunner;
    199             try {
    200                 targetRunner = runnerFactory.newRunner(monitor, qualification, klass,
    201                         skipPastReference, testEnvironment, timeoutSeconds, args);
    202             } catch (RuntimeException e) {
    203                 monitor.outcomeStarted(klass.getName());
    204                 e.printStackTrace();
    205                 monitor.outcomeFinished(Result.ERROR);
    206                 return;
    207             }
    208 
    209             if (targetRunner == null) {
    210                 monitor.outcomeStarted(klass.getName());
    211                 System.out.println("Skipping " + klass.getName()
    212                         + ": no associated runner class");
    213                 monitor.outcomeFinished(Result.UNSUPPORTED);
    214                 continue;
    215             }
    216 
    217             boolean completedNormally = targetRunner.run();
    218             if (!completedNormally) {
    219                 return; // let the caller start another process
    220             }
    221         }
    222 
    223         monitor.completedNormally(true);
    224     }
    225 
    226     public static void main(String[] args) throws IOException {
    227         new TestRunner(loadProperties(), new ArrayList<>(Arrays.asList(args))).run();
    228         System.exit(0);
    229     }
    230 
    231     /**
    232      * A {@link RunnerFactory} that will traverse a list of {@link RunnerFactory} instances to find
    233      * one that can be used to run the code.
    234      */
    235     private static class CompositeRunnerFactory implements RunnerFactory {
    236 
    237         private final List<? extends RunnerFactory> runnerFactories;
    238 
    239         private CompositeRunnerFactory(List<RunnerFactory> factories) {
    240             this.runnerFactories = factories;
    241         }
    242 
    243         @Override @Nullable
    244         public TargetRunner newRunner(TargetMonitor monitor, String qualification,
    245                 Class<?> klass, AtomicReference<String> skipPastReference,
    246                 TestEnvironment testEnvironment, int timeoutSeconds, String[] args) {
    247             for (RunnerFactory runnerFactory : runnerFactories) {
    248                 TargetRunner targetRunner = runnerFactory.newRunner(monitor, qualification, klass,
    249                         skipPastReference, testEnvironment, timeoutSeconds, args);
    250                 if (targetRunner != null) {
    251                     return targetRunner;
    252                 }
    253             }
    254 
    255             return null;
    256         }
    257     }
    258 }
    259