/* * Copyright (C) 2013 DroidDriver committers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.droiddriver.runner; import android.annotation.TargetApi; import android.app.Activity; import android.os.Build; import android.os.Bundle; import android.test.AndroidTestRunner; import android.test.InstrumentationTestRunner; import android.test.suitebuilder.TestMethod; import android.util.Log; import com.android.internal.util.Predicate; import com.google.android.droiddriver.helpers.DroidDrivers; import com.google.android.droiddriver.util.ActivityUtils; import com.google.android.droiddriver.util.ActivityUtils.Supplier; import com.google.android.droiddriver.util.Logs; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestListener; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; /** * Adds activity watcher to InstrumentationTestRunner. */ public class TestRunner extends InstrumentationTestRunner { private final Set activities = new HashSet(); private final AndroidTestRunner androidTestRunner = new AndroidTestRunner(); private volatile Activity runningActivity; /** * Returns an {@link AndroidTestRunner} that is shared by this and super, such * that we can add custom {@link TestListener}s. */ @Override protected AndroidTestRunner getAndroidTestRunner() { return androidTestRunner; } /** * {@inheritDoc} *

* Adds a {@link TestListener} that finishes all created activities. */ @Override @TargetApi(18) public void onStart() { if (Build.VERSION.SDK_INT >= 18) { DroidDrivers.initInstrumentation(this, getArguments()); } getAndroidTestRunner().addTestListener(new TestListener() { @Override public void endTest(Test test) { // Try to finish activity on best-effort basis - TestListener should // not throw. final Activity[] activitiesCopy; synchronized (activities) { if (activities.isEmpty()) { return; } activitiesCopy = activities.toArray(new Activity[activities.size()]); } runOnMainSyncWithTimeLimit(new Runnable() { @Override public void run() { for (Activity activity : activitiesCopy) { if (!activity.isFinishing()) { try { Logs.log(Log.INFO, "Stopping activity: " + activity); activity.finish(); } catch (Throwable e) { Logs.log(Log.ERROR, e, "Failed to stop activity"); } } } } }); // We've done what we can. Clear activities if any are left. synchronized (activities) { activities.clear(); runningActivity = null; } } @Override public void addError(Test arg0, Throwable arg1) {} @Override public void addFailure(Test arg0, AssertionFailedError arg1) {} @Override public void startTest(Test arg0) {} }); ActivityUtils.setRunningActivitySupplier(new Supplier() { @Override public Activity get() { return runningActivity; } }); super.onStart(); } // Overrides InstrumentationTestRunner List> getBuilderRequirements() { List> requirements = new ArrayList>(); requirements.add(new Predicate() { @Override public boolean apply(TestMethod arg0) { MinSdkVersion minSdkVersion = getAnnotation(arg0, MinSdkVersion.class); if (minSdkVersion != null && minSdkVersion.value() > Build.VERSION.SDK_INT) { Logs.logfmt(Log.INFO, "filtered %s#%s: MinSdkVersion=%d", arg0.getEnclosingClassname(), arg0.getName(), minSdkVersion.value()); return false; } UseUiAutomation useUiAutomation = getAnnotation(arg0, UseUiAutomation.class); if (useUiAutomation != null && !DroidDrivers.hasUiAutomation()) { Logs.logfmt(Log.INFO, "filtered %s#%s: Has @UseUiAutomation, but ro.build.version.sdk=%d", arg0.getEnclosingClassname(), arg0.getName(), Build.VERSION.SDK_INT); return false; } return true; } private T getAnnotation(TestMethod testMethod, Class clazz) { T annotation = testMethod.getAnnotation(clazz); if (annotation == null) { annotation = testMethod.getEnclosingClass().getAnnotation(clazz); } return annotation; } }); return requirements; } @Override public void callActivityOnDestroy(Activity activity) { super.callActivityOnDestroy(activity); synchronized (activities) { activities.remove(activity); } } @Override public void callActivityOnCreate(Activity activity, Bundle bundle) { super.callActivityOnCreate(activity, bundle); synchronized (activities) { activities.add(activity); } } @Override public void callActivityOnResume(Activity activity) { super.callActivityOnResume(activity); runningActivity = activity; } @Override public void callActivityOnPause(Activity activity) { super.callActivityOnPause(activity); if (activity == runningActivity) { runningActivity = null; } } private boolean runOnMainSyncWithTimeLimit(Runnable runnable) { // Do we need it configurable? Now only used in endTest. long timeoutMillis = 10000L; final FutureTask futureTask = new FutureTask(runnable, null); new Thread(new Runnable() { @Override public void run() { runOnMainSync(futureTask); } }).start(); try { futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS); return true; } catch (Throwable e) { Logs.log(Log.WARN, e, String.format( "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync", timeoutMillis)); futureTask.cancel(false); return false; } } }