1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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 com.android.ide.eclipse.adt.internal.launch.junit.runtime; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.IDevice; 21 import com.android.ddmlib.ShellCommandUnresponsiveException; 22 import com.android.ddmlib.TimeoutException; 23 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize; 24 import com.android.ddmlib.testrunner.ITestRunListener; 25 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 26 import com.android.ddmlib.testrunner.TestIdentifier; 27 import com.android.ide.eclipse.adt.AdtPlugin; 28 import com.android.ide.eclipse.adt.internal.launch.LaunchMessages; 29 30 import org.eclipse.core.runtime.IProgressMonitor; 31 import org.eclipse.core.runtime.IStatus; 32 import org.eclipse.core.runtime.Status; 33 import org.eclipse.core.runtime.jobs.Job; 34 import org.eclipse.jdt.internal.junit.runner.IListensToTestExecutions; 35 import org.eclipse.jdt.internal.junit.runner.ITestReference; 36 import org.eclipse.jdt.internal.junit.runner.MessageIds; 37 import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner; 38 import org.eclipse.jdt.internal.junit.runner.TestExecution; 39 import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure; 40 41 import java.io.IOException; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * Supports Eclipse JUnit execution of Android tests. 48 * <p/> 49 * Communicates back to a Eclipse JDT JUnit client via a socket connection. 50 * 51 * @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol 52 */ 53 @SuppressWarnings("restriction") 54 public class RemoteAdtTestRunner extends RemoteTestRunner { 55 56 private static final String DELAY_MSEC_KEY = "delay_msec"; 57 /** the delay between each test execution when in collecting test info */ 58 private static final String COLLECT_TEST_DELAY_MS = "15"; 59 60 private AndroidJUnitLaunchInfo mLaunchInfo; 61 private TestExecution mExecution; 62 63 /** 64 * Initialize the JDT JUnit test runner parameters from the {@code args}. 65 * 66 * @param args name-value pair of arguments to pass to parent JUnit runner. 67 * @param launchInfo the Android specific test launch info 68 */ init(String[] args, AndroidJUnitLaunchInfo launchInfo)69 protected void init(String[] args, AndroidJUnitLaunchInfo launchInfo) { 70 defaultInit(args); 71 mLaunchInfo = launchInfo; 72 } 73 74 /** 75 * Runs a set of tests, and reports back results using parent class. 76 * <p/> 77 * JDT Unit expects to be sent data in the following sequence: 78 * <ol> 79 * <li>The total number of tests to be executed.</li> 80 * <li>The test 'tree' data about the tests to be executed, which is composed of the set of 81 * test class names, the number of tests in each class, and the names of each test in the 82 * class.</li> 83 * <li>The test execution result for each test method. Expects individual notifications of 84 * the test execution start, any failures, and the end of the test execution.</li> 85 * <li>The end of the test run, with its elapsed time.</li> 86 * </ol> 87 * <p/> 88 * In order to satisfy this, this method performs two actual Android instrumentation runs. 89 * The first is a 'log only' run that will collect the test tree data, without actually 90 * executing the tests, and send it back to JDT JUnit. The second is the actual test execution, 91 * whose results will be communicated back in real-time to JDT JUnit. 92 * 93 * The tests are run concurrently on all devices. The overall structure is as follows: 94 * <ol> 95 * <li> First, a separate job per device is run to collect test tree data. A per device 96 * {@link TestCollector} records information regarding the tests run on the device. 97 * </li> 98 * <li> Once all the devices have finished collecting the test tree data, the tree info is 99 * collected from all of them and passed to the Junit UI </li> 100 * <li> A job per device is again launched to do the actual test run. A per device 101 * {@link TestRunListener} notifies the shared {@link TestResultsNotifier} of test 102 * status. </li> 103 * <li> As tests complete, the test run listener updates the Junit UI </li> 104 * </ol> 105 * 106 * @param testClassNames ignored - the AndroidJUnitLaunchInfo will be used to determine which 107 * tests to run. 108 * @param testName ignored 109 * @param execution used to report test progress 110 */ 111 @Override runTests(String[] testClassNames, String testName, TestExecution execution)112 public void runTests(String[] testClassNames, String testName, TestExecution execution) { 113 // hold onto this execution reference so it can be used to report test progress 114 mExecution = execution; 115 116 List<IDevice> devices = new ArrayList<IDevice>(mLaunchInfo.getDevices()); 117 List<RemoteAndroidTestRunner> runners = 118 new ArrayList<RemoteAndroidTestRunner>(devices.size()); 119 120 for (IDevice device : devices) { 121 RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner( 122 mLaunchInfo.getAppPackage(), mLaunchInfo.getRunner(), device); 123 124 if (mLaunchInfo.getTestClass() != null) { 125 if (mLaunchInfo.getTestMethod() != null) { 126 runner.setMethodName(mLaunchInfo.getTestClass(), mLaunchInfo.getTestMethod()); 127 } else { 128 runner.setClassName(mLaunchInfo.getTestClass()); 129 } 130 } 131 132 if (mLaunchInfo.getTestPackage() != null) { 133 runner.setTestPackageName(mLaunchInfo.getTestPackage()); 134 } 135 136 TestSize size = mLaunchInfo.getTestSize(); 137 if (size != null) { 138 runner.setTestSize(size); 139 } 140 141 runners.add(runner); 142 } 143 144 // Launch all test info collector jobs 145 List<TestTreeCollectorJob> collectorJobs = 146 new ArrayList<TestTreeCollectorJob>(devices.size()); 147 List<TestCollector> perDeviceCollectors = new ArrayList<TestCollector>(devices.size()); 148 for (int i = 0; i < devices.size(); i++) { 149 RemoteAndroidTestRunner runner = runners.get(i); 150 String deviceName = devices.get(i).getName(); 151 TestCollector collector = new TestCollector(deviceName); 152 perDeviceCollectors.add(collector); 153 154 TestTreeCollectorJob job = new TestTreeCollectorJob( 155 "Test Tree Collector for " + deviceName, 156 runner, mLaunchInfo.isDebugMode(), collector); 157 job.setPriority(Job.INTERACTIVE); 158 job.schedule(); 159 160 collectorJobs.add(job); 161 } 162 163 // wait for all test info collector jobs to complete 164 int totalTests = 0; 165 for (TestTreeCollectorJob job : collectorJobs) { 166 try { 167 job.join(); 168 } catch (InterruptedException e) { 169 endTestRunWithError(e.getMessage()); 170 return; 171 } 172 173 if (!job.getResult().isOK()) { 174 endTestRunWithError(job.getResult().getMessage()); 175 return; 176 } 177 178 TestCollector collector = job.getCollector(); 179 String err = collector.getErrorMessage(); 180 if (err != null) { 181 endTestRunWithError(err); 182 return; 183 } 184 185 totalTests += collector.getTestCaseCount(); 186 } 187 188 AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Sending test information to Eclipse"); 189 notifyTestRunStarted(totalTests); 190 sendTestTrees(perDeviceCollectors); 191 192 List<TestRunnerJob> instrumentationRunnerJobs = 193 new ArrayList<TestRunnerJob>(devices.size()); 194 195 TestResultsNotifier notifier = new TestResultsNotifier(mExecution.getListener(), 196 devices.size()); 197 198 // Spawn all instrumentation runner jobs 199 for (int i = 0; i < devices.size(); i++) { 200 RemoteAndroidTestRunner runner = runners.get(i); 201 String deviceName = devices.get(i).getName(); 202 TestRunListener testRunListener = new TestRunListener(deviceName, notifier); 203 InstrumentationRunJob job = new InstrumentationRunJob( 204 "Test Tree Collector for " + deviceName, 205 runner, mLaunchInfo.isDebugMode(), testRunListener); 206 job.setPriority(Job.INTERACTIVE); 207 job.schedule(); 208 209 instrumentationRunnerJobs.add(job); 210 } 211 212 // Wait for all jobs to complete 213 for (TestRunnerJob job : instrumentationRunnerJobs) { 214 try { 215 job.join(); 216 } catch (InterruptedException e) { 217 endTestRunWithError(e.getMessage()); 218 return; 219 } 220 221 if (!job.getResult().isOK()) { 222 endTestRunWithError(job.getResult().getMessage()); 223 return; 224 } 225 } 226 } 227 228 /** Sends info about the test tree to be executed (ie the suites and their enclosed tests) */ sendTestTrees(List<TestCollector> perDeviceCollectors)229 private void sendTestTrees(List<TestCollector> perDeviceCollectors) { 230 for (TestCollector c : perDeviceCollectors) { 231 ITestReference ref = c.getDeviceSuite(); 232 ref.sendTree(this); 233 } 234 } 235 236 private static abstract class TestRunnerJob extends Job { 237 private ITestRunListener mListener; 238 private RemoteAndroidTestRunner mRunner; 239 private boolean mIsDebug; 240 TestRunnerJob(String name, RemoteAndroidTestRunner runner, boolean isDebug, ITestRunListener listener)241 public TestRunnerJob(String name, RemoteAndroidTestRunner runner, 242 boolean isDebug, ITestRunListener listener) { 243 super(name); 244 245 mRunner = runner; 246 mIsDebug = isDebug; 247 mListener = listener; 248 } 249 250 @Override run(IProgressMonitor monitor)251 protected IStatus run(IProgressMonitor monitor) { 252 try { 253 setupRunner(); 254 mRunner.run(mListener); 255 } catch (TimeoutException e) { 256 return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, 257 LaunchMessages.RemoteAdtTestRunner_RunTimeoutException, 258 e); 259 } catch (IOException e) { 260 return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, 261 String.format(LaunchMessages.RemoteAdtTestRunner_RunIOException_s, 262 e.getMessage()), 263 e); 264 } catch (AdbCommandRejectedException e) { 265 return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, 266 String.format( 267 LaunchMessages.RemoteAdtTestRunner_RunAdbCommandRejectedException_s, 268 e.getMessage()), 269 e); 270 } catch (ShellCommandUnresponsiveException e) { 271 return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, 272 LaunchMessages.RemoteAdtTestRunner_RunTimeoutException, 273 e); 274 } 275 276 return Status.OK_STATUS; 277 } 278 getRunner()279 public RemoteAndroidTestRunner getRunner() { 280 return mRunner; 281 } 282 isDebug()283 public boolean isDebug() { 284 return mIsDebug; 285 } 286 getListener()287 public ITestRunListener getListener() { 288 return mListener; 289 } 290 setupRunner()291 protected abstract void setupRunner(); 292 } 293 294 private static class TestTreeCollectorJob extends TestRunnerJob { TestTreeCollectorJob(String name, RemoteAndroidTestRunner runner, boolean isDebug, TestCollector listener)295 public TestTreeCollectorJob(String name, RemoteAndroidTestRunner runner, boolean isDebug, 296 TestCollector listener) { 297 super(name, runner, isDebug, listener); 298 } 299 300 @Override setupRunner()301 protected void setupRunner() { 302 RemoteAndroidTestRunner runner = getRunner(); 303 304 // set log only to just collect test case info, 305 // so Eclipse has correct test case count/tree info 306 runner.setLogOnly(true); 307 308 // add a small delay between each test. Otherwise for large test suites framework may 309 // report Binder transaction failures 310 runner.addInstrumentationArg(DELAY_MSEC_KEY, COLLECT_TEST_DELAY_MS); 311 } 312 getCollector()313 public TestCollector getCollector() { 314 return (TestCollector) getListener(); 315 } 316 } 317 318 private static class InstrumentationRunJob extends TestRunnerJob { InstrumentationRunJob(String name, RemoteAndroidTestRunner runner, boolean isDebug, ITestRunListener listener)319 public InstrumentationRunJob(String name, RemoteAndroidTestRunner runner, boolean isDebug, 320 ITestRunListener listener) { 321 super(name, runner, isDebug, listener); 322 } 323 324 @Override setupRunner()325 protected void setupRunner() { 326 RemoteAndroidTestRunner runner = getRunner(); 327 runner.setLogOnly(false); 328 runner.removeInstrumentationArg(DELAY_MSEC_KEY); 329 if (isDebug()) { 330 runner.setDebug(true); 331 } 332 } 333 } 334 335 /** 336 * Main entry method to run tests 337 * 338 * @param programArgs JDT JUnit program arguments to be processed by parent 339 * @param junitInfo the {@link AndroidJUnitLaunchInfo} containing info about this test ru 340 */ runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo)341 public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) { 342 init(programArgs, junitInfo); 343 run(); 344 } 345 346 /** 347 * Stop the current test run. 348 */ terminate()349 public void terminate() { 350 stop(); 351 } 352 353 @Override stop()354 protected void stop() { 355 if (mExecution != null) { 356 mExecution.stop(); 357 } 358 } 359 notifyTestRunEnded(long elapsedTime)360 private void notifyTestRunEnded(long elapsedTime) { 361 // copy from parent - not ideal, but method is private 362 sendMessage(MessageIds.TEST_RUN_END + elapsedTime); 363 flush(); 364 //shutDown(); 365 } 366 367 /** 368 * @param errorMessage 369 */ reportError(String errorMessage)370 private void reportError(String errorMessage) { 371 AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), 372 String.format(LaunchMessages.RemoteAdtTestRunner_RunFailedMsg_s, errorMessage)); 373 // is this needed? 374 //notifyTestRunStopped(-1); 375 } 376 endTestRunWithError(String message)377 private void endTestRunWithError(String message) { 378 reportError(message); 379 notifyTestRunEnded(0); 380 } 381 382 /** 383 * This class provides the interface to notify the JDT UI regarding the status of tests. 384 * When running tests on multiple devices, there is a {@link TestRunListener} that listens 385 * to results from each device. Rather than all such listeners directly notifying JDT 386 * from different threads, they all notify this class which notifies JDT. In addition, 387 * the {@link #testRunEnded(String, long)} method make sure that JDT is notified that the 388 * test run has completed only when tests on all devices have completed. 389 * */ 390 private class TestResultsNotifier { 391 private final IListensToTestExecutions mListener; 392 private final int mDeviceCount; 393 394 private int mCompletedRuns; 395 private long mMaxElapsedTime; 396 TestResultsNotifier(IListensToTestExecutions listener, int nDevices)397 public TestResultsNotifier(IListensToTestExecutions listener, int nDevices) { 398 mListener = listener; 399 mDeviceCount = nDevices; 400 } 401 testEnded(TestCaseReference ref)402 public synchronized void testEnded(TestCaseReference ref) { 403 mListener.notifyTestEnded(ref); 404 } 405 testFailed(TestReferenceFailure ref)406 public synchronized void testFailed(TestReferenceFailure ref) { 407 mListener.notifyTestFailed(ref); 408 } 409 testRunEnded(String mDeviceName, long elapsedTime)410 public synchronized void testRunEnded(String mDeviceName, long elapsedTime) { 411 mCompletedRuns++; 412 413 if (elapsedTime > mMaxElapsedTime) { 414 mMaxElapsedTime = elapsedTime; 415 } 416 417 if (mCompletedRuns == mDeviceCount) { 418 notifyTestRunEnded(mMaxElapsedTime); 419 } 420 } 421 testStarted(TestCaseReference testId)422 public synchronized void testStarted(TestCaseReference testId) { 423 mListener.notifyTestStarted(testId); 424 } 425 } 426 427 /** 428 * TestRunListener that communicates results in real-time back to JDT JUnit via the 429 * {@link TestResultsNotifier}. 430 * */ 431 private class TestRunListener implements ITestRunListener { 432 private final String mDeviceName; 433 private TestResultsNotifier mNotifier; 434 435 /** 436 * Constructs a {@link ITestRunListener} that listens for test results on given device. 437 * @param deviceName device on which the tests are being run 438 * @param notifier notifier to inform of test status 439 */ TestRunListener(String deviceName, TestResultsNotifier notifier)440 public TestRunListener(String deviceName, TestResultsNotifier notifier) { 441 mDeviceName = deviceName; 442 mNotifier = notifier; 443 } 444 445 @Override testEnded(TestIdentifier test, Map<String, String> ignoredTestMetrics)446 public void testEnded(TestIdentifier test, Map<String, String> ignoredTestMetrics) { 447 mNotifier.testEnded(new TestCaseReference(mDeviceName, test)); 448 } 449 450 @Override testFailed(TestIdentifier test, String trace)451 public void testFailed(TestIdentifier test, String trace) { 452 TestReferenceFailure failure = 453 new TestReferenceFailure(new TestCaseReference(mDeviceName, test), 454 MessageIds.TEST_FAILED, trace, null); 455 mNotifier.testFailed(failure); 456 } 457 458 @Override testAssumptionFailure(TestIdentifier test, String trace)459 public void testAssumptionFailure(TestIdentifier test, String trace) { 460 TestReferenceFailure failure = 461 new TestReferenceFailure(new TestCaseReference(mDeviceName, test), 462 MessageIds.TEST_FAILED, trace, null); 463 mNotifier.testFailed(failure); 464 } 465 466 @Override testIgnored(TestIdentifier test)467 public void testIgnored(TestIdentifier test) { 468 // TODO: implement me? 469 } 470 471 @Override testRunEnded(long elapsedTime, Map<String, String> runMetrics)472 public synchronized void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { 473 mNotifier.testRunEnded(mDeviceName, elapsedTime); 474 AdtPlugin.printToConsole(mLaunchInfo.getProject(), 475 LaunchMessages.RemoteAdtTestRunner_RunCompleteMsg); 476 } 477 478 @Override testRunFailed(String errorMessage)479 public synchronized void testRunFailed(String errorMessage) { 480 reportError(errorMessage); 481 } 482 483 @Override testRunStarted(String runName, int testCount)484 public synchronized void testRunStarted(String runName, int testCount) { 485 // ignore 486 } 487 488 @Override testRunStopped(long elapsedTime)489 public synchronized void testRunStopped(long elapsedTime) { 490 notifyTestRunStopped(elapsedTime); 491 AdtPlugin.printToConsole(mLaunchInfo.getProject(), 492 LaunchMessages.RemoteAdtTestRunner_RunStoppedMsg); 493 } 494 495 @Override testStarted(TestIdentifier test)496 public synchronized void testStarted(TestIdentifier test) { 497 TestCaseReference testId = new TestCaseReference(mDeviceName, test); 498 mNotifier.testStarted(testId); 499 } 500 } 501 502 /** Override parent to get extra logs. */ 503 @Override connect()504 protected boolean connect() { 505 boolean result = super.connect(); 506 if (!result) { 507 AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), 508 "Connect to Eclipse test result listener failed"); 509 } 510 return result; 511 } 512 513 /** Override parent to dump error message to console. */ 514 @Override runFailed(String message, Exception exception)515 public void runFailed(String message, Exception exception) { 516 if (exception != null) { 517 AdtPlugin.logAndPrintError(exception, mLaunchInfo.getProject().getName(), 518 "Test launch failed: %s", message); 519 } else { 520 AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), "Test launch failed: %s", 521 message); 522 } 523 } 524 } 525