package com.google.android.apps.common.testing.testrunner;
import android.app.Activity;
import android.app.Application;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.test.AndroidTestRunner;
import android.test.InstrumentationTestRunner;
import android.test.TestSuiteProvider;
import android.util.Log;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestSuite;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Custom instrumentation runner for google android test cases.
*
*/
public class
GoogleInstrumentationTestRunner
extends
GoogleInstrumentation
implements TestSuiteProvider {
private static final long MILLIS_TO_WAIT_FOR_ACTIVITY_TO_STOP = TimeUnit.SECONDS.toMillis(2);
private static final String LOG_TAG = "GoogleInstrTest";
private BridgeTestRunner bridgeTestRunner = new BridgeTestRunner();
@Override
public void finish(int resultCode, Bundle results) {
try {
UsageTrackerRegistry.getInstance().sendUsages();
} catch (RuntimeException re) {
Log.w(LOG_TAG, "Failed to send analytics.", re);
}
super.finish(resultCode, results);
}
// ActivityUnitTestCase defaults to building the ComponentName via
// Activity.getClass().getPackage().getName(). This will cause a problem if the Java Package of
// the Activity is not the Android Package of the application, specifically
// Activity.getPackageName() will return an incorrect value. For example, android compatibility
// lib rev 19 is broken by this behaviour because it will eventually call through to
// PackageManager with Activity.getComponentName().getPackageName() and which PM will know nothing
// about.
@Override
public Activity newActivity(Class<?> clazz,
Context context,
IBinder token,
Application application,
Intent intent,
ActivityInfo info,
CharSequence title,
Activity parent,
String id,
Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException {
String activityClassPackageName = clazz.getPackage().getName();
String contextPackageName = context.getPackageName();
ComponentName intentComponentName = intent.getComponent();
if (!contextPackageName.equals(intentComponentName.getPackageName())) {
if (activityClassPackageName.equals(intentComponentName.getPackageName())) {
intent.setComponent(
new ComponentName(contextPackageName, intentComponentName.getClassName()));
}
}
return super.newActivity(clazz,
context,
token,
application,
intent,
info,
title,
parent,
id,
lastNonConfigurationInstance);
}
@Override
public void onCreate(Bundle arguments) {
super.onCreate(arguments);
mockitoWorkarounds();
String disableAnalyticsStringValue = arguments.getString(
"disableAnalytics");
boolean disableAnalytics = Boolean.parseBoolean(disableAnalyticsStringValue);
if (!disableAnalytics) {
UsageTracker tracker = new AnalyticsBasedUsageTracker.Builder(
getTargetContext()).buildIfPossible();
if (null != tracker) {
UsageTrackerRegistry.registerInstance(tracker);
}
}
Log.i(LOG_TAG, "Test Started!");
// bridge will call start()
bridgeTestRunner.onCreate(arguments);
}
@Override
public TestSuite getTestSuite() {
return bridgeTestRunner.getTestSuite();
}
/**
* Provides access to the underlying AndroidTestRunner.
*/
public AndroidTestRunner getAndroidTestRunner() {
return bridgeTestRunner.getAndroidTestRunner();
}
@Override
public void start() {
List<TestCase> testCases = bridgeTestRunner.getAndroidTestRunner().getTestCases();
// Register a listener to update the current test description.
bridgeTestRunner.getAndroidTestRunner().addTestListener(new TestListener() {
@Override
public void startTest(Test test) {
runOnMainSync(new ActivityFinisher());
}
@Override
public void endTest(Test test) {
}
@Override
public void addFailure(Test test, AssertionFailedError ae) {
}
@Override
public void addError(Test test, Throwable t) {
}
});
super.start();
}
@Override
public void onStart() {
// let the parent bring the app to a sane state.
super.onStart();
UsageTrackerRegistry.getInstance().trackUsage("TestRunner");
try {
// actually run tests!
bridgeTestRunner.onStart();
} finally {
}
}
private void mockitoWorkarounds() {
workaroundForMockitoOnEclair();
specifyDexMakerCacheProperty();
}
/**
* Enables the use of Mockito on Eclair (and below?).
*/
private static void workaroundForMockitoOnEclair() {
// This is a workaround for Eclair for http://code.google.com/p/mockito/issues/detail?id=354.
// Mockito loads the Android-specific MockMaker (provided by DexMaker) using the current
// thread's context ClassLoader. On Eclair this ClassLoader is set to the system ClassLoader
// which doesn't know anything about this app (which includes DexMaker). The workaround is to
// use the app's ClassLoader.
// TODO(user): Remove this workaround once Eclair is no longer supported.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ECLAIR_MR1) {
return;
}
// Make Mockito look up a MockMaker using the app's ClassLoader, by asking Mockito to create
// a mock of an interface (java.lang.Runnable).
ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(
GoogleInstrumentationTestRunner.class.getClassLoader());
// Since we don't require users of this class to use Mockito, we can only invoke Mockito via
// the Reflection API.
try {
Class mockitoClass = Class.forName("org.mockito.Mockito");
try {
// Invoke org.mockito.Mockito.mock(Runnable.class)
mockitoClass.getMethod("mock", Class.class).invoke(null, Runnable.class);
} catch (Exception e) {
throw new RuntimeException("Workaround for Mockito on Eclair and below failed", e);
}
} catch (ClassNotFoundException ignored) {
// Mockito not present -- no need to do anything
} finally {
Thread.currentThread().setContextClassLoader(originalContextClassLoader);
}
}
/**
* Bridge that allows us to use the argument processing / awareness of stock
* InstrumentationTestRunner along side the seperate inheritance hierarchy of
* GoogleInstrumentation(and TestRunner).
*
* This is regrettable but android's ITR is not very extension friendly.
* You may have to add additional method bridging in the future.
*/
private class BridgeTestRunner extends InstrumentationTestRunner {
private AndroidTestRunner myAndroidTestRunner = new AndroidTestRunner() {
@Override
public void setInstrumentation(Instrumentation instr) {
super.setInstrumentation(GoogleInstrumentationTestRunner.this);
}
@Override
public void setInstrumentaiton(Instrumentation instr) {
super.setInstrumentation(GoogleInstrumentationTestRunner.this);
}
};
@Override
public Context getTargetContext() {
return GoogleInstrumentationTestRunner.this.getTargetContext();
}
@Override
public Context getContext() {
return GoogleInstrumentationTestRunner.this.getContext();
}
@Override
public void start() {
GoogleInstrumentationTestRunner.this.start();
}
@Override
public AndroidTestRunner getAndroidTestRunner() {
return myAndroidTestRunner;
}
@Override
public void sendStatus(int resultCode, Bundle results) {
GoogleInstrumentationTestRunner.this.sendStatus(resultCode, results);
}
@Override
public void finish(int resultCode, Bundle results) {
GoogleInstrumentationTestRunner.this.finish(resultCode, results);
}
}
}