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 17package vogar.target; 18 19import com.google.common.annotations.VisibleForTesting; 20import java.io.File; 21import java.io.IOException; 22import java.io.InputStream; 23import java.io.PrintStream; 24import java.util.ArrayList; 25import java.util.Arrays; 26import java.util.HashSet; 27import java.util.Iterator; 28import java.util.List; 29import java.util.Properties; 30import java.util.Set; 31import java.util.concurrent.atomic.AtomicReference; 32import javax.annotation.Nullable; 33import vogar.Result; 34import vogar.RunnerType; 35import vogar.TestProperties; 36import vogar.monitor.TargetMonitor; 37import vogar.target.junit.JUnitRunnerFactory; 38 39/** 40 * Runs an action, in process on the target. 41 */ 42public 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