You've already forked android_translation_layer
mirror of
https://gitlab.com/android_translation_layer/android_translation_layer.git
synced 2025-10-27 11:48:10 -07:00
implement instrumentation
This commit is contained in:
16
meson.build
16
meson.build
@@ -24,15 +24,17 @@ libandroidfw_dep = [
|
||||
cc.find_library('androidfw', dirs : [ '/usr' / get_option('libdir') / 'art', '/usr/local' / get_option('libdir') / 'art', get_option('prefix') / get_option('libdir') / 'art' ]),
|
||||
]
|
||||
if fs.is_file('/usr' / get_option('libdir') / 'java/core-all_classes.jar')
|
||||
bootclasspath = '/usr' / get_option('libdir') / 'java/core-all_classes.jar'
|
||||
bootclasspath_dir = '/usr' / get_option('libdir') / 'java'
|
||||
elif fs.is_file('/usr/local' / get_option('libdir') / 'java/core-all_classes.jar')
|
||||
bootclasspath = '/usr/local' / get_option('libdir') / 'java/core-all_classes.jar'
|
||||
bootclasspath_dir = '/usr/local' / get_option('libdir') / 'java'
|
||||
elif fs.is_file(get_option('prefix') / get_option('libdir') / 'java/core-all_classes.jar')
|
||||
bootclasspath = get_option('prefix') / get_option('libdir') / 'java/core-all_classes.jar'
|
||||
bootclasspath_dir = get_option('prefix') / get_option('libdir') / 'java'
|
||||
else
|
||||
error('bootclasspath "core-all_classes.jar" not found')
|
||||
endif
|
||||
|
||||
bootclasspath = bootclasspath_dir / 'core-all_classes.jar' + ':' + bootclasspath_dir / 'core-junit_classes.jar' + ':' + bootclasspath_dir / 'junit-runner_classes.jar'
|
||||
|
||||
marshal_files = gnome.genmarshal('marshal',
|
||||
sources: 'src/api-impl-jni/widgets/marshal.list',
|
||||
valist_marshallers: true,
|
||||
@@ -194,6 +196,14 @@ custom_target('api-impl.jar', build_by_default: true, input: [hax_jar], output:
|
||||
install_dir : get_option('libdir') / 'java/dex/android_translation_layer',
|
||||
command: ['dx', '--dex', '--output='+join_paths(builddir_base, 'api-impl.jar'), hax_jar.full_path()])
|
||||
|
||||
# test-runner.jar
|
||||
subdir('src/test-runner')
|
||||
|
||||
custom_target('test_runner.jar', build_by_default: true, input: [test_runner_jar], output: ['test_runner.jar'],
|
||||
install: true,
|
||||
install_dir : get_option('libdir') / 'java/dex/android_translation_layer',
|
||||
command: ['dx', '--dex', '--output='+join_paths(builddir_base, 'test_runner.jar'), test_runner_jar.full_path()])
|
||||
|
||||
#framework-res.apk
|
||||
subdir('res')
|
||||
|
||||
|
||||
@@ -166,8 +166,11 @@ void set_up_handle_cache(JNIEnv *env)
|
||||
handle_cache.drawable.setBounds = _METHOD(handle_cache.drawable.class, "setBounds", "(IIII)V");
|
||||
|
||||
handle_cache.intent.class = _REF((*env)->FindClass(env, "android/content/Intent"));
|
||||
handle_cache.intent.constructor = _METHOD(handle_cache.intent.class, "<init>", "()V");
|
||||
handle_cache.intent.putExtraCharSequence = _METHOD(handle_cache.intent.class, "putExtra", "(Ljava/lang/String;Ljava/lang/CharSequence;)Landroid/content/Intent;");
|
||||
|
||||
handle_cache.instrumentation.class = _REF((*env)->FindClass(env, "android/app/Instrumentation"));
|
||||
|
||||
handle_cache.webview.class = _REF((*env)->FindClass(env, "android/webkit/WebView"));
|
||||
handle_cache.webview.internalGetAssetManager = _METHOD(handle_cache.webview.class, "internalGetAssetManager", "()Landroid/content/res/AssetManager;");
|
||||
handle_cache.webview.internalLoadChanged = _METHOD(handle_cache.webview.class, "internalLoadChanged", "(ILjava/lang/String;)V");
|
||||
|
||||
@@ -115,6 +115,7 @@ struct handle_cache {
|
||||
} drawable;
|
||||
struct {
|
||||
jclass class;
|
||||
jmethodID constructor;
|
||||
jmethodID putExtraCharSequence;
|
||||
} intent;
|
||||
struct {
|
||||
@@ -122,6 +123,11 @@ struct handle_cache {
|
||||
jmethodID internalGetAssetManager;
|
||||
jmethodID internalLoadChanged;
|
||||
} webview;
|
||||
struct {
|
||||
jclass class;
|
||||
jmethodID onCreate;
|
||||
jmethodID start;
|
||||
} instrumentation;
|
||||
};
|
||||
|
||||
extern struct handle_cache handle_cache;
|
||||
|
||||
381
src/api-impl/android/app/Instrumentation.java
Normal file
381
src/api-impl/android/app/Instrumentation.java
Normal file
@@ -0,0 +1,381 @@
|
||||
package android.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageParser;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.MessageQueue;
|
||||
import android.util.Slog;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/* for hacky classloader patching */
|
||||
import dalvik.system.DexFile;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public class Instrumentation {
|
||||
private static final String TAG = "Instrumentation";
|
||||
public static final String REPORT_KEY_IDENTIFIER = "id";
|
||||
public static final String REPORT_KEY_STREAMRESULT = "stream";
|
||||
|
||||
private static Instrumentation create(String className, Intent arguments) throws Exception {
|
||||
Thread.setUncaughtExceptionPreHandler(new ExceptionHandler());
|
||||
try {
|
||||
String target_package = null;
|
||||
for (PackageParser.Instrumentation instrumentation : Context.pkg.instrumentation) {
|
||||
if(className.equals(instrumentation.className)) {
|
||||
target_package = instrumentation.info.targetPackage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("targetPackage: " + target_package);
|
||||
|
||||
String target_path = android.os.Environment.getExternalStorageDirectory()+"/../_installed_apks_/"+target_package+".apk";
|
||||
|
||||
Context.this_application.getAssets().addAssetPath(target_path);
|
||||
|
||||
patchClassLoader(DexClassLoader.getSystemClassLoader(), new File(target_path));
|
||||
|
||||
Class<? extends Instrumentation> cls = Class.forName(className).asSubclass(Instrumentation.class);
|
||||
Constructor<? extends Instrumentation> constructor = cls.getConstructor();
|
||||
Instrumentation i = constructor.newInstance();
|
||||
i.onCreate(arguments.getExtras());
|
||||
|
||||
return i;
|
||||
} catch (Exception e) {
|
||||
/* there is no global handler for exceptions on the main thread */
|
||||
Thread.getUncaughtExceptionPreHandler().uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
return null; // we will never get here
|
||||
}
|
||||
|
||||
public Instrumentation() {
|
||||
}
|
||||
|
||||
public void start() {
|
||||
Thread t = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
//Looper.prepare();
|
||||
onStart();
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
}
|
||||
|
||||
public void onCreate(Bundle arguments) {
|
||||
}
|
||||
|
||||
public boolean onException(Object obj, Throwable e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onStart() {
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return new Context();
|
||||
}
|
||||
|
||||
public Context getTargetContext() {
|
||||
return new Context();
|
||||
}
|
||||
|
||||
public void setAutomaticPerformanceSnapshots() {
|
||||
}
|
||||
|
||||
public void setInTouchMode(boolean inTouch) {
|
||||
Slog.w(TAG, "FIXME: Instrumentation.setInTouchMode: " + inTouch);
|
||||
}
|
||||
|
||||
public void sendKeySync(KeyEvent event) {
|
||||
validateNotAppThread();
|
||||
/*long downTime = event.getDownTime();
|
||||
long eventTime = event.getEventTime();
|
||||
int source = event.getSource();
|
||||
if (source == InputDevice.SOURCE_UNKNOWN) {
|
||||
source = InputDevice.SOURCE_KEYBOARD;
|
||||
}
|
||||
if (eventTime == 0) {
|
||||
eventTime = SystemClock.uptimeMillis();
|
||||
}
|
||||
if (downTime == 0) {
|
||||
downTime = eventTime;
|
||||
}
|
||||
KeyEvent newEvent = new KeyEvent(event);
|
||||
newEvent.setTime(downTime, eventTime);
|
||||
newEvent.setSource(source);
|
||||
newEvent.setFlags(event.getFlags() | KeyEvent.FLAG_FROM_SYSTEM);
|
||||
setDisplayIfNeeded(newEvent);
|
||||
InputManagerGlobal.getInstance().injectInputEvent(newEvent,
|
||||
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);*/
|
||||
Slog.w(TAG, "FIXME: Instrumentation.sendKeySync: " + event);
|
||||
}
|
||||
|
||||
public void sendKeyDownUpSync(int keyCode) {
|
||||
sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
}
|
||||
|
||||
static public Application newApplication(Class<?> clazz, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
|
||||
return Context.this_application; // we don't (currently?) support multiple applications in a single process
|
||||
}
|
||||
|
||||
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 {
|
||||
Activity activity = (Activity)clazz.newInstance();
|
||||
activity.getWindow().native_window = Context.this_application.native_window;
|
||||
Slog.i(TAG, "activity.getWindow().native_window >"+activity.getWindow().native_window+"<");
|
||||
return activity;
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
public void callActivityOnCreate(Activity activity, Bundle savedState) {
|
||||
//prePerformCreate(activity);
|
||||
runOnMainSync(new Runnable() {
|
||||
public void run() {
|
||||
activity.onCreate(savedState);
|
||||
}
|
||||
});
|
||||
//postPerformCreate(activity);
|
||||
}
|
||||
|
||||
public Activity startActivitySync(Intent intent) {
|
||||
return startActivitySync(intent, null);
|
||||
}
|
||||
|
||||
/* TODO - deduplicate with startActivityForResult? */
|
||||
public Activity startActivitySync(Intent intent, Bundle options) {
|
||||
Slog.i(TAG, "startActivitySync(" + intent + ", " + options + ") called");
|
||||
if (intent.getComponent() != null) {
|
||||
try {
|
||||
Class<? extends Activity> cls = Class.forName(intent.getComponent().getClassName()).asSubclass(Activity.class);
|
||||
Constructor<? extends Activity> constructor = cls.getConstructor();
|
||||
final Activity activity = constructor.newInstance();
|
||||
activity.intent = intent;
|
||||
activity.getWindow().native_window = Context.this_application.native_window;
|
||||
runOnMainSync(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Activity.nativeStartActivity(activity);
|
||||
}
|
||||
});
|
||||
|
||||
return activity;
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
/* not sure what to do here */
|
||||
}
|
||||
} /*else if (FILE_CHOOSER_ACTIONS.contains(intent.getAction())) { // not sure what to do here either
|
||||
nativeFileChooser(FILE_CHOOSER_ACTIONS.indexOf(intent.getAction()), intent.getType(), intent.getStringExtra("android.intent.extra.TITLE"), requestCode);
|
||||
} */else {
|
||||
Slog.i(TAG, "startActivityForResult: intent was not handled.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void runOnMainSync(Runnable runner) {
|
||||
validateNotAppThread();
|
||||
SyncRunnable sr = new SyncRunnable(runner);
|
||||
new Handler(Looper.getMainLooper()).post(sr);
|
||||
sr.waitForComplete();
|
||||
}
|
||||
|
||||
public void sendStatus(int resultCode, Bundle results) {
|
||||
if (results != null) {
|
||||
for (String key : sorted(results.keySet())) {
|
||||
System.out.println("INSTRUMENTATION_STATUS: " + key + "=" + results.get(key));
|
||||
}
|
||||
}
|
||||
System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
|
||||
}
|
||||
|
||||
public void finish(int resultCode, Bundle results) {
|
||||
boolean need_hack = false;
|
||||
if (results != null) {
|
||||
for (String key : sorted(results.keySet())) {
|
||||
System.out.println("INSTRUMENTATION_RESULT: " + key + "=" + results.get(key));
|
||||
/* HACK: no idea why this isn't recognized as an error otherwise */
|
||||
if(((String)results.get(key)).contains("Test run aborted due to unexpected exception"))
|
||||
need_hack = true;
|
||||
}
|
||||
}
|
||||
if(need_hack) {
|
||||
System.out.println("INSTRUMENTATION_STATUS: shortMsg=ugly hack: Test run aborted due to unexpected exception");
|
||||
System.out.println("INSTRUMENTATION_STATUS_CODE: -1");
|
||||
}
|
||||
|
||||
System.out.println("INSTRUMENTATION_CODE: " + resultCode);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
private static Collection<String> sorted(Collection<String> list) {
|
||||
final ArrayList<String> copy = new ArrayList<>(list);
|
||||
Collections.sort(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
private static final class SyncRunnable implements Runnable {
|
||||
private final Runnable mTarget;
|
||||
private boolean mComplete;
|
||||
public SyncRunnable(Runnable target) {
|
||||
mTarget = target;
|
||||
}
|
||||
public void run() {
|
||||
mTarget.run();
|
||||
synchronized (this) {
|
||||
mComplete = true;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
public void waitForComplete() {
|
||||
synchronized (this) {
|
||||
while (!mComplete) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
private static final class EmptyRunnable implements Runnable {
|
||||
public void run() {}
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
private static final class Idler implements MessageQueue.IdleHandler {
|
||||
private final Runnable mCallback;
|
||||
private boolean mIdle;
|
||||
public Idler(Runnable callback) {
|
||||
mCallback = callback;
|
||||
mIdle = false;
|
||||
}
|
||||
public final boolean queueIdle() {
|
||||
if (mCallback != null) {
|
||||
mCallback.run();
|
||||
}
|
||||
synchronized (this) {
|
||||
mIdle = true;
|
||||
notifyAll();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public void waitForIdle() {
|
||||
synchronized (this) {
|
||||
while (!mIdle) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
public void waitForIdleSync() {
|
||||
/*validateNotAppThread();
|
||||
Idler idler = new Idler(null);
|
||||
Looper.myLooper().myQueue().addIdleHandler(idler);
|
||||
new Handler(Looper.myLooper()).post(new EmptyRunnable());
|
||||
idler.waitForIdle();*/
|
||||
}
|
||||
|
||||
/* Copyright (C) 2006 The Android Open Source Project */
|
||||
private final void validateNotAppThread() {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
throw new RuntimeException("This method can not be called from the main application thread");
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
//onException(null /*FIXME?*/, e);
|
||||
System.out.print("INSTRUMENTATION_RESULT: shortMsg=");
|
||||
e.printStackTrace();
|
||||
System.out.println("INSTRUMENTATION_STATUS_CODE: -1");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* -- a hacky method to patch in a classpath entry (there should be a better way *in theory*, but other approaches didn't work) -- */
|
||||
private static Object getFieldObject(Class cls, Object obj, String field_name) {
|
||||
try {
|
||||
Field field = cls.getDeclaredField(field_name);
|
||||
field.setAccessible(true);
|
||||
Object ret = field.get(obj);
|
||||
field.setAccessible(false);
|
||||
return ret;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setFieldObject(Class cls, Object obj, String field_name, Object value) {
|
||||
try {
|
||||
Field field = cls.getDeclaredField(field_name);
|
||||
field.setAccessible(true);
|
||||
field.set(obj, value);
|
||||
field.setAccessible(false);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object createObject(Class cls, Class[] type_array, Object[] value_array) {
|
||||
try {
|
||||
Constructor ctor = cls.getDeclaredConstructor(type_array);
|
||||
ctor.setAccessible(true);
|
||||
Object ret = ctor.newInstance(value_array);
|
||||
ctor.setAccessible(false);
|
||||
return ret;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void patchClassLoader(ClassLoader cl, File apk_path) throws IOException {
|
||||
// get cl.pathList
|
||||
Object pathList = getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");
|
||||
// get pathList.dexElements
|
||||
Object[] dexElements = (Object[]) getFieldObject(pathList.getClass(), pathList, "dexElements");
|
||||
// Element type
|
||||
Class<?> Element_class = dexElements.getClass().getComponentType();
|
||||
// Create an array to replace the original array
|
||||
Object[] DexElements_new = (Object[]) Array.newInstance(Element_class, dexElements.length + 1);
|
||||
// use this constructor: ElementDexFile.class(DexFile dexFile, File file)
|
||||
Class[] type_array = {DexFile.class, File.class};
|
||||
Object[] value_array = {DexFile.loadDex(apk_path.getCanonicalPath(), null, 0), apk_path};
|
||||
Object new_element = createObject(Element_class, type_array, value_array);
|
||||
Object[] new_element_wrapper_array = new Object[] {new_element};
|
||||
// Copy the original elements
|
||||
System.arraycopy(dexElements, 0, DexElements_new, 0, dexElements.length);
|
||||
// The element of the plugin is copied in
|
||||
System.arraycopy(new_element_wrapper_array, 0, DexElements_new, dexElements.length, new_element_wrapper_array.length);
|
||||
// replace
|
||||
setFieldObject(pathList.getClass(), pathList, "dexElements", DexElements_new);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Drawable.ConstantState;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
// import android.os.IBinder;
|
||||
import android.os.IBinder;
|
||||
import android.os.Trace;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -48,8 +48,6 @@ import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
class Movie {}
|
||||
|
||||
class IBinder {}
|
||||
|
||||
/**
|
||||
* Class for accessing an application's resources. This sits on top of the
|
||||
* asset manager of the application (accessible through {@link #getAssets}) and
|
||||
@@ -701,7 +699,10 @@ public class Resources {
|
||||
}
|
||||
getValue(id, value, true);
|
||||
}
|
||||
Drawable res = loadDrawable(value, id);
|
||||
Drawable res = null;
|
||||
try {
|
||||
res = loadDrawable(value, id);
|
||||
} catch (NotFoundException e) { e.printStackTrace(); }
|
||||
synchronized (mAccessLock) {
|
||||
if (mTmpValue == null) {
|
||||
mTmpValue = value;
|
||||
|
||||
@@ -35,6 +35,7 @@ hax_jar = jar('hax', [
|
||||
'android/app/Fragment.java',
|
||||
'android/app/FragmentManager.java',
|
||||
'android/app/FragmentTransaction.java',
|
||||
'android/app/Instrumentation.java',
|
||||
'android/app/IntentService.java',
|
||||
'android/app/KeyguardManager.java',
|
||||
'android/app/ListActivity.java',
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <gtk/gtk.h>
|
||||
#include <libportal/portal.h>
|
||||
|
||||
|
||||
#include "../api-impl-jni/defines.h"
|
||||
#include "../api-impl-jni/util.h"
|
||||
#include "../api-impl-jni/app/android_app_Activity.h"
|
||||
@@ -78,7 +77,7 @@ char *construct_classpath(char *prefix, char **cp_array, size_t len)
|
||||
|
||||
#define JDWP_ARG "-XjdwpOptions:transport=dt_socket,server=y,suspend=y,address="
|
||||
|
||||
JNIEnv *create_vm(char *api_impl_jar, char *apk_classpath, char *microg_apk, char *framework_res_apk, char *api_impl_natives_dir, char *app_lib_dir, char **extra_jvm_options)
|
||||
JNIEnv *create_vm(char *api_impl_jar, char *apk_classpath, char *microg_apk, char *framework_res_apk, char *test_runner_jar, char *api_impl_natives_dir, char *app_lib_dir, char **extra_jvm_options)
|
||||
{
|
||||
JavaVM *jvm;
|
||||
JNIEnv *env;
|
||||
@@ -106,7 +105,7 @@ JNIEnv *create_vm(char *api_impl_jar, char *apk_classpath, char *microg_apk, cha
|
||||
options[0].optionString = construct_classpath("-Djava.library.path=", (char *[]){api_impl_natives_dir, app_lib_dir}, 2);
|
||||
}
|
||||
|
||||
options[1].optionString = construct_classpath("-Djava.class.path=", (char *[]){api_impl_jar, apk_classpath, microg_apk, framework_res_apk}, 4);
|
||||
options[1].optionString = construct_classpath("-Djava.class.path=", (char *[]){api_impl_jar, apk_classpath, microg_apk, framework_res_apk, test_runner_jar}, 5);
|
||||
options[2].optionString = "-Xcheck:jni";
|
||||
if (jdwp_port) {
|
||||
strncat(jdwp_option_string, jdwp_port, 5); // 5 chars is enough for a port number, and won't overflow our array
|
||||
@@ -182,19 +181,23 @@ void dl_parse_library_path(const char *path, char *delim);
|
||||
#define REL_DEX_INSTALL_PATH "/../java/dex"
|
||||
|
||||
#define REL_API_IMPL_JAR_INSTALL_PATH "/android_translation_layer/api-impl.jar"
|
||||
#define REL_TEST_RUNNER_JAR_INSTALL_PATH "/android_translation_layer/test_runner.jar"
|
||||
#define REL_API_IMPL_NATIVES_INSTALL_PATH "/android_translation_layer/natives"
|
||||
#define REL_MICROG_APK_INSTALL_PATH "/microg/com.google.android.gms.apk"
|
||||
#define REL_FRAMEWORK_RES_INSTALL_PATH "/android_translation_layer/framework-res.apk"
|
||||
|
||||
#define API_IMPL_JAR_PATH_LOCAL "./api-impl.jar"
|
||||
#define TEST_RUNNER_JAR_PATH_LOCAL "./test_runner.jar"
|
||||
#define MICROG_APK_PATH_LOCAL "./com.google.android.gms.apk"
|
||||
#define FRAMEWORK_RES_PATH_LOCAL "./res/framework-res.apk"
|
||||
|
||||
struct jni_callback_data {
|
||||
char *apk_main_activity_class;
|
||||
char *apk_instrumentation_class;
|
||||
uint32_t window_width;
|
||||
uint32_t window_height;
|
||||
gboolean install;
|
||||
gboolean install_internal;
|
||||
char *prgname;
|
||||
char **extra_jvm_options;
|
||||
char **extra_string_keys;
|
||||
@@ -202,6 +205,27 @@ struct jni_callback_data {
|
||||
|
||||
static char *uri_option = NULL;
|
||||
|
||||
static void parse_string_extras(JNIEnv *env, char **extra_string_keys, jobject intent)
|
||||
{
|
||||
GError *error = NULL;
|
||||
GRegex *regex = g_regex_new("(?<!\\\\)=", 0, 0, &error);
|
||||
if (!regex) {
|
||||
fprintf(stderr, "g_regex_new error: '%s'\n", error->message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for (char **arg = extra_string_keys; *arg; arg++) {
|
||||
gchar **keyval = g_regex_split_full(regex, *arg, -1, 0, 0, 2, NULL);
|
||||
if (!keyval || !keyval[0] || !keyval[1]) {
|
||||
fprintf(stderr, "extra string arg not in 'key=value' format: '%s'\n", *arg);
|
||||
exit(1);
|
||||
}
|
||||
(*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraCharSequence, _JSTRING(keyval[0]), _JSTRING(keyval[1]));
|
||||
g_strfreev(keyval);
|
||||
}
|
||||
g_regex_unref(regex);
|
||||
}
|
||||
|
||||
static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *hint, struct jni_callback_data *d)
|
||||
{
|
||||
// TODO: pass all files to classpath
|
||||
@@ -213,6 +237,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
*/
|
||||
char *dex_install_dir;
|
||||
char *api_impl_jar;
|
||||
char *test_runner_jar = NULL;
|
||||
char *microg_apk = NULL;
|
||||
char *framework_res_apk = NULL;
|
||||
const char *package_name;
|
||||
@@ -231,7 +256,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
}
|
||||
|
||||
if (access(apk_classpath, F_OK) < 0) {
|
||||
printf("error: the specified file path doesn't seem to exist (%m)\n");
|
||||
printf("error: the specified file path (%s) doesn't seem to exist (%m)\n", apk_classpath);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -353,6 +378,30 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
}
|
||||
}
|
||||
|
||||
if(d->apk_instrumentation_class) {
|
||||
ret = stat(TEST_RUNNER_JAR_PATH_LOCAL, &dont_care);
|
||||
errno_localdir = errno;
|
||||
if (!ret) {
|
||||
test_runner_jar = strdup(TEST_RUNNER_JAR_PATH_LOCAL); // for running out of builddir; using strdup so we can always safely call free on this
|
||||
} else {
|
||||
char *test_runner_jar_install_dir = malloc(strlen(dex_install_dir) + strlen(REL_TEST_RUNNER_JAR_INSTALL_PATH) + 1); // +1 for NULL
|
||||
strcpy(test_runner_jar_install_dir, dex_install_dir);
|
||||
strcat(test_runner_jar_install_dir, REL_TEST_RUNNER_JAR_INSTALL_PATH);
|
||||
|
||||
ret = stat(test_runner_jar_install_dir, &dont_care);
|
||||
errno_libdir = errno;
|
||||
if (!ret) {
|
||||
test_runner_jar = test_runner_jar_install_dir;
|
||||
} else {
|
||||
printf("warning: can't stat test_runner.jar; tried:\n"
|
||||
"\t\"" TEST_RUNNER_JAR_PATH_LOCAL "\", got - %s\n"
|
||||
"\t\"%s\", got - %s\n",
|
||||
strerror(errno_localdir),
|
||||
test_runner_jar_install_dir, strerror(errno_libdir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char *api_impl_natives_dir = malloc(strlen(dex_install_dir) + strlen(REL_API_IMPL_NATIVES_INSTALL_PATH) + 1); // +1 for NULL
|
||||
strcpy(api_impl_natives_dir, dex_install_dir);
|
||||
strcat(api_impl_natives_dir, REL_API_IMPL_NATIVES_INSTALL_PATH);
|
||||
@@ -371,7 +420,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
dl_parse_library_path(ld_path, ":");
|
||||
g_free(ld_path);
|
||||
|
||||
JNIEnv *env = create_vm(api_impl_jar, apk_classpath, microg_apk, framework_res_apk, api_impl_natives_dir, app_lib_dir, d->extra_jvm_options);
|
||||
JNIEnv *env = create_vm(api_impl_jar, apk_classpath, microg_apk, framework_res_apk, test_runner_jar, api_impl_natives_dir, app_lib_dir, d->extra_jvm_options);
|
||||
|
||||
free(app_lib_dir);
|
||||
|
||||
@@ -443,7 +492,29 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
(*env)->ExceptionDescribe(env);
|
||||
|
||||
if (d->apk_instrumentation_class) {
|
||||
if (d->apk_main_activity_class) {
|
||||
fprintf(stderr, "error: both --instrument and --launch-activity supplied, exiting\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
jobject intent = NULL;
|
||||
|
||||
if (d->extra_string_keys) {
|
||||
intent = (*env)->NewObject(env, handle_cache.intent.class, handle_cache.intent.constructor);
|
||||
parse_string_extras(env, d->extra_string_keys, intent);
|
||||
}
|
||||
|
||||
(*env)->CallStaticObjectMethod(env, handle_cache.instrumentation.class,
|
||||
_STATIC_METHOD(handle_cache.instrumentation.class, "create", "(Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Instrumentation;"),
|
||||
_JSTRING(d->apk_instrumentation_class), intent);
|
||||
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
(*env)->ExceptionDescribe(env);
|
||||
}
|
||||
|
||||
// construct main Activity
|
||||
if (!d->apk_instrumentation_class && !d->install_internal) {
|
||||
activity_object = (*env)->CallStaticObjectMethod(env, handle_cache.activity.class,
|
||||
_STATIC_METHOD(handle_cache.activity.class, "createMainActivity", "(Ljava/lang/String;JLjava/lang/String;)Landroid/app/Activity;"),
|
||||
_JSTRING(d->apk_main_activity_class), _INTPTR(window), (uri_option && *uri_option) ? _JSTRING(uri_option) : NULL);
|
||||
@@ -453,41 +524,31 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
g_free(uri_option);
|
||||
|
||||
if (d->extra_string_keys) {
|
||||
GError *error = NULL;
|
||||
GRegex *regex = g_regex_new("(?<!\\\\)=", 0, 0, &error);
|
||||
if (!regex) {
|
||||
fprintf(stderr, "g_regex_new error: '%s'\n", error->message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
jobject intent = _GET_OBJ_FIELD(activity_object, "intent", "Landroid/content/Intent;");
|
||||
|
||||
for (char **arg = d->extra_string_keys; *arg; arg++) {
|
||||
gchar **keyval = g_regex_split_full(regex, *arg, -1, 0, 0, 2, NULL);
|
||||
if (!keyval || !keyval[0] || !keyval[1]) {
|
||||
fprintf(stderr, "extra string arg not in 'key=value' format: '%s'\n", *arg);
|
||||
exit(1);
|
||||
}
|
||||
(*env)->CallObjectMethod(env, intent, handle_cache.intent.putExtraCharSequence, _JSTRING(keyval[0]), _JSTRING(keyval[1]));
|
||||
g_strfreev(keyval);
|
||||
}
|
||||
g_regex_unref(regex);
|
||||
parse_string_extras(env, d->extra_string_keys, intent);
|
||||
g_strfreev(d->extra_string_keys);
|
||||
}
|
||||
|
||||
}
|
||||
/* -- set the window title and app icon -- */
|
||||
|
||||
jstring package_name_jstr = (*env)->CallObjectMethod(env, activity_object, handle_cache.context.get_package_name);
|
||||
if (!d->apk_instrumentation_class) {
|
||||
jstring package_name_jstr = (*env)->CallObjectMethod(env, application_object, handle_cache.context.get_package_name);
|
||||
package_name = package_name_jstr ? _CSTRING(package_name_jstr) : NULL;
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
(*env)->ExceptionDescribe(env);
|
||||
}
|
||||
|
||||
jstring app_icon_path_jstr = (*env)->CallObjectMethod(env, application_object, handle_cache.application.get_app_icon_path);
|
||||
const char *app_icon_path = app_icon_path_jstr ? _CSTRING(app_icon_path_jstr) : NULL;
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
(*env)->ExceptionDescribe(env);
|
||||
|
||||
if (d->install) {
|
||||
if (d->install || d->install_internal) {
|
||||
if (d->apk_instrumentation_class) {
|
||||
fprintf(stderr, "error: --instrument supplied together with --install, exiting\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
XdpPortal *portal = xdp_portal_new();
|
||||
|
||||
const char *app_label = _CSTRING((*env)->CallObjectMethod(env, application_object, _METHOD(handle_cache.application.class, "get_app_label", "()Ljava/lang/String;")));
|
||||
@@ -495,7 +556,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
(*env)->ExceptionDescribe(env);
|
||||
|
||||
GVariant *icon_serialized = NULL;
|
||||
if (app_icon_path) {
|
||||
if (app_icon_path && !d->install_internal) {
|
||||
extract_from_apk(app_icon_path, app_icon_path);
|
||||
char *app_icon_path_full = g_strdup_printf("%s/%s", app_data_dir, app_icon_path);
|
||||
GMappedFile *icon_file = g_mapped_file_new(app_icon_path_full, FALSE, NULL);
|
||||
@@ -507,10 +568,19 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
g_mapped_file_unref(icon_file);
|
||||
g_free(app_icon_path_full);
|
||||
}
|
||||
GFile *dest = g_file_new_build_filename(app_data_dir_base, "_installed_apks_", apk_name, NULL);
|
||||
|
||||
gchar *dest_name = g_strdup_printf("%s.apk", package_name);
|
||||
GFile *dest = g_file_new_build_filename(app_data_dir_base, "_installed_apks_", d->install_internal ? dest_name : apk_name, NULL);
|
||||
free(dest_name);
|
||||
printf("installing %s to %s\n", apk_name, g_file_get_path(dest));
|
||||
g_file_make_directory(g_file_get_parent(dest), NULL, NULL);
|
||||
g_file_copy(files[0], dest, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, NULL);
|
||||
GError *err = NULL;
|
||||
g_file_copy(files[0], dest, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &err);
|
||||
if(err)
|
||||
printf("error copying apk: %s\n", err->message);
|
||||
|
||||
if(d->install_internal)
|
||||
exit(0);
|
||||
|
||||
jmethodID get_supported_mime_types = _METHOD(handle_cache.application.class, "get_supported_mime_types", "()Ljava/lang/String;");
|
||||
jstring supported_mime_types_jstr = (*env)->CallObjectMethod(env, application_object, get_supported_mime_types);
|
||||
@@ -550,6 +620,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d->apk_instrumentation_class)
|
||||
gtk_window_set_title(GTK_WINDOW(window), package_name);
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), d->window_width, d->window_height);
|
||||
g_signal_connect(window, "close-request", G_CALLBACK(app_exit), env);
|
||||
@@ -563,7 +634,7 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
|
||||
// set package name as application id for window icon on Wayland. Needs a {package_name}.desktop file defining the icon
|
||||
GdkToplevel *toplevel = GDK_TOPLEVEL(gtk_native_get_surface(GTK_NATIVE(window)));
|
||||
if (GDK_IS_WAYLAND_TOPLEVEL(toplevel)) {
|
||||
if (GDK_IS_WAYLAND_TOPLEVEL(toplevel) && !d->apk_instrumentation_class) {
|
||||
gdk_wayland_toplevel_set_application_id(GDK_WAYLAND_TOPLEVEL(toplevel), package_name);
|
||||
}
|
||||
GdkMonitor *monitor = gdk_display_get_monitor_at_surface(gdk_display_get_default(), GDK_SURFACE(toplevel));
|
||||
@@ -593,9 +664,11 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h
|
||||
g_signal_connect_after(window, "realize", G_CALLBACK(icon_override), icon_list);
|
||||
}
|
||||
|
||||
if (!d->apk_instrumentation_class) {
|
||||
activity_start(env, activity_object);
|
||||
|
||||
g_timeout_add(10, G_SOURCE_FUNC(hacky_on_window_focus_changed_callback), env);
|
||||
}
|
||||
|
||||
jobject input_queue_callback = g_object_get_data(G_OBJECT(window), "input_queue_callback");
|
||||
if (input_queue_callback) {
|
||||
@@ -626,9 +699,11 @@ void init_cmd_parameters(GApplication *app, struct jni_callback_data *d)
|
||||
const GOptionEntry cmd_params[] = {
|
||||
/* long_name | short_name | flags | arg | arg_data | description | arg_desc */
|
||||
{ "launch-activity", 'l', 0, G_OPTION_ARG_STRING, &d->apk_main_activity_class, "the fully qualifed name of the activity you wish to launch (usually the apk's main activity)", "ACTIVITY_NAME" },
|
||||
{ "instrument", 0, 0, G_OPTION_ARG_STRING, &d->apk_instrumentation_class, "the fully qualifed name of the instrumentation you wish to launch", "CLASS_NAME" },
|
||||
{ "window-width", 'w', 0, G_OPTION_ARG_INT, &d->window_width, "window width to launch with (some apps react poorly to runtime window size adjustments)", "WIDTH" },
|
||||
{ "window-height", 'h', 0, G_OPTION_ARG_INT, &d->window_height, "window height to launch with (some apps react poorly to runtime window size adjustments)", "HEIGHT" },
|
||||
{ "install", 'i', 0, G_OPTION_ARG_NONE, &d->install, "install .desktop file for the given apk", NULL },
|
||||
{ "install-internal", 0 , 0, G_OPTION_ARG_NONE, &d->install_internal, "copy an apk to _installed_apks_ but don't create a desktop entry", NULL },
|
||||
{ "extra-jvm-option", 'X', 0, G_OPTION_ARG_STRING_ARRAY, &d->extra_jvm_options, "pass an additional option directly to art (e.g -X \"-verbose:jni\")", "\"OPTION\"" },
|
||||
{ "extra-string-key", 'e', 0, G_OPTION_ARG_STRING_ARRAY, &d->extra_string_keys, "pass a string extra (-e key=value)", "\"KEY=VALUE\"" },
|
||||
{ "uri", 'u', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, option_uri_cb, "open the given URI inside the application", "URI" },
|
||||
@@ -655,9 +730,11 @@ int main(int argc, char **argv)
|
||||
|
||||
struct jni_callback_data *callback_data = malloc(sizeof(struct jni_callback_data));
|
||||
callback_data->apk_main_activity_class = NULL;
|
||||
callback_data->apk_instrumentation_class = NULL;
|
||||
callback_data->window_width = 960;
|
||||
callback_data->window_height = 540;
|
||||
callback_data->install = FALSE;
|
||||
callback_data->install_internal = FALSE;
|
||||
callback_data->prgname = argv[0];
|
||||
callback_data->extra_jvm_options = NULL;
|
||||
callback_data->extra_string_keys = NULL;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* This class provides functional testing of a single activity. The activity under test will
|
||||
* be created using the system infrastructure (by calling InstrumentationTestCase.launchActivity())
|
||||
* and you will then be able to manipulate your Activity directly. Most of the work is handled
|
||||
* automatically here by {@link #setUp} and {@link #tearDown}.
|
||||
*
|
||||
* <p>If you prefer an isolated unit test, see {@link android.test.ActivityUnitTestCase}.
|
||||
*
|
||||
* @deprecated new tests should be written using
|
||||
* {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for
|
||||
* configuring the Activity under test
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class ActivityInstrumentationTestCase<T extends Activity>
|
||||
extends ActivityTestCase {
|
||||
String mPackage;
|
||||
Class<T> mActivityClass;
|
||||
boolean mInitialTouchMode = false;
|
||||
|
||||
/**
|
||||
* Creates an {@link ActivityInstrumentationTestCase} in non-touch mode.
|
||||
*
|
||||
* @param pkg ignored - no longer in use.
|
||||
* @param activityClass The activity to test. This must be a class in the instrumentation
|
||||
* targetPackage specified in the AndroidManifest.xml
|
||||
*/
|
||||
public ActivityInstrumentationTestCase(String pkg, Class<T> activityClass) {
|
||||
this(pkg, activityClass, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link ActivityInstrumentationTestCase}.
|
||||
*
|
||||
* @param pkg ignored - no longer in use.
|
||||
* @param activityClass The activity to test. This must be a class in the instrumentation
|
||||
* targetPackage specified in the AndroidManifest.xml
|
||||
* @param initialTouchMode true = in touch mode
|
||||
*/
|
||||
public ActivityInstrumentationTestCase(String pkg, Class<T> activityClass,
|
||||
boolean initialTouchMode) {
|
||||
mActivityClass = activityClass;
|
||||
mInitialTouchMode = initialTouchMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getActivity() {
|
||||
return (T) super.getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
// set initial touch mode
|
||||
getInstrumentation().setInTouchMode(mInitialTouchMode);
|
||||
final String targetPackageName = getInstrumentation().getTargetContext().getPackageName();
|
||||
setActivity(launchActivity(targetPackageName, mActivityClass, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
getActivity().finish();
|
||||
setActivity(null);
|
||||
|
||||
// Scrub out members - protects against memory leaks in the case where someone
|
||||
// creates a non-static inner class (thus referencing the test case) and gives it to
|
||||
// someone else to hold onto
|
||||
scrubClass(ActivityInstrumentationTestCase.class);
|
||||
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public void testActivityTestCaseSetUpProperly() throws Exception {
|
||||
assertNotNull("activity should be launched successfully", getActivity());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* This class provides functional testing of a single activity. The activity under test will
|
||||
* be created using the system infrastructure (by calling InstrumentationTestCase.launchActivity())
|
||||
* and you will then be able to manipulate your Activity directly.
|
||||
*
|
||||
* <p>Other options supported by this test case include:
|
||||
* <ul>
|
||||
* <li>You can run any test method on the UI thread (see {@link android.test.UiThreadTest}).</li>
|
||||
* <li>You can inject custom Intents into your Activity (see
|
||||
* {@link #setActivityIntent(Intent)}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>This class replaces {@link android.test.ActivityInstrumentationTestCase}, which is deprecated.
|
||||
* New tests should be written using this base class.
|
||||
*
|
||||
* <p>If you prefer an isolated unit test, see {@link android.test.ActivityUnitTestCase}.
|
||||
*
|
||||
* @deprecated Use
|
||||
* <a href="{@docRoot}reference/android/support/test/rule/ActivityTestRule.html">
|
||||
* ActivityTestRule</a> instead. New tests should be written using the
|
||||
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class ActivityInstrumentationTestCase2<T extends Activity>
|
||||
extends ActivityTestCase {
|
||||
Class<T> mActivityClass;
|
||||
boolean mInitialTouchMode = false;
|
||||
Intent mActivityIntent = null;
|
||||
|
||||
/**
|
||||
* Creates an {@link ActivityInstrumentationTestCase2}.
|
||||
*
|
||||
* @param pkg ignored - no longer in use.
|
||||
* @param activityClass The activity to test. This must be a class in the instrumentation
|
||||
* targetPackage specified in the AndroidManifest.xml
|
||||
*
|
||||
* @deprecated use {@link #ActivityInstrumentationTestCase2(Class)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public ActivityInstrumentationTestCase2(String pkg, Class<T> activityClass) {
|
||||
this(activityClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link ActivityInstrumentationTestCase2}.
|
||||
*
|
||||
* @param activityClass The activity to test. This must be a class in the instrumentation
|
||||
* targetPackage specified in the AndroidManifest.xml
|
||||
*/
|
||||
public ActivityInstrumentationTestCase2(Class<T> activityClass) {
|
||||
mActivityClass = activityClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Activity under test, starting it if necessary.
|
||||
*
|
||||
* For each test method invocation, the Activity will not actually be created until the first
|
||||
* time this method is called.
|
||||
*
|
||||
* <p>If you wish to provide custom setup values to your Activity, you may call
|
||||
* {@link #setActivityIntent(Intent)} and/or {@link #setActivityInitialTouchMode(boolean)}
|
||||
* before your first call to getActivity(). Calling them after your Activity has
|
||||
* started will have no effect.
|
||||
*
|
||||
* <p><b>NOTE:</b> Activities under test may not be started from within the UI thread.
|
||||
* If your test method is annotated with {@link android.test.UiThreadTest}, then your Activity
|
||||
* will be started automatically just before your test method is run. You still call this
|
||||
* method in order to get the Activity under test.
|
||||
*
|
||||
* @return the Activity under test
|
||||
*/
|
||||
@Override
|
||||
public T getActivity() {
|
||||
Activity a = super.getActivity();
|
||||
if (a == null) {
|
||||
// set initial touch mode
|
||||
getInstrumentation().setInTouchMode(mInitialTouchMode);
|
||||
final String targetPackage = getInstrumentation().getTargetContext().getPackageName();
|
||||
// inject custom intent, if provided
|
||||
if (mActivityIntent == null) {
|
||||
a = launchActivity(targetPackage, mActivityClass, null);
|
||||
} else {
|
||||
a = launchActivityWithIntent(targetPackage, mActivityClass, mActivityIntent);
|
||||
}
|
||||
setActivity(a);
|
||||
}
|
||||
return (T) a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method before the first call to {@link #getActivity} to inject a customized Intent
|
||||
* into the Activity under test.
|
||||
*
|
||||
* <p>If you do not call this, the default intent will be provided. If you call this after
|
||||
* your Activity has been started, it will have no effect.
|
||||
*
|
||||
* <p><b>NOTE:</b> Activities under test may not be started from within the UI thread.
|
||||
* If your test method is annotated with {@link android.test.UiThreadTest}, then you must call
|
||||
* {@link #setActivityIntent(Intent)} from {@link #setUp()}.
|
||||
*
|
||||
* <p>The default Intent (if this method is not called) is:
|
||||
* action = {@link Intent#ACTION_MAIN}
|
||||
* flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK}
|
||||
* All other fields are null or empty.
|
||||
*
|
||||
* @param i The Intent to start the Activity with, or null to reset to the default Intent.
|
||||
*/
|
||||
public void setActivityIntent(Intent i) {
|
||||
mActivityIntent = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method before the first call to {@link #getActivity} to set the initial touch
|
||||
* mode for the Activity under test.
|
||||
*
|
||||
* <p>If you do not call this, the touch mode will be false. If you call this after
|
||||
* your Activity has been started, it will have no effect.
|
||||
*
|
||||
* <p><b>NOTE:</b> Activities under test may not be started from within the UI thread.
|
||||
* If your test method is annotated with {@link android.test.UiThreadTest}, then you must call
|
||||
* {@link #setActivityInitialTouchMode(boolean)} from {@link #setUp()}.
|
||||
*
|
||||
* @param initialTouchMode true if the Activity should be placed into "touch mode" when started
|
||||
*/
|
||||
public void setActivityInitialTouchMode(boolean initialTouchMode) {
|
||||
mInitialTouchMode = initialTouchMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mInitialTouchMode = false;
|
||||
mActivityIntent = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
// Finish the Activity off (unless was never launched anyway)
|
||||
Activity a = super.getActivity();
|
||||
if (a != null) {
|
||||
a.finish();
|
||||
setActivity(null);
|
||||
}
|
||||
|
||||
// Scrub out members - protects against memory leaks in the case where someone
|
||||
// creates a non-static inner class (thus referencing the test case) and gives it to
|
||||
// someone else to hold onto
|
||||
scrubClass(ActivityInstrumentationTestCase2.class);
|
||||
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the current unit test. If the unit test is annotated with
|
||||
* {@link android.test.UiThreadTest}, force the Activity to be created before switching to
|
||||
* the UI thread.
|
||||
*/
|
||||
@Override
|
||||
protected void runTest() throws Throwable {
|
||||
try {
|
||||
Method method = getClass().getMethod(getName(), (Class[]) null);
|
||||
if (method.isAnnotationPresent(UiThreadTest.class)) {
|
||||
getActivity();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// eat the exception here; super.runTest() will catch it again and handle it properly
|
||||
}
|
||||
super.runTest();
|
||||
}
|
||||
|
||||
}
|
||||
88
src/test-runner/android/test/ActivityTestCase.java
Normal file
88
src/test-runner/android/test/ActivityTestCase.java
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
/**
|
||||
* This is common code used to support Activity test cases. For more useful classes, please see
|
||||
* {@link android.test.ActivityUnitTestCase} and
|
||||
* {@link android.test.ActivityInstrumentationTestCase}.
|
||||
*
|
||||
* @deprecated New tests should be written using the
|
||||
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class ActivityTestCase extends InstrumentationTestCase {
|
||||
|
||||
/**
|
||||
* The activity that will be set up for use in each test method.
|
||||
*/
|
||||
private Activity mActivity;
|
||||
|
||||
/**
|
||||
* @return Returns the activity under test.
|
||||
*/
|
||||
protected Activity getActivity() {
|
||||
return mActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the activity under test.
|
||||
* @param testActivity The activity under test
|
||||
*/
|
||||
protected void setActivity(Activity testActivity) {
|
||||
mActivity = testActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called by various TestCase implementations, at tearDown() time, in order
|
||||
* to scrub out any class variables. This protects against memory leaks in the case where a
|
||||
* test case creates a non-static inner class (thus referencing the test case) and gives it to
|
||||
* someone else to hold onto.
|
||||
*
|
||||
* @param testCaseClass The class of the derived TestCase implementation.
|
||||
*
|
||||
* @throws IllegalAccessException
|
||||
*/
|
||||
protected void scrubClass(final Class<?> testCaseClass)
|
||||
throws IllegalAccessException {
|
||||
final Field[] fields = getClass().getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
final Class<?> fieldClass = field.getDeclaringClass();
|
||||
if (testCaseClass.isAssignableFrom(fieldClass) && !field.getType().isPrimitive()
|
||||
&& (field.getModifiers() & Modifier.FINAL) == 0) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
field.set(this, null);
|
||||
} catch (Exception e) {
|
||||
android.util.Log.d("TestCase", "Error: Could not nullify field!");
|
||||
}
|
||||
|
||||
if (field.get(this) != null) {
|
||||
android.util.Log.d("TestCase", "Error: Could not nullify field!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
348
src/test-runner/android/test/ActivityUnitTestCase.java
Normal file
348
src/test-runner/android/test/ActivityUnitTestCase.java
Normal file
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
//import android.test.mock.MockApplication;
|
||||
import android.view.Window;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class provides isolated testing of a single activity. The activity under test will
|
||||
* be created with minimal connection to the system infrastructure, and you can inject mocked or
|
||||
* wrappered versions of many of Activity's dependencies. Most of the work is handled
|
||||
* automatically here by {@link #setUp} and {@link #tearDown}.
|
||||
*
|
||||
* <p>If you prefer a functional test, see {@link android.test.ActivityInstrumentationTestCase}.
|
||||
*
|
||||
* <p>It must be noted that, as a true unit test, your Activity will not be running in the
|
||||
* normal system and will not participate in the normal interactions with other Activities.
|
||||
* The following methods should not be called in this configuration - most of them will throw
|
||||
* exceptions:
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#createPendingResult(int, Intent, int)}</li>
|
||||
* <li>{@link android.app.Activity#startActivityIfNeeded(Intent, int)}</li>
|
||||
* <li>{@link android.app.Activity#startActivityFromChild(Activity, Intent, int)}</li>
|
||||
* <li>{@link android.app.Activity#startNextMatchingActivity(Intent)}</li>
|
||||
* <li>{@link android.app.Activity#getCallingActivity()}</li>
|
||||
* <li>{@link android.app.Activity#getCallingPackage()}</li>
|
||||
* <li>{@link android.app.Activity#createPendingResult(int, Intent, int)}</li>
|
||||
* <li>{@link android.app.Activity#getTaskId()}</li>
|
||||
* <li>{@link android.app.Activity#isTaskRoot()}</li>
|
||||
* <li>{@link android.app.Activity#moveTaskToBack(boolean)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The following methods may be called but will not do anything. For test purposes, you can use
|
||||
* the methods {@link #getStartedActivityIntent()} and {@link #getStartedActivityRequest()} to
|
||||
* inspect the parameters that they were called with.
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#startActivity(Intent)}</li>
|
||||
* <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The following methods may be called but will not do anything. For test purposes, you can use
|
||||
* the methods {@link #isFinishCalled()} and {@link #getFinishedActivityRequest()} to inspect the
|
||||
* parameters that they were called with.
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#finish()}</li>
|
||||
* <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
|
||||
* <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Write
|
||||
* <a href="{@docRoot}training/testing/unit-testing/local-unit-tests.html">Local Unit Tests</a>
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class ActivityUnitTestCase<T extends Activity>
|
||||
extends ActivityTestCase {
|
||||
|
||||
private static final String TAG = "ActivityUnitTestCase";
|
||||
private Class<T> mActivityClass;
|
||||
|
||||
private Context mActivityContext;
|
||||
private Application mApplication;
|
||||
private MockParent mMockParent;
|
||||
|
||||
private boolean mAttached = false;
|
||||
private boolean mCreated = false;
|
||||
|
||||
public ActivityUnitTestCase(Class<T> activityClass) {
|
||||
mActivityClass = activityClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getActivity() {
|
||||
return (T) super.getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
// default value for target context, as a default
|
||||
mActivityContext = getInstrumentation().getTargetContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the activity under test, in the same way as if it was started by
|
||||
* {@link android.content.Context#startActivity Context.startActivity()}, providing the
|
||||
* arguments it supplied. When you use this method to start the activity, it will automatically
|
||||
* be stopped by {@link #tearDown}.
|
||||
*
|
||||
* <p>This method will call onCreate(), but if you wish to further exercise Activity life
|
||||
* cycle methods, you must call them yourself from your test case.
|
||||
*
|
||||
* <p><i>Do not call from your setUp() method. You must call this method from each of your
|
||||
* test methods.</i>
|
||||
*
|
||||
* @param intent The Intent as if supplied to {@link android.content.Context#startActivity}.
|
||||
* @param savedInstanceState The instance state, if you are simulating this part of the life
|
||||
* cycle. Typically null.
|
||||
* @param lastNonConfigurationInstance This Object will be available to the
|
||||
* Activity if it calls {@link android.app.Activity#getLastNonConfigurationInstance()}.
|
||||
* Typically null.
|
||||
* @return Returns the Activity that was created
|
||||
*/
|
||||
protected T startActivity(Intent intent, Bundle savedInstanceState,
|
||||
Object lastNonConfigurationInstance) {
|
||||
assertFalse("Activity already created", mCreated);
|
||||
|
||||
if (!mAttached) {
|
||||
assertNotNull(mActivityClass);
|
||||
setActivity(null);
|
||||
T newActivity = null;
|
||||
try {
|
||||
IBinder token = null;
|
||||
if (mApplication == null) {
|
||||
setApplication(Context.this_application/*new MockApplication()*/);
|
||||
}
|
||||
ComponentName cn = new ComponentName(mActivityClass.getPackage().getName(),
|
||||
mActivityClass.getName());
|
||||
intent.setComponent(cn);
|
||||
ActivityInfo info = new ActivityInfo();
|
||||
CharSequence title = mActivityClass.getName();
|
||||
mMockParent = new MockParent();
|
||||
String id = null;
|
||||
|
||||
newActivity = (T) getInstrumentation().newActivity(mActivityClass, mActivityContext,
|
||||
token, mApplication, intent, info, title, mMockParent, id,
|
||||
lastNonConfigurationInstance);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Catching exception", e);
|
||||
assertNotNull(newActivity);
|
||||
}
|
||||
|
||||
assertNotNull(newActivity);
|
||||
setActivity(newActivity);
|
||||
|
||||
mAttached = true;
|
||||
}
|
||||
|
||||
T result = getActivity();
|
||||
if (result != null) {
|
||||
getInstrumentation().callActivityOnCreate(getActivity(), savedInstanceState);
|
||||
mCreated = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
|
||||
setActivity(null);
|
||||
|
||||
// Scrub out members - protects against memory leaks in the case where someone
|
||||
// creates a non-static inner class (thus referencing the test case) and gives it to
|
||||
// someone else to hold onto
|
||||
scrubClass(ActivityInstrumentationTestCase.class);
|
||||
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application for use during the test. You must call this function before calling
|
||||
* {@link #startActivity}. If your test does not call this method,
|
||||
* @param application The Application object that will be injected into the Activity under test.
|
||||
*/
|
||||
public void setApplication(Application application) {
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you wish to inject a Mock, Isolated, or otherwise altered context, you can do so
|
||||
* here. You must call this function before calling {@link #startActivity}. If you wish to
|
||||
* obtain a real Context, as a building block, use getInstrumentation().getTargetContext().
|
||||
*/
|
||||
public void setActivityContext(Context activityContext) {
|
||||
mActivityContext = activityContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will return the value if your Activity under test calls
|
||||
* {@link android.app.Activity#setRequestedOrientation}.
|
||||
*/
|
||||
public int getRequestedOrientation() {
|
||||
if (mMockParent != null) {
|
||||
return mMockParent.mRequestedOrientation;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will return the launch intent if your Activity under test calls
|
||||
* {@link android.app.Activity#startActivity(Intent)} or
|
||||
* {@link android.app.Activity#startActivityForResult(Intent, int)}.
|
||||
* @return The Intent provided in the start call, or null if no start call was made.
|
||||
*/
|
||||
public Intent getStartedActivityIntent() {
|
||||
if (mMockParent != null) {
|
||||
return mMockParent.mStartedActivityIntent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will return the launch request code if your Activity under test calls
|
||||
* {@link android.app.Activity#startActivityForResult(Intent, int)}.
|
||||
* @return The request code provided in the start call, or -1 if no start call was made.
|
||||
*/
|
||||
public int getStartedActivityRequest() {
|
||||
if (mMockParent != null) {
|
||||
return mMockParent.mStartedActivityRequest;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will notify you if the Activity under test called
|
||||
* {@link android.app.Activity#finish()},
|
||||
* {@link android.app.Activity#finishFromChild(Activity)}, or
|
||||
* {@link android.app.Activity#finishActivity(int)}.
|
||||
* @return Returns true if one of the listed finish methods was called.
|
||||
*/
|
||||
public boolean isFinishCalled() {
|
||||
if (mMockParent != null) {
|
||||
return mMockParent.mFinished;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will return the request code if the Activity under test called
|
||||
* {@link android.app.Activity#finishActivity(int)}.
|
||||
* @return The request code provided in the start call, or -1 if no finish call was made.
|
||||
*/
|
||||
public int getFinishedActivityRequest() {
|
||||
if (mMockParent != null) {
|
||||
return mMockParent.mFinishedActivityRequest;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mock Activity represents the "parent" activity. By injecting this, we allow the user
|
||||
* to call a few more Activity methods, including:
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#getRequestedOrientation()}</li>
|
||||
* <li>{@link android.app.Activity#setRequestedOrientation(int)}</li>
|
||||
* <li>{@link android.app.Activity#finish()}</li>
|
||||
* <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
|
||||
* <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* TODO: Make this overrideable, and the unit test can look for calls to other methods
|
||||
*/
|
||||
private static class MockParent extends Activity {
|
||||
|
||||
public int mRequestedOrientation = 0;
|
||||
public Intent mStartedActivityIntent = null;
|
||||
public int mStartedActivityRequest = -1;
|
||||
public boolean mFinished = false;
|
||||
public int mFinishedActivityRequest = -1;
|
||||
|
||||
/**
|
||||
* Implementing in the parent allows the user to call this function on the tested activity.
|
||||
*/
|
||||
@Override
|
||||
public void setRequestedOrientation(int requestedOrientation) {
|
||||
mRequestedOrientation = requestedOrientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementing in the parent allows the user to call this function on the tested activity.
|
||||
*/
|
||||
@Override
|
||||
public int getRequestedOrientation() {
|
||||
return mRequestedOrientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* By returning null here, we inhibit the creation of any "container" for the window.
|
||||
*/
|
||||
@Override
|
||||
public Window getWindow() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* By defining this in the parent, we allow the tested activity to call
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#startActivity(Intent)}</li>
|
||||
* <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
|
||||
* </ul>
|
||||
*/
|
||||
//@Override
|
||||
public void startActivityFromChild(Activity child, Intent intent, int requestCode) {
|
||||
mStartedActivityIntent = intent;
|
||||
mStartedActivityRequest = requestCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* By defining this in the parent, we allow the tested activity to call
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#finish()}</li>
|
||||
* <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
|
||||
* </ul>
|
||||
*/
|
||||
//@Override
|
||||
public void finishFromChild(Activity child) {
|
||||
mFinished = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* By defining this in the parent, we allow the tested activity to call
|
||||
* <ul>
|
||||
* <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
|
||||
* </ul>
|
||||
*/
|
||||
//@Override
|
||||
public void finishActivityFromChild(Activity child, int requestCode) {
|
||||
mFinished = true;
|
||||
mFinishedActivityRequest = requestCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/test-runner/android/test/AndroidTestCase.java
Normal file
182
src/test-runner/android/test/AndroidTestCase.java
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright (C) 2006 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.test.suitebuilder.annotation.Suppress;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
/**
|
||||
* Extend this if you need to access Resources or other things that depend on Activity Context.
|
||||
*
|
||||
* @deprecated Use
|
||||
* <a href="{@docRoot}reference/android/support/test/InstrumentationRegistry.html">
|
||||
* InstrumentationRegistry</a> instead. New tests should be written using the
|
||||
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public class AndroidTestCase extends TestCase {
|
||||
|
||||
protected Context mContext;
|
||||
private Context mTestContext;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Suppress
|
||||
public void testAndroidTestCaseSetupProperly() {
|
||||
assertNotNull("Context is null. setContext should be called before tests are run",
|
||||
mContext);
|
||||
}
|
||||
|
||||
public void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test context can be used to access resources from the test's own package
|
||||
* as opposed to the resources from the test target package. Access to the
|
||||
* latter is provided by the context set with the {@link #setContext}
|
||||
* method.
|
||||
*
|
||||
*/
|
||||
public void setTestContext(Context context) {
|
||||
mTestContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test context that was set via {@link #setTestContext(Context)}.
|
||||
*/
|
||||
public Context getTestContext() {
|
||||
return mTestContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that launching a given activity is protected by a particular permission by
|
||||
* attempting to start the activity and validating that a {@link SecurityException}
|
||||
* is thrown that mentions the permission in its error message.
|
||||
*
|
||||
* Note that an instrumentation isn't needed because all we are looking for is a security error
|
||||
* and we don't need to wait for the activity to launch and get a handle to the activity.
|
||||
*
|
||||
* @param packageName The package name of the activity to launch.
|
||||
* @param className The class of the activity to launch.
|
||||
* @param permission The name of the permission.
|
||||
*/
|
||||
public void assertActivityRequiresPermission(
|
||||
String packageName, String className, String permission) {
|
||||
final Intent intent = new Intent();
|
||||
intent.setClassName(packageName, className);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
try {
|
||||
getContext().startActivity(intent);
|
||||
fail("expected security exception for " + permission);
|
||||
} catch (SecurityException expected) {
|
||||
assertNotNull("security exception's error message.", expected.getMessage());
|
||||
assertTrue("error message should contain " + permission + ".",
|
||||
expected.getMessage().contains(permission));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asserts that reading from the content uri requires a particular permission by querying the
|
||||
* uri and ensuring a {@link SecurityException} is thrown mentioning the particular permission.
|
||||
*
|
||||
* @param uri The uri that requires a permission to query.
|
||||
* @param permission The permission that should be required.
|
||||
*/
|
||||
public void assertReadingContentUriRequiresPermission(Uri uri, String permission) {
|
||||
try {
|
||||
getContext().getContentResolver().query(uri, null, null, null, null);
|
||||
fail("expected SecurityException requiring " + permission);
|
||||
} catch (SecurityException expected) {
|
||||
assertNotNull("security exception's error message.", expected.getMessage());
|
||||
assertTrue("error message should contain " + permission + ".",
|
||||
expected.getMessage().contains(permission));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that writing to the content uri requires a particular permission by inserting into
|
||||
* the uri and ensuring a {@link SecurityException} is thrown mentioning the particular
|
||||
* permission.
|
||||
*
|
||||
* @param uri The uri that requires a permission to query.
|
||||
* @param permission The permission that should be required.
|
||||
*/
|
||||
public void assertWritingContentUriRequiresPermission(Uri uri, String permission) {
|
||||
try {
|
||||
getContext().getContentResolver().insert(uri, new ContentValues());
|
||||
fail("expected SecurityException requiring " + permission);
|
||||
} catch (SecurityException expected) {
|
||||
assertNotNull("security exception's error message.", expected.getMessage());
|
||||
assertTrue("error message should contain \"" + permission + "\". Got: \""
|
||||
+ expected.getMessage() + "\".",
|
||||
expected.getMessage().contains(permission));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called by various TestCase implementations, at tearDown() time, in order
|
||||
* to scrub out any class variables. This protects against memory leaks in the case where a
|
||||
* test case creates a non-static inner class (thus referencing the test case) and gives it to
|
||||
* someone else to hold onto.
|
||||
*
|
||||
* @param testCaseClass The class of the derived TestCase implementation.
|
||||
*
|
||||
* @throws IllegalAccessException
|
||||
*/
|
||||
protected void scrubClass(final Class<?> testCaseClass)
|
||||
throws IllegalAccessException {
|
||||
final Field[] fields = getClass().getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
if (!field.getType().isPrimitive() &&
|
||||
!Modifier.isStatic(field.getModifiers())) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
field.set(this, null);
|
||||
} catch (Exception e) {
|
||||
android.util.Log.d("TestCase", "Error: Could not nullify field!");
|
||||
}
|
||||
|
||||
if (field.get(this) != null) {
|
||||
android.util.Log.d("TestCase", "Error: Could not nullify field!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
251
src/test-runner/android/test/AndroidTestRunner.java
Normal file
251
src/test-runner/android/test/AndroidTestRunner.java
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestListener;
|
||||
import junit.framework.TestResult;
|
||||
import junit.framework.TestSuite;
|
||||
import junit.runner.BaseTestRunner;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @deprecated Use
|
||||
* <a href="{@docRoot}reference/android/support/test/runner/AndroidJUnitRunner.html">
|
||||
* AndroidJUnitRunner</a> instead. New tests should be written using the
|
||||
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public class AndroidTestRunner extends BaseTestRunner {
|
||||
|
||||
private TestResult mTestResult;
|
||||
private String mTestClassName;
|
||||
private List<TestCase> mTestCases;
|
||||
private Context mContext;
|
||||
private boolean mSkipExecution = false;
|
||||
|
||||
private List<TestListener> mTestListeners = new ArrayList<>();
|
||||
private Instrumentation mInstrumentation;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setTestClassName(String testClassName, String testMethodName) {
|
||||
Class testClass = loadTestClass(testClassName);
|
||||
|
||||
if (shouldRunSingleTestMethod(testMethodName, testClass)) {
|
||||
TestCase testCase = buildSingleTestMethod(testClass, testMethodName);
|
||||
mTestCases = new ArrayList<>();
|
||||
mTestCases.add(testCase);
|
||||
mTestClassName = testClass.getSimpleName();
|
||||
} else {
|
||||
setTest(getTest(testClass), testClass);
|
||||
}
|
||||
}
|
||||
|
||||
public void setTest(Test test) {
|
||||
setTest(test, test.getClass());
|
||||
}
|
||||
|
||||
private void setTest(Test test, Class<? extends Test> testClass) {
|
||||
mTestCases = (List<TestCase>) TestCaseUtil.getTests(test, true);
|
||||
if (TestSuite.class.isAssignableFrom(testClass)) {
|
||||
mTestClassName = TestCaseUtil.getTestName(test);
|
||||
} else {
|
||||
mTestClassName = testClass.getSimpleName();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearTestListeners() {
|
||||
mTestListeners.clear();
|
||||
}
|
||||
|
||||
public void addTestListener(TestListener testListener) {
|
||||
if (testListener != null) {
|
||||
mTestListeners.add(testListener);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Class<? extends Test> loadTestClass(String testClassName) {
|
||||
try {
|
||||
return (Class<? extends Test>) mContext.getClassLoader().loadClass(testClassName);
|
||||
} catch (ClassNotFoundException e) {
|
||||
runFailed("Could not find test class. Class: " + testClassName, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private TestCase buildSingleTestMethod(Class testClass, String testMethodName) {
|
||||
try {
|
||||
Constructor c = testClass.getConstructor();
|
||||
return newSingleTestMethod(testClass, testMethodName, c);
|
||||
} catch (NoSuchMethodException e) {
|
||||
}
|
||||
|
||||
try {
|
||||
Constructor c = testClass.getConstructor(String.class);
|
||||
return newSingleTestMethod(testClass, testMethodName, c, testMethodName);
|
||||
} catch (NoSuchMethodException e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private TestCase newSingleTestMethod(Class testClass, String testMethodName,
|
||||
Constructor constructor, Object... args) {
|
||||
try {
|
||||
TestCase testCase = (TestCase) constructor.newInstance(args);
|
||||
testCase.setName(testMethodName);
|
||||
return testCase;
|
||||
} catch (IllegalAccessException e) {
|
||||
runFailed("Could not access test class. Class: " + testClass.getName(), e);
|
||||
} catch (InstantiationException e) {
|
||||
runFailed("Could not instantiate test class. Class: " + testClass.getName(), e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
runFailed("Illegal argument passed to constructor. Class: " + testClass.getName(), e);
|
||||
} catch (InvocationTargetException e) {
|
||||
runFailed("Constructor threw an exception. Class: " + testClass.getName(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean shouldRunSingleTestMethod(String testMethodName,
|
||||
Class<? extends Test> testClass) {
|
||||
return testMethodName != null && TestCase.class.isAssignableFrom(testClass);
|
||||
}
|
||||
|
||||
private Test getTest(Class clazz) {
|
||||
if (TestSuiteProvider.class.isAssignableFrom(clazz)) {
|
||||
try {
|
||||
TestSuiteProvider testSuiteProvider =
|
||||
(TestSuiteProvider) clazz.getConstructor().newInstance();
|
||||
return testSuiteProvider.getTestSuite();
|
||||
} catch (InstantiationException e) {
|
||||
runFailed("Could not instantiate test suite provider. Class: " + clazz.getName(), e);
|
||||
} catch (IllegalAccessException e) {
|
||||
runFailed("Illegal access of test suite provider. Class: " + clazz.getName(), e);
|
||||
} catch (InvocationTargetException e) {
|
||||
runFailed("Invocation exception test suite provider. Class: " + clazz.getName(), e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
runFailed("No such method on test suite provider. Class: " + clazz.getName(), e);
|
||||
}
|
||||
}
|
||||
return getTest(clazz.getName());
|
||||
}
|
||||
|
||||
protected TestResult createTestResult() {
|
||||
if (mSkipExecution) {
|
||||
return new NoExecTestResult();
|
||||
}
|
||||
return new TestResult();
|
||||
}
|
||||
|
||||
void setSkipExecution(boolean skip) {
|
||||
mSkipExecution = skip;
|
||||
}
|
||||
|
||||
public List<TestCase> getTestCases() {
|
||||
return mTestCases;
|
||||
}
|
||||
|
||||
public String getTestClassName() {
|
||||
return mTestClassName;
|
||||
}
|
||||
|
||||
public TestResult getTestResult() {
|
||||
return mTestResult;
|
||||
}
|
||||
|
||||
public void runTest() {
|
||||
runTest(createTestResult());
|
||||
}
|
||||
|
||||
public void runTest(TestResult testResult) {
|
||||
mTestResult = testResult;
|
||||
|
||||
for (TestListener testListener : mTestListeners) {
|
||||
mTestResult.addListener(testListener);
|
||||
}
|
||||
|
||||
Context testContext = mInstrumentation == null ? mContext : mInstrumentation.getContext();
|
||||
for (TestCase testCase : mTestCases) {
|
||||
setContextIfAndroidTestCase(testCase, mContext, testContext);
|
||||
setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
|
||||
testCase.run(mTestResult);
|
||||
}
|
||||
}
|
||||
|
||||
private void setContextIfAndroidTestCase(Test test, Context context, Context testContext) {
|
||||
if (AndroidTestCase.class.isAssignableFrom(test.getClass())) {
|
||||
((AndroidTestCase) test).setContext(context);
|
||||
((AndroidTestCase) test).setTestContext(testContext);
|
||||
}
|
||||
}
|
||||
|
||||
public void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
private void setInstrumentationIfInstrumentationTestCase(
|
||||
Test test, Instrumentation instrumentation) {
|
||||
if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {
|
||||
((InstrumentationTestCase) test).injectInstrumentation(instrumentation);
|
||||
}
|
||||
}
|
||||
|
||||
public void setInstrumentation(Instrumentation instrumentation) {
|
||||
mInstrumentation = instrumentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Incorrect spelling,
|
||||
* use {@link #setInstrumentation(android.app.Instrumentation)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setInstrumentaiton(Instrumentation instrumentation) {
|
||||
setInstrumentation(instrumentation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class loadSuiteClass(String suiteClassName) throws ClassNotFoundException {
|
||||
return mContext.getClassLoader().loadClass(suiteClassName);
|
||||
}
|
||||
|
||||
public void testStarted(String testName) {
|
||||
}
|
||||
|
||||
public void testEnded(String testName) {
|
||||
}
|
||||
|
||||
public void testFailed(int status, Test test, Throwable t) {
|
||||
}
|
||||
|
||||
protected void runFailed(String message) {
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
protected void runFailed(String message, Throwable cause) {
|
||||
throw new RuntimeException(message, cause);
|
||||
}
|
||||
}
|
||||
181
src/test-runner/android/test/ApplicationTestCase.java
Normal file
181
src/test-runner/android/test/ApplicationTestCase.java
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* This test case provides a framework in which you can test Application classes in
|
||||
* a controlled environment. It provides basic support for the lifecycle of a
|
||||
* Application, and hooks by which you can inject various dependencies and control
|
||||
* the environment in which your Application is tested.
|
||||
*
|
||||
* <p><b>Lifecycle Support.</b>
|
||||
* Every Application is designed to be accessed within a specific sequence of
|
||||
* method calls (see {@link android.app.Application} for more details).
|
||||
* In order to support the lifecycle of a Application, this test case will make the
|
||||
* following calls at the following times.
|
||||
*
|
||||
* <ul><li>The test case will not call onCreate() until your test calls
|
||||
* {@link #createApplication()}. This gives you a chance
|
||||
* to set up or adjust any additional framework or test logic before
|
||||
* onCreate().</li>
|
||||
* <li>After your test completes, the test case {@link #tearDown} method is
|
||||
* automatically called, and it will stop & destroy your application by calling its
|
||||
* onDestroy() method.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Dependency Injection.</b>
|
||||
* Every Application has one inherent dependency, the {@link android.content.Context Context} in
|
||||
* which it runs.
|
||||
* This framework allows you to inject a modified, mock, or isolated replacement for this
|
||||
* dependencies, and thus perform a true unit test.
|
||||
*
|
||||
* <p>If simply run your tests as-is, your Application will be injected with a fully-functional
|
||||
* Context.
|
||||
* You can create and inject alternative types of Contexts by calling
|
||||
* {@link AndroidTestCase#setContext(Context) setContext()}. You must do this <i>before</i> calling
|
||||
* {@link #createApplication()}. The test framework provides a
|
||||
* number of alternatives for Context, including {@link android.test.mock.MockContext MockContext},
|
||||
* {@link android.test.RenamingDelegatingContext RenamingDelegatingContext}, and
|
||||
* {@link android.content.ContextWrapper ContextWrapper}.
|
||||
*
|
||||
* @deprecated Use
|
||||
* <a href="{@docRoot}reference/android/support/test/InstrumentationRegistry.html">
|
||||
* InstrumentationRegistry</a> instead. New tests should be written using the
|
||||
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public abstract class ApplicationTestCase<T extends Application> extends AndroidTestCase {
|
||||
|
||||
Class<T> mApplicationClass;
|
||||
|
||||
private Context mSystemContext;
|
||||
|
||||
public ApplicationTestCase(Class<T> applicationClass) {
|
||||
mApplicationClass = applicationClass;
|
||||
}
|
||||
|
||||
private T mApplication;
|
||||
private boolean mAttached = false;
|
||||
private boolean mCreated = false;
|
||||
|
||||
/**
|
||||
* @return Returns the actual Application under test.
|
||||
*/
|
||||
public T getApplication() {
|
||||
return mApplication;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will do the work to instantiate the Application under test. After this, your test
|
||||
* code must also start and stop the Application.
|
||||
*/
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
// get the real context, before the individual tests have a chance to muck with it
|
||||
mSystemContext = getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and attach the application under test.
|
||||
*/
|
||||
private void setupApplication() {
|
||||
mApplication = null;
|
||||
try {
|
||||
mApplication = (T) Instrumentation.newApplication(mApplicationClass, getContext());
|
||||
} catch (Exception e) {
|
||||
assertNotNull(mApplication);
|
||||
}
|
||||
mAttached = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Application under test, in the same way as if it was started by the system.
|
||||
* If you use this method to start the Application, it will automatically
|
||||
* be stopped by {@link #tearDown}. If you wish to inject a specialized Context for your
|
||||
* test, by calling {@link AndroidTestCase#setContext(Context) setContext()},
|
||||
* you must do so before calling this method.
|
||||
*/
|
||||
final protected void createApplication() {
|
||||
assertFalse(mCreated);
|
||||
|
||||
if (!mAttached) {
|
||||
setupApplication();
|
||||
}
|
||||
assertNotNull(mApplication);
|
||||
|
||||
mApplication.onCreate();
|
||||
mCreated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will make the necessary calls to terminate the Application under test (it will
|
||||
* call onTerminate(). Ordinarily this will be called automatically (by {@link #tearDown}, but
|
||||
* you can call it directly from your test in order to check for proper shutdown behaviors.
|
||||
*/
|
||||
final protected void terminateApplication() {
|
||||
if (mCreated) {
|
||||
mApplication.onTerminate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the Application under test. Also makes sure all resources are cleaned up and
|
||||
* garbage collected before moving on to the next
|
||||
* test. Subclasses that override this method should make sure they call super.tearDown()
|
||||
* at the end of the overriding method.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
terminateApplication();
|
||||
mApplication = null;
|
||||
|
||||
// Scrub out members - protects against memory leaks in the case where someone
|
||||
// creates a non-static inner class (thus referencing the test case) and gives it to
|
||||
// someone else to hold onto
|
||||
scrubClass(ApplicationTestCase.class);
|
||||
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a real (not mocked or instrumented) system Context that can be used when generating
|
||||
* Mock or other Context objects for your Application under test.
|
||||
*
|
||||
* @return Returns a reference to a normal Context.
|
||||
*/
|
||||
public Context getSystemContext() {
|
||||
return mSystemContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* This test simply confirms that the Application class can be instantiated properly.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
final public void testApplicationTestCaseSetUpProperly() throws Exception {
|
||||
setupApplication();
|
||||
assertNotNull("Application class could not be instantiated successfully", mApplication);
|
||||
}
|
||||
}
|
||||
36
src/test-runner/android/test/AssertionFailedError.java
Normal file
36
src/test-runner/android/test/AssertionFailedError.java
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
/**
|
||||
* Thrown when an assertion failed.
|
||||
*
|
||||
* @deprecated use junit.framework.AssertionFailedError
|
||||
*/
|
||||
@Deprecated
|
||||
public class AssertionFailedError extends Error {
|
||||
|
||||
/**
|
||||
* It is more typical to call {@link #AssertionFailedError(String)}.
|
||||
*/
|
||||
public AssertionFailedError() {
|
||||
}
|
||||
|
||||
public AssertionFailedError(String errorMessage) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
262
src/test-runner/android/test/ClassPathPackageInfoSource.java
Normal file
262
src/test-runner/android/test/ClassPathPackageInfoSource.java
Normal file
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.util.Log;
|
||||
import dalvik.system.DexFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Generate {@link ClassPathPackageInfo}s by scanning apk paths.
|
||||
*
|
||||
* {@hide} Not needed for 1.0 SDK.
|
||||
*/
|
||||
@Deprecated
|
||||
public class ClassPathPackageInfoSource {
|
||||
|
||||
private static final ClassLoader CLASS_LOADER
|
||||
= ClassPathPackageInfoSource.class.getClassLoader();
|
||||
|
||||
private static String[] apkPaths;
|
||||
|
||||
private static ClassPathPackageInfoSource classPathSource;
|
||||
|
||||
private final SimpleCache<String, ClassPathPackageInfo> cache =
|
||||
new SimpleCache<String, ClassPathPackageInfo>() {
|
||||
@Override
|
||||
protected ClassPathPackageInfo load(String pkgName) {
|
||||
return createPackageInfo(pkgName);
|
||||
}
|
||||
};
|
||||
|
||||
// The class path of the running application
|
||||
private final String[] classPath;
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
private ClassPathPackageInfoSource(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
classPath = getClassPath();
|
||||
}
|
||||
|
||||
static void setApkPaths(String[] apkPaths) {
|
||||
ClassPathPackageInfoSource.apkPaths = apkPaths;
|
||||
}
|
||||
|
||||
public static ClassPathPackageInfoSource forClassPath(ClassLoader classLoader) {
|
||||
if (classPathSource == null) {
|
||||
classPathSource = new ClassPathPackageInfoSource(classLoader);
|
||||
}
|
||||
return classPathSource;
|
||||
}
|
||||
|
||||
public Set<Class<?>> getTopLevelClassesRecursive(String packageName) {
|
||||
ClassPathPackageInfo packageInfo = cache.get(packageName);
|
||||
return packageInfo.getTopLevelClassesRecursive();
|
||||
}
|
||||
|
||||
private ClassPathPackageInfo createPackageInfo(String packageName) {
|
||||
Set<String> subpackageNames = new TreeSet<String>();
|
||||
Set<String> classNames = new TreeSet<String>();
|
||||
Set<Class<?>> topLevelClasses = new HashSet<>();
|
||||
findClasses(packageName, classNames, subpackageNames);
|
||||
for (String className : classNames) {
|
||||
if (className.endsWith(".R") || className.endsWith(".Manifest")) {
|
||||
// Don't try to load classes that are generated. They usually aren't in test apks.
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// We get errors in the emulator if we don't use the caller's class loader.
|
||||
topLevelClasses.add(Class.forName(className, false,
|
||||
(classLoader != null) ? classLoader : CLASS_LOADER));
|
||||
} catch (ClassNotFoundException | NoClassDefFoundError e) {
|
||||
// Should not happen unless there is a generated class that is not included in
|
||||
// the .apk.
|
||||
Log.w("ClassPathPackageInfoSource", "Cannot load class. "
|
||||
+ "Make sure it is in your apk. Class name: '" + className
|
||||
+ "'. Message: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return new ClassPathPackageInfo(packageName, subpackageNames,
|
||||
topLevelClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all classes and sub packages that are below the packageName and
|
||||
* add them to the respective sets. Searches the package on the whole class
|
||||
* path.
|
||||
*/
|
||||
private void findClasses(String packageName, Set<String> classNames,
|
||||
Set<String> subpackageNames) {
|
||||
for (String entryName : classPath) {
|
||||
File classPathEntry = new File(entryName);
|
||||
|
||||
// Forge may not have brought over every item in the classpath. Be
|
||||
// polite and ignore missing entries.
|
||||
if (classPathEntry.exists()) {
|
||||
try {
|
||||
if (entryName.endsWith(".apk")) {
|
||||
findClassesInApk(entryName, packageName, classNames, subpackageNames);
|
||||
} else {
|
||||
// scan the directories that contain apk files.
|
||||
for (String apkPath : apkPaths) {
|
||||
File file = new File(apkPath);
|
||||
scanForApkFiles(file, packageName, classNames, subpackageNames);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Can't read classpath entry " +
|
||||
entryName + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scanForApkFiles(File source, String packageName,
|
||||
Set<String> classNames, Set<String> subpackageNames) throws IOException {
|
||||
if (source.getPath().endsWith(".apk")) {
|
||||
findClassesInApk(source.getPath(), packageName, classNames, subpackageNames);
|
||||
} else {
|
||||
File[] files = source.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
scanForApkFiles(file, packageName, classNames, subpackageNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all classes and sub packages that are below the packageName and
|
||||
* add them to the respective sets. Searches the package in a single apk file.
|
||||
*/
|
||||
private void findClassesInApk(String apkPath, String packageName,
|
||||
Set<String> classNames, Set<String> subpackageNames)
|
||||
throws IOException {
|
||||
|
||||
DexFile dexFile = null;
|
||||
try {
|
||||
dexFile = new DexFile(apkPath);
|
||||
Enumeration<String> apkClassNames = dexFile.entries();
|
||||
while (apkClassNames.hasMoreElements()) {
|
||||
String className = apkClassNames.nextElement();
|
||||
|
||||
if (className.startsWith(packageName)) {
|
||||
String subPackageName = packageName;
|
||||
int lastPackageSeparator = className.lastIndexOf('.');
|
||||
if (lastPackageSeparator > 0) {
|
||||
subPackageName = className.substring(0, lastPackageSeparator);
|
||||
}
|
||||
if (subPackageName.length() > packageName.length()) {
|
||||
subpackageNames.add(subPackageName);
|
||||
} else if (isToplevelClass(className)) {
|
||||
classNames.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (false) {
|
||||
Log.w("ClassPathPackageInfoSource",
|
||||
"Error finding classes at apk path: " + apkPath, e);
|
||||
}
|
||||
} finally {
|
||||
if (dexFile != null) {
|
||||
// Todo: figure out why closing causes a dalvik error resulting in vm shutdown.
|
||||
// dexFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given file name represents a toplevel class.
|
||||
*/
|
||||
private static boolean isToplevelClass(String fileName) {
|
||||
return fileName.indexOf('$') < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the class path from the System Property "java.class.path" and splits
|
||||
* it up into the individual elements.
|
||||
*/
|
||||
private static String[] getClassPath() {
|
||||
String classPath = System.getProperty("java.class.path");
|
||||
String separator = System.getProperty("path.separator", ":");
|
||||
return classPath.split(Pattern.quote(separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* The Package object doesn't allow you to iterate over the contained
|
||||
* classes and subpackages of that package. This is a version that does.
|
||||
*/
|
||||
private class ClassPathPackageInfo {
|
||||
|
||||
private final String packageName;
|
||||
private final Set<String> subpackageNames;
|
||||
private final Set<Class<?>> topLevelClasses;
|
||||
|
||||
private ClassPathPackageInfo(String packageName,
|
||||
Set<String> subpackageNames, Set<Class<?>> topLevelClasses) {
|
||||
this.packageName = packageName;
|
||||
this.subpackageNames = Collections.unmodifiableSet(subpackageNames);
|
||||
this.topLevelClasses = Collections.unmodifiableSet(topLevelClasses);
|
||||
}
|
||||
|
||||
private Set<ClassPathPackageInfo> getSubpackages() {
|
||||
Set<ClassPathPackageInfo> info = new HashSet<>();
|
||||
for (String name : subpackageNames) {
|
||||
info.add(cache.get(name));
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private Set<Class<?>> getTopLevelClassesRecursive() {
|
||||
Set<Class<?>> set = new HashSet<>();
|
||||
addTopLevelClassesTo(set);
|
||||
return set;
|
||||
}
|
||||
|
||||
private void addTopLevelClassesTo(Set<Class<?>> set) {
|
||||
set.addAll(topLevelClasses);
|
||||
for (ClassPathPackageInfo info : getSubpackages()) {
|
||||
info.addTopLevelClassesTo(set);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof ClassPathPackageInfo) {
|
||||
ClassPathPackageInfo that = (ClassPathPackageInfo) obj;
|
||||
return (this.packageName).equals(that.packageName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return packageName.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/test-runner/android/test/ComparisonFailure.java
Normal file
35
src/test-runner/android/test/ComparisonFailure.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
/**
|
||||
* Thrown when an assert equals for Strings failed.
|
||||
*
|
||||
* @deprecated use org.junit.ComparisonFailure
|
||||
*/
|
||||
@Deprecated
|
||||
public class ComparisonFailure extends AssertionFailedError {
|
||||
private junit.framework.ComparisonFailure mComparison;
|
||||
|
||||
public ComparisonFailure(String message, String expected, String actual) {
|
||||
mComparison = new junit.framework.ComparisonFailure(message, expected, actual);
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return mComparison.getMessage();
|
||||
}
|
||||
}
|
||||
47
src/test-runner/android/test/FlakyTest.java
Normal file
47
src/test-runner/android/test/FlakyTest.java
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.annotation.ElementType;
|
||||
|
||||
/**
|
||||
* This annotation can be used on an {@link android.test.InstrumentationTestCase}'s
|
||||
* test methods. When the annotation is present, the test method is re-executed if
|
||||
* the test fails. The total number of executions is specified by the tolerance and
|
||||
* defaults to 1.
|
||||
*
|
||||
* @deprecated Use
|
||||
* <a href="{@docRoot}reference/android/support/test/filters/FlakyTest.html">
|
||||
* FlakyTest</a> instead. New tests should be written using the
|
||||
* <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface FlakyTest {
|
||||
/**
|
||||
* Indicates how many times a test can run and fail before being reported
|
||||
* as a failed test. If the tolerance factor is less than 1, the test runs
|
||||
* only once.
|
||||
*
|
||||
* @return The total number of allowed run, the default is 1.
|
||||
*/
|
||||
int tolerance() default 1;
|
||||
}
|
||||
369
src/test-runner/android/test/InstrumentationTestCase.java
Normal file
369
src/test-runner/android/test/InstrumentationTestCase.java
Normal file
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* 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 android.test;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* A test case that has access to {@link Instrumentation}.
|
||||
*
|
||||
* @deprecated Use
|
||||
* <a href="{@docRoot}reference/androidx/test/platform/app/InstrumentationRegistry.html">
|
||||
* InstrumentationRegistry</a> instead. New tests should be written using the
|
||||
* <a href="{@docRoot}training/testing/index.html">AndroidX Test Library</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public class InstrumentationTestCase extends TestCase {
|
||||
|
||||
private Instrumentation mInstrumentation;
|
||||
|
||||
/**
|
||||
* Injects instrumentation into this test case. This method is
|
||||
* called by the test runner during test setup.
|
||||
*
|
||||
* @param instrumentation the instrumentation to use with this instance
|
||||
*/
|
||||
public void injectInstrumentation(Instrumentation instrumentation) {
|
||||
mInstrumentation = instrumentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects instrumentation into this test case. This method is
|
||||
* called by the test runner during test setup.
|
||||
*
|
||||
* @param instrumentation the instrumentation to use with this instance
|
||||
*
|
||||
* @deprecated Incorrect spelling,
|
||||
* use {@link #injectInstrumentation(android.app.Instrumentation)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void injectInsrumentation(Instrumentation instrumentation) {
|
||||
injectInstrumentation(instrumentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inheritors can access the instrumentation using this.
|
||||
* @return instrumentation
|
||||
*/
|
||||
public Instrumentation getInstrumentation() {
|
||||
return mInstrumentation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for launching an activity.
|
||||
*
|
||||
* <p>The {@link Intent} used to launch the Activity is:
|
||||
* action = {@link Intent#ACTION_MAIN}
|
||||
* extras = null, unless a custom bundle is provided here
|
||||
* All other fields are null or empty.
|
||||
*
|
||||
* <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
|
||||
* package hosting the activity to be launched, which is specified in the AndroidManifest.xml
|
||||
* file. This is not necessarily the same as the java package name.
|
||||
*
|
||||
* @param pkg The package hosting the activity to be launched.
|
||||
* @param activityCls The activity class to launch.
|
||||
* @param extras Optional extra stuff to pass to the activity.
|
||||
* @return The activity, or null if non launched.
|
||||
*/
|
||||
public final <T extends Activity> T launchActivity(
|
||||
String pkg,
|
||||
Class<T> activityCls,
|
||||
Bundle extras) {
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
if (extras != null) {
|
||||
intent.putExtras(extras);
|
||||
}
|
||||
return launchActivityWithIntent(pkg, activityCls, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for launching an activity with a specific Intent.
|
||||
*
|
||||
* <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the
|
||||
* package hosting the activity to be launched, which is specified in the AndroidManifest.xml
|
||||
* file. This is not necessarily the same as the java package name.
|
||||
*
|
||||
* @param pkg The package hosting the activity to be launched.
|
||||
* @param activityCls The activity class to launch.
|
||||
* @param intent The intent to launch with
|
||||
* @return The activity, or null if non launched.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <T extends Activity> T launchActivityWithIntent(
|
||||
String pkg,
|
||||
Class<T> activityCls,
|
||||
Intent intent) {
|
||||
intent.setClassName(pkg, activityCls.getName());
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
T activity = (T) getInstrumentation().startActivitySync(intent);
|
||||
getInstrumentation().waitForIdleSync();
|
||||
return activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for running portions of a test on the UI thread.
|
||||
*
|
||||
* Note, in most cases it is simpler to annotate the test method with
|
||||
* {@link android.test.UiThreadTest}, which will run the entire test method on the UI thread.
|
||||
* Use this method if you need to switch in and out of the UI thread to perform your test.
|
||||
*
|
||||
* @param r runnable containing test code in the {@link Runnable#run()} method
|
||||
*/
|
||||
public void runTestOnUiThread(final Runnable r) throws Throwable {
|
||||
final Throwable[] exceptions = new Throwable[1];
|
||||
getInstrumentation().runOnMainSync(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
r.run();
|
||||
} catch (Throwable throwable) {
|
||||
exceptions[0] = throwable;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (exceptions[0] != null) {
|
||||
throw exceptions[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the current unit test. If the unit test is annotated with
|
||||
* {@link android.test.UiThreadTest}, the test is run on the UI thread.
|
||||
*/
|
||||
@Override
|
||||
protected void runTest() throws Throwable {
|
||||
String fName = getName();
|
||||
assertNotNull(fName);
|
||||
Method method = null;
|
||||
try {
|
||||
// use getMethod to get all public inherited
|
||||
// methods. getDeclaredMethods returns all
|
||||
// methods of this class but excludes the
|
||||
// inherited ones.
|
||||
method = getClass().getMethod(fName, (Class[]) null);
|
||||
} catch (NoSuchMethodException e) {
|
||||
fail("Method \""+fName+"\" not found");
|
||||
}
|
||||
|
||||
if (!Modifier.isPublic(method.getModifiers())) {
|
||||
fail("Method \""+fName+"\" should be public");
|
||||
}
|
||||
|
||||
int runCount = 1;
|
||||
boolean isRepetitive = false;
|
||||
if (method.isAnnotationPresent(FlakyTest.class)) {
|
||||
runCount = method.getAnnotation(FlakyTest.class).tolerance();
|
||||
} else if (method.isAnnotationPresent(RepetitiveTest.class)) {
|
||||
runCount = method.getAnnotation(RepetitiveTest.class).numIterations();
|
||||
isRepetitive = true;
|
||||
}
|
||||
|
||||
if (method.isAnnotationPresent(UiThreadTest.class)) {
|
||||
final int tolerance = runCount;
|
||||
final boolean repetitive = isRepetitive;
|
||||
final Method testMethod = method;
|
||||
final Throwable[] exceptions = new Throwable[1];
|
||||
getInstrumentation().runOnMainSync(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
runMethod(testMethod, tolerance, repetitive);
|
||||
} catch (Throwable throwable) {
|
||||
exceptions[0] = throwable;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (exceptions[0] != null) {
|
||||
throw exceptions[0];
|
||||
}
|
||||
} else {
|
||||
runMethod(method, runCount, isRepetitive);
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards-compatibility after adding isRepetitive
|
||||
private void runMethod(Method runMethod, int tolerance) throws Throwable {
|
||||
runMethod(runMethod, tolerance, false);
|
||||
}
|
||||
|
||||
private void runMethod(Method runMethod, int tolerance, boolean isRepetitive) throws Throwable {
|
||||
Throwable exception = null;
|
||||
|
||||
int runCount = 0;
|
||||
do {
|
||||
try {
|
||||
runMethod.invoke(this, (Object[]) null);
|
||||
exception = null;
|
||||
} catch (InvocationTargetException e) {
|
||||
e.fillInStackTrace();
|
||||
exception = e.getTargetException();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.fillInStackTrace();
|
||||
exception = e;
|
||||
} finally {
|
||||
runCount++;
|
||||
// Report current iteration number, if test is repetitive
|
||||
if (isRepetitive) {
|
||||
Bundle iterations = new Bundle();
|
||||
iterations.putInt("currentiterations", runCount);
|
||||
getInstrumentation().sendStatus(2, iterations);
|
||||
}
|
||||
}
|
||||
} while ((runCount < tolerance) && (isRepetitive || exception != null));
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a series of key events through instrumentation and waits for idle. The sequence
|
||||
* of keys is a string containing the key names as specified in KeyEvent, without the
|
||||
* KEYCODE_ prefix. For instance: sendKeys("DPAD_LEFT A B C DPAD_CENTER"). Each key can
|
||||
* be repeated by using the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use
|
||||
* the following: sendKeys("2*DPAD_LEFT").
|
||||
*
|
||||
* @param keysSequence The sequence of keys.
|
||||
*/
|
||||
public void sendKeys(String keysSequence) {
|
||||
final String[] keys = keysSequence.split(" ");
|
||||
final int count = keys.length;
|
||||
|
||||
final Instrumentation instrumentation = getInstrumentation();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
String key = keys[i];
|
||||
int repeater = key.indexOf('*');
|
||||
|
||||
int keyCount;
|
||||
try {
|
||||
keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w("ActivityTestCase", "Invalid repeat count: " + key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repeater != -1) {
|
||||
key = key.substring(repeater + 1);
|
||||
}
|
||||
|
||||
for (int j = 0; j < keyCount; j++) {
|
||||
try {
|
||||
final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
|
||||
final int keyCode = keyCodeField.getInt(null);
|
||||
try {
|
||||
instrumentation.sendKeyDownUpSync(keyCode);
|
||||
} catch (SecurityException e) {
|
||||
// Ignore security exceptions that are now thrown
|
||||
// when trying to send to another app, to retain
|
||||
// compatibility with existing tests.
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
|
||||
break;
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instrumentation.waitForIdleSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a series of key events through instrumentation and waits for idle. For instance:
|
||||
* sendKeys(KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
|
||||
*
|
||||
* @param keys The series of key codes to send through instrumentation.
|
||||
*/
|
||||
public void sendKeys(int... keys) {
|
||||
final int count = keys.length;
|
||||
final Instrumentation instrumentation = getInstrumentation();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
try {
|
||||
instrumentation.sendKeyDownUpSync(keys[i]);
|
||||
} catch (SecurityException e) {
|
||||
// Ignore security exceptions that are now thrown
|
||||
// when trying to send to another app, to retain
|
||||
// compatibility with existing tests.
|
||||
}
|
||||
}
|
||||
|
||||
instrumentation.waitForIdleSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a series of key events through instrumentation and waits for idle. Each key code
|
||||
* must be preceded by the number of times the key code must be sent. For instance:
|
||||
* sendRepeatedKeys(1, KEYCODE_DPAD_CENTER, 2, KEYCODE_DPAD_LEFT).
|
||||
*
|
||||
* @param keys The series of key repeats and codes to send through instrumentation.
|
||||
*/
|
||||
public void sendRepeatedKeys(int... keys) {
|
||||
final int count = keys.length;
|
||||
if ((count & 0x1) == 0x1) {
|
||||
throw new IllegalArgumentException("The size of the keys array must "
|
||||
+ "be a multiple of 2");
|
||||
}
|
||||
|
||||
final Instrumentation instrumentation = getInstrumentation();
|
||||
|
||||
for (int i = 0; i < count; i += 2) {
|
||||
final int keyCount = keys[i];
|
||||
final int keyCode = keys[i + 1];
|
||||
for (int j = 0; j < keyCount; j++) {
|
||||
try {
|
||||
instrumentation.sendKeyDownUpSync(keyCode);
|
||||
} catch (SecurityException e) {
|
||||
// Ignore security exceptions that are now thrown
|
||||
// when trying to send to another app, to retain
|
||||
// compatibility with existing tests.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instrumentation.waitForIdleSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure all resources are cleaned up and garbage collected before moving on to the next
|
||||
* test. Subclasses that override this method should make sure they call super.tearDown()
|
||||
* at the end of the overriding method.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
Runtime.getRuntime().gc();
|
||||
Runtime.getRuntime().runFinalization();
|
||||
Runtime.getRuntime().gc();
|
||||
super.tearDown();
|
||||
}
|
||||
}
|
||||
853
src/test-runner/android/test/InstrumentationTestRunner.java
Normal file
853
src/test-runner/android/test/InstrumentationTestRunner.java
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user