diff options
Diffstat (limited to 'src/io/appium/droiddriver/runner/TestRunner.java')
-rw-r--r-- | src/io/appium/droiddriver/runner/TestRunner.java | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/runner/TestRunner.java b/src/io/appium/droiddriver/runner/TestRunner.java new file mode 100644 index 0000000..ec97f9c --- /dev/null +++ b/src/io/appium/droiddriver/runner/TestRunner.java @@ -0,0 +1,218 @@ +/* + * 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 io.appium.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 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; + +import io.appium.droiddriver.helpers.DroidDrivers; +import io.appium.droiddriver.util.ActivityUtils; +import io.appium.droiddriver.util.ActivityUtils.Supplier; +import io.appium.droiddriver.util.Logs; + +/** + * Adds activity watcher to InstrumentationTestRunner. + */ +public class TestRunner extends InstrumentationTestRunner { + private final Set<Activity> activities = new HashSet<Activity>(); + 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} + * <p> + * 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<Activity>() { + @Override + public Activity get() { + return runningActivity; + } + }); + + super.onStart(); + } + + // Overrides InstrumentationTestRunner + List<Predicate<TestMethod>> getBuilderRequirements() { + List<Predicate<TestMethod>> requirements = new ArrayList<Predicate<TestMethod>>(); + requirements.add(new Predicate<TestMethod>() { + @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 extends Annotation> T getAnnotation(TestMethod testMethod, Class<T> 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<Void>(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; + } + } +} |