Home | History | Annotate | Download | only in runtime
      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      */
     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
    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) */
    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 
    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
    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 
    279         public RemoteAndroidTestRunner getRunner() {
    280             return mRunner;
    281         }
    282 
    283         public boolean isDebug() {
    284             return mIsDebug;
    285         }
    286 
    287         public ITestRunListener getListener() {
    288             return mListener;
    289         }
    290 
    291         protected abstract void setupRunner();
    292     }
    293 
    294     private static class TestTreeCollectorJob extends TestRunnerJob {
    295         public TestTreeCollectorJob(String name, RemoteAndroidTestRunner runner, boolean isDebug,
    296                 TestCollector listener) {
    297             super(name, runner, isDebug, listener);
    298         }
    299 
    300         @Override
    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 
    313         public TestCollector getCollector() {
    314             return (TestCollector) getListener();
    315         }
    316     }
    317 
    318     private static class InstrumentationRunJob extends TestRunnerJob {
    319         public InstrumentationRunJob(String name, RemoteAndroidTestRunner runner, boolean isDebug,
    320                 ITestRunListener listener) {
    321             super(name, runner, isDebug, listener);
    322         }
    323 
    324         @Override
    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      */
    341     public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) {
    342         init(programArgs, junitInfo);
    343         run();
    344     }
    345 
    346     /**
    347      * Stop the current test run.
    348      */
    349     public void terminate() {
    350         stop();
    351     }
    352 
    353     @Override
    354     protected void stop() {
    355         if (mExecution != null) {
    356             mExecution.stop();
    357         }
    358     }
    359 
    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      */
    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 
    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 
    397         public TestResultsNotifier(IListensToTestExecutions listener, int nDevices) {
    398             mListener = listener;
    399             mDeviceCount = nDevices;
    400         }
    401 
    402         public synchronized void testEnded(TestCaseReference ref) {
    403             mListener.notifyTestEnded(ref);
    404         }
    405 
    406         public synchronized void testFailed(TestReferenceFailure ref) {
    407             mListener.notifyTestFailed(ref);
    408         }
    409 
    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 
    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          */
    440         public TestRunListener(String deviceName, TestResultsNotifier notifier) {
    441             mDeviceName = deviceName;
    442             mNotifier = notifier;
    443         }
    444 
    445         @Override
    446         public void testEnded(TestIdentifier test, Map<String, String> ignoredTestMetrics) {
    447             mNotifier.testEnded(new TestCaseReference(mDeviceName, test));
    448         }
    449 
    450         @Override
    451         public void testFailed(TestFailure status, TestIdentifier test, String trace) {
    452             String statusString;
    453             if (status == TestFailure.ERROR) {
    454                 statusString = MessageIds.TEST_ERROR;
    455             } else {
    456                 statusString = MessageIds.TEST_FAILED;
    457             }
    458             TestReferenceFailure failure =
    459                 new TestReferenceFailure(new TestCaseReference(mDeviceName, test),
    460                         statusString, trace, null);
    461             mNotifier.testFailed(failure);
    462         }
    463 
    464         @Override
    465         public synchronized void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
    466             mNotifier.testRunEnded(mDeviceName, elapsedTime);
    467             AdtPlugin.printToConsole(mLaunchInfo.getProject(),
    468                     LaunchMessages.RemoteAdtTestRunner_RunCompleteMsg);
    469         }
    470 
    471         @Override
    472         public synchronized void testRunFailed(String errorMessage) {
    473             reportError(errorMessage);
    474         }
    475 
    476         @Override
    477         public synchronized void testRunStarted(String runName, int testCount) {
    478             // ignore
    479         }
    480 
    481         @Override
    482         public synchronized void testRunStopped(long elapsedTime) {
    483             notifyTestRunStopped(elapsedTime);
    484             AdtPlugin.printToConsole(mLaunchInfo.getProject(),
    485                     LaunchMessages.RemoteAdtTestRunner_RunStoppedMsg);
    486         }
    487 
    488         @Override
    489         public synchronized void testStarted(TestIdentifier test) {
    490             TestCaseReference testId = new TestCaseReference(mDeviceName, test);
    491             mNotifier.testStarted(testId);
    492         }
    493     }
    494 
    495     /** Override parent to get extra logs. */
    496     @Override
    497     protected boolean connect() {
    498         boolean result = super.connect();
    499         if (!result) {
    500             AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
    501                     "Connect to Eclipse test result listener failed");
    502         }
    503         return result;
    504     }
    505 
    506     /** Override parent to dump error message to console. */
    507     @Override
    508     public void runFailed(String message, Exception exception) {
    509         if (exception != null) {
    510             AdtPlugin.logAndPrintError(exception, mLaunchInfo.getProject().getName(),
    511                     "Test launch failed: %s", message);
    512         } else {
    513             AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), "Test launch failed: %s",
    514                     message);
    515         }
    516     }
    517 }
    518