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