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