diff --git a/meson.build b/meson.build index 7d325cc1..afb75220 100644 --- a/meson.build +++ b/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') diff --git a/src/api-impl-jni/util.c b/src/api-impl-jni/util.c index 93135b62..9bed4d1f 100644 --- a/src/api-impl-jni/util.c +++ b/src/api-impl-jni/util.c @@ -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, "", "()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"); diff --git a/src/api-impl-jni/util.h b/src/api-impl-jni/util.h index 3c65e830..5776d4e3 100644 --- a/src/api-impl-jni/util.h +++ b/src/api-impl-jni/util.h @@ -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; diff --git a/src/api-impl/android/app/Instrumentation.java b/src/api-impl/android/app/Instrumentation.java new file mode 100644 index 00000000..1cec57cb --- /dev/null +++ b/src/api-impl/android/app/Instrumentation.java @@ -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 cls = Class.forName(className).asSubclass(Instrumentation.class); + Constructor 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 cls = Class.forName(intent.getComponent().getClassName()).asSubclass(Activity.class); + Constructor 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 sorted(Collection list) { + final ArrayList 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); + } +} diff --git a/src/api-impl/android/content/res/Resources.java b/src/api-impl/android/content/res/Resources.java index 1fe223ef..2ed1a211 100644 --- a/src/api-impl/android/content/res/Resources.java +++ b/src/api-impl/android/content/res/Resources.java @@ -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; diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 923d861e..e0665452 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -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', diff --git a/src/main-executable/main.c b/src/main-executable/main.c index 67d0f4d1..c01049d4 100644 --- a/src/main-executable/main.c +++ b/src/main-executable/main.c @@ -5,7 +5,6 @@ #include #include - #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("(?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,51 +492,63 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h if ((*env)->ExceptionCheck(env)) (*env)->ExceptionDescribe(env); - // construct main Activity - 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); - if ((*env)->ExceptionCheck(env)) - (*env)->ExceptionDescribe(env); - if (uri_option) - g_free(uri_option); - - if (d->extra_string_keys) { - GError *error = NULL; - GRegex *regex = g_regex_new("(?message); + 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 = _GET_OBJ_FIELD(activity_object, "intent", "Landroid/content/Intent;"); + jobject intent = NULL; - 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); + 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); } - g_regex_unref(regex); - g_strfreev(d->extra_string_keys); + + (*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); + if ((*env)->ExceptionCheck(env)) + (*env)->ExceptionDescribe(env); + if (uri_option) + g_free(uri_option); + + if (d->extra_string_keys) { + jobject intent = _GET_OBJ_FIELD(activity_object, "intent", "Landroid/content/Intent;"); + 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); - package_name = package_name_jstr ? _CSTRING(package_name_jstr) : NULL; - if ((*env)->ExceptionCheck(env)) - (*env)->ExceptionDescribe(env); + 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,7 +620,8 @@ static void open(GtkApplication *app, GFile **files, gint nfiles, const gchar *h return; } - gtk_window_set_title(GTK_WINDOW(window), package_name); + 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); } - activity_start(env, activity_object); + if (!d->apk_instrumentation_class) { + activity_start(env, activity_object); - g_timeout_add(10, G_SOURCE_FUNC(hacky_on_window_focus_changed_callback), env); + 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) { @@ -624,14 +697,16 @@ static gboolean option_uri_cb(const gchar* option_name, const gchar* value, gpoi 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" }, - { "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 }, - { "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" }, + /* 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" }, {NULL} }; @@ -646,7 +721,7 @@ int main(int argc, char **argv) GtkApplication *app; int status; - /* this has to be done in the main executable, so might as well do it here*/ + /* this has to be done in the main executable, so might as well do it here */ init__r_debug(); // locale on android is always either C or C.UTF-8, and some apps might unbeknownst to them depend on that @@ -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; diff --git a/src/test-runner/android/test/ActivityInstrumentationTestCase.java b/src/test-runner/android/test/ActivityInstrumentationTestCase.java new file mode 100644 index 00000000..aca1c160 --- /dev/null +++ b/src/test-runner/android/test/ActivityInstrumentationTestCase.java @@ -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}. + * + *

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 + extends ActivityTestCase { + String mPackage; + Class 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 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 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()); + } +} diff --git a/src/test-runner/android/test/ActivityInstrumentationTestCase2.java b/src/test-runner/android/test/ActivityInstrumentationTestCase2.java new file mode 100644 index 00000000..0e61ce72 --- /dev/null +++ b/src/test-runner/android/test/ActivityInstrumentationTestCase2.java @@ -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. + * + *

Other options supported by this test case include: + *

    + *
  • You can run any test method on the UI thread (see {@link android.test.UiThreadTest}).
  • + *
  • You can inject custom Intents into your Activity (see + * {@link #setActivityIntent(Intent)}).
  • + *
+ * + *

This class replaces {@link android.test.ActivityInstrumentationTestCase}, which is deprecated. + * New tests should be written using this base class. + * + *

If you prefer an isolated unit test, see {@link android.test.ActivityUnitTestCase}. + * + * @deprecated Use + * + * ActivityTestRule instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public abstract class ActivityInstrumentationTestCase2 + extends ActivityTestCase { + Class 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 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 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. + * + *

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. + * + *

NOTE: 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. + * + *

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. + * + *

NOTE: 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()}. + * + *

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. + * + *

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. + * + *

NOTE: 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(); + } + +} diff --git a/src/test-runner/android/test/ActivityTestCase.java b/src/test-runner/android/test/ActivityTestCase.java new file mode 100644 index 00000000..51dd3ef8 --- /dev/null +++ b/src/test-runner/android/test/ActivityTestCase.java @@ -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 + * Android Testing Support Library. + */ +@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!"); + } + } + } + } + + + +} diff --git a/src/test-runner/android/test/ActivityUnitTestCase.java b/src/test-runner/android/test/ActivityUnitTestCase.java new file mode 100644 index 00000000..1488d320 --- /dev/null +++ b/src/test-runner/android/test/ActivityUnitTestCase.java @@ -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}. + * + *

If you prefer a functional test, see {@link android.test.ActivityInstrumentationTestCase}. + * + *

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: + *

    + *
  • {@link android.app.Activity#createPendingResult(int, Intent, int)}
  • + *
  • {@link android.app.Activity#startActivityIfNeeded(Intent, int)}
  • + *
  • {@link android.app.Activity#startActivityFromChild(Activity, Intent, int)}
  • + *
  • {@link android.app.Activity#startNextMatchingActivity(Intent)}
  • + *
  • {@link android.app.Activity#getCallingActivity()}
  • + *
  • {@link android.app.Activity#getCallingPackage()}
  • + *
  • {@link android.app.Activity#createPendingResult(int, Intent, int)}
  • + *
  • {@link android.app.Activity#getTaskId()}
  • + *
  • {@link android.app.Activity#isTaskRoot()}
  • + *
  • {@link android.app.Activity#moveTaskToBack(boolean)}
  • + *
+ * + *

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. + *

    + *
  • {@link android.app.Activity#startActivity(Intent)}
  • + *
  • {@link android.app.Activity#startActivityForResult(Intent, int)}
  • + *
+ * + *

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. + *

    + *
  • {@link android.app.Activity#finish()}
  • + *
  • {@link android.app.Activity#finishFromChild(Activity child)}
  • + *
  • {@link android.app.Activity#finishActivity(int requestCode)}
  • + *
+ * + * @deprecated Write + * Local Unit Tests + * instead. + */ +@Deprecated +public abstract class ActivityUnitTestCase + extends ActivityTestCase { + + private static final String TAG = "ActivityUnitTestCase"; + private Class mActivityClass; + + private Context mActivityContext; + private Application mApplication; + private MockParent mMockParent; + + private boolean mAttached = false; + private boolean mCreated = false; + + public ActivityUnitTestCase(Class 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}. + * + *

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. + * + *

Do not call from your setUp() method. You must call this method from each of your + * test methods. + * + * @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: + *

    + *
  • {@link android.app.Activity#getRequestedOrientation()}
  • + *
  • {@link android.app.Activity#setRequestedOrientation(int)}
  • + *
  • {@link android.app.Activity#finish()}
  • + *
  • {@link android.app.Activity#finishActivity(int requestCode)}
  • + *
  • {@link android.app.Activity#finishFromChild(Activity child)}
  • + *
+ * + * 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 + *
    + *
  • {@link android.app.Activity#startActivity(Intent)}
  • + *
  • {@link android.app.Activity#startActivityForResult(Intent, int)}
  • + *
+ */ + //@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 + *
    + *
  • {@link android.app.Activity#finish()}
  • + *
  • {@link android.app.Activity#finishFromChild(Activity child)}
  • + *
+ */ + //@Override + public void finishFromChild(Activity child) { + mFinished = true; + } + + /** + * By defining this in the parent, we allow the tested activity to call + *
    + *
  • {@link android.app.Activity#finishActivity(int requestCode)}
  • + *
+ */ + //@Override + public void finishActivityFromChild(Activity child, int requestCode) { + mFinished = true; + mFinishedActivityRequest = requestCode; + } + } +} diff --git a/src/test-runner/android/test/AndroidTestCase.java b/src/test-runner/android/test/AndroidTestCase.java new file mode 100644 index 00000000..d3e896be --- /dev/null +++ b/src/test-runner/android/test/AndroidTestCase.java @@ -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 + * + * InstrumentationRegistry instead. New tests should be written using the + * Android Testing Support Library. + */ +@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!"); + } + } + } + } +} diff --git a/src/test-runner/android/test/AndroidTestRunner.java b/src/test-runner/android/test/AndroidTestRunner.java new file mode 100644 index 00000000..efd8a0d7 --- /dev/null +++ b/src/test-runner/android/test/AndroidTestRunner.java @@ -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 + * + * AndroidJUnitRunner instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class AndroidTestRunner extends BaseTestRunner { + + private TestResult mTestResult; + private String mTestClassName; + private List mTestCases; + private Context mContext; + private boolean mSkipExecution = false; + + private List 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 testClass) { + mTestCases = (List) 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 loadTestClass(String testClassName) { + try { + return (Class) 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 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 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); + } +} diff --git a/src/test-runner/android/test/ApplicationTestCase.java b/src/test-runner/android/test/ApplicationTestCase.java new file mode 100644 index 00000000..4d73f530 --- /dev/null +++ b/src/test-runner/android/test/ApplicationTestCase.java @@ -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. + * + *

Lifecycle Support. + * 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. + * + *

  • 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().
  • + *
  • 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.
  • + *
+ * + *

Dependency Injection. + * 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. + * + *

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 before 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 + * + * InstrumentationRegistry instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public abstract class ApplicationTestCase extends AndroidTestCase { + + Class mApplicationClass; + + private Context mSystemContext; + + public ApplicationTestCase(Class 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); + } +} diff --git a/src/test-runner/android/test/AssertionFailedError.java b/src/test-runner/android/test/AssertionFailedError.java new file mode 100644 index 00000000..fc3e98e9 --- /dev/null +++ b/src/test-runner/android/test/AssertionFailedError.java @@ -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); + } +} diff --git a/src/test-runner/android/test/ClassPathPackageInfoSource.java b/src/test-runner/android/test/ClassPathPackageInfoSource.java new file mode 100644 index 00000000..755b540c --- /dev/null +++ b/src/test-runner/android/test/ClassPathPackageInfoSource.java @@ -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 cache = + new SimpleCache() { + @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> getTopLevelClassesRecursive(String packageName) { + ClassPathPackageInfo packageInfo = cache.get(packageName); + return packageInfo.getTopLevelClassesRecursive(); + } + + private ClassPathPackageInfo createPackageInfo(String packageName) { + Set subpackageNames = new TreeSet(); + Set classNames = new TreeSet(); + Set> 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 classNames, + Set 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 classNames, Set 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 classNames, Set subpackageNames) + throws IOException { + + DexFile dexFile = null; + try { + dexFile = new DexFile(apkPath); + Enumeration 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 subpackageNames; + private final Set> topLevelClasses; + + private ClassPathPackageInfo(String packageName, + Set subpackageNames, Set> topLevelClasses) { + this.packageName = packageName; + this.subpackageNames = Collections.unmodifiableSet(subpackageNames); + this.topLevelClasses = Collections.unmodifiableSet(topLevelClasses); + } + + private Set getSubpackages() { + Set info = new HashSet<>(); + for (String name : subpackageNames) { + info.add(cache.get(name)); + } + return info; + } + + private Set> getTopLevelClassesRecursive() { + Set> set = new HashSet<>(); + addTopLevelClassesTo(set); + return set; + } + + private void addTopLevelClassesTo(Set> 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(); + } + } +} diff --git a/src/test-runner/android/test/ComparisonFailure.java b/src/test-runner/android/test/ComparisonFailure.java new file mode 100644 index 00000000..d86b7007 --- /dev/null +++ b/src/test-runner/android/test/ComparisonFailure.java @@ -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(); + } +} diff --git a/src/test-runner/android/test/FlakyTest.java b/src/test-runner/android/test/FlakyTest.java new file mode 100644 index 00000000..4e5c4e35 --- /dev/null +++ b/src/test-runner/android/test/FlakyTest.java @@ -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 + * + * FlakyTest instead. New tests should be written using the + * Android Testing Support Library. + */ +@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; +} diff --git a/src/test-runner/android/test/InstrumentationTestCase.java b/src/test-runner/android/test/InstrumentationTestCase.java new file mode 100644 index 00000000..9f7a2fa4 --- /dev/null +++ b/src/test-runner/android/test/InstrumentationTestCase.java @@ -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 + * + * InstrumentationRegistry instead. New tests should be written using the + * AndroidX Test Library. + */ +@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. + * + *

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. + * + *

NOTE: The parameter pkg 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 launchActivity( + String pkg, + Class 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. + * + *

NOTE: The parameter pkg 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 launchActivityWithIntent( + String pkg, + Class 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(); + } +} \ No newline at end of file diff --git a/src/test-runner/android/test/InstrumentationTestRunner.java b/src/test-runner/android/test/InstrumentationTestRunner.java new file mode 100644 index 00000000..b2a88470 --- /dev/null +++ b/src/test-runner/android/test/InstrumentationTestRunner.java @@ -0,0 +1,853 @@ +/* + * 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 static android.test.suitebuilder.TestPredicates.hasAnnotation; + +import android.app.Activity; +import android.app.Instrumentation; +import android.os.Bundle; +import android.os.Debug; +import android.os.Looper; +import android.test.suitebuilder.TestMethod; +import android.test.suitebuilder.TestPredicates; +import android.test.suitebuilder.TestSuiteBuilder; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import com.android.internal.util.Predicate; + +import junit.framework.AssertionFailedError; +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 junit.textui.ResultPrinter; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * An {@link Instrumentation} that runs various types of {@link junit.framework.TestCase}s against + * an Android package (application). + * + *

+ *

Developer Guides

+ *

For more information about application testing, read the + * Testing developer guide.

+ *
+ * + *

Typical Usage

+ *
    + *
  1. Write {@link junit.framework.TestCase}s that perform unit, functional, or performance tests + * against the classes in your package. Typically these are subclassed from: + *
    • {@link android.test.ActivityInstrumentationTestCase2}
    • + *
    • {@link android.test.ActivityUnitTestCase}
    • + *
    • {@link android.test.AndroidTestCase}
    • + *
    • {@link android.test.ApplicationTestCase}
    • + *
    • {@link android.test.InstrumentationTestCase}
    • + *
    • {@link android.test.ProviderTestCase}
    • + *
    • {@link android.test.ServiceTestCase}
    • + *
    • {@link android.test.SingleLaunchActivityTestCase}
    + *
  2. Set the android:targetPackage attribute of the <instrumentation> + * element in the test package's manifest. You should set the attribute value + * to the package name of the target application under test. + *
  3. Run the instrumentation using "adb shell am instrument -w", + * with no optional arguments, to run all tests (except performance tests). + *
  4. Run the instrumentation using "adb shell am instrument -w", + * with the argument '-e func true' to run all functional tests. These are tests that derive from + * {@link android.test.InstrumentationTestCase}. + *
  5. Run the instrumentation using "adb shell am instrument -w", + * with the argument '-e unit true' to run all unit tests. These are tests that do notderive + * from {@link android.test.InstrumentationTestCase} (and are not performance tests). + *
  6. Run the instrumentation using "adb shell am instrument -w", + * with the argument '-e class' set to run an individual {@link junit.framework.TestCase}. + *
+ *

+ * Running all tests: adb shell am instrument -w + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running all small tests: adb shell am instrument -w + * -e size small + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running all medium tests: adb shell am instrument -w + * -e size medium + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running all large tests: adb shell am instrument -w + * -e size large + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Filter test run to tests with given annotation: adb shell am instrument -w + * -e annotation com.android.foo.MyAnnotation + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * If used with other options, the resulting test run will contain the union of the two options. + * e.g. "-e size large -e annotation com.android.foo.MyAnnotation" will run only tests with both + * the {@link LargeTest} and "com.android.foo.MyAnnotation" annotations. + *

+ * Filter test run to tests without given annotation: adb shell am instrument -w + * -e notAnnotation com.android.foo.MyAnnotation + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running a single testcase: adb shell am instrument -w + * -e class com.android.foo.FooTest + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running a single test: adb shell am instrument -w + * -e class com.android.foo.FooTest#testFoo + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running multiple tests: adb shell am instrument -w + * -e class com.android.foo.FooTest,com.android.foo.TooTest + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Running all tests in a java package: adb shell am instrument -w + * -e package com.android.foo.subpkg + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * Including performance tests: adb shell am instrument -w + * -e perf true + * com.android.foo/android.test.InstrumentationTestRunner + *

+ * To debug your tests, set a break point in your code and pass: + * -e debug true + *

+ * To run in 'log only' mode + * -e log true + * This option will load and iterate through all test classes and methods, but will bypass actual + * test execution. Useful for quickly obtaining info on the tests to be executed by an + * instrumentation command. + *

+ * To generate EMMA code coverage: + * -e coverage true + * Note: this requires an emma instrumented build. By default, the code coverage results file + * will be saved in a /data//coverage.ec file, unless overridden by coverageFile flag (see + * below) + *

+ * To specify EMMA code coverage results file path: + * -e coverageFile /sdcard/myFile.ec + *
+ * in addition to the other arguments. + * @deprecated Use + * + * AndroidJUnitRunner instead. New tests should be written using the + * Android Testing Support Library. + */ + +/* (not JavaDoc) + * Although not necessary in most case, another way to use this class is to extend it and have the + * derived class return the desired test suite from the {@link #getTestSuite()} method. The test + * suite returned from this method will be used if no target class is defined in the meta-data or + * command line argument parameters. If a derived class is used it needs to be added as an + * instrumentation to the AndroidManifest.xml and the command to run it would look like: + *

+ * adb shell am instrument -w com.android.foo/com.android.FooInstrumentationTestRunner + *

+ * Where com.android.FooInstrumentationTestRunner is the derived class. + * + * This model is used by many existing app tests, but can probably be deprecated. + */ +@Deprecated +public class InstrumentationTestRunner extends Instrumentation implements TestSuiteProvider { + + /** @hide */ + static final String ARGUMENT_TEST_CLASS = "class"; + /** @hide */ + private static final String ARGUMENT_TEST_PACKAGE = "package"; + /** @hide */ + private static final String ARGUMENT_TEST_SIZE_PREDICATE = "size"; + /** @hide */ + static final String ARGUMENT_DELAY_MSEC = "delay_msec"; + + private static final String SMALL_SUITE = "small"; + private static final String MEDIUM_SUITE = "medium"; + private static final String LARGE_SUITE = "large"; + + private static final String ARGUMENT_LOG_ONLY = "log"; + /** @hide */ + static final String ARGUMENT_ANNOTATION = "annotation"; + /** @hide */ + static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation"; + + private static final Predicate SELECT_SMALL = hasAnnotation(SmallTest.class); + + private static final Predicate SELECT_MEDIUM = hasAnnotation(MediumTest.class); + + private static final Predicate SELECT_LARGE = hasAnnotation(LargeTest.class); + + /** + * This constant defines the maximum allowed runtime (in ms) for a test included in the "small" + * suite. It is used to make an educated guess at what suite an unlabeled test belongs. + */ + private static final float SMALL_SUITE_MAX_RUNTIME = 100; + + /** + * This constant defines the maximum allowed runtime (in ms) for a test included in the + * "medium" suite. It is used to make an educated guess at what suite an unlabeled test belongs. + */ + private static final float MEDIUM_SUITE_MAX_RUNTIME = 1000; + + /* + * The following keys are used in the status bundle to provide structured reports to + * an IInstrumentationWatcher. + */ + + /** + * This value, if stored with key {@link android.app.Instrumentation#REPORT_KEY_IDENTIFIER}, + * identifies InstrumentationTestRunner as the source of the report. This is sent with all + * status messages. + */ + public static final String REPORT_VALUE_ID = "InstrumentationTestRunner"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * identifies the total number of tests that are being run. This is sent with all status + * messages. + */ + public static final String REPORT_KEY_NUM_TOTAL = "numtests"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * identifies the sequence number of the current test. This is sent with any status message + * describing a specific test being started or completed. + */ + public static final String REPORT_KEY_NUM_CURRENT = "current"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * identifies the name of the current test class. This is sent with any status message + * describing a specific test being started or completed. + */ + public static final String REPORT_KEY_NAME_CLASS = "class"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * identifies the name of the current test. This is sent with any status message + * describing a specific test being started or completed. + */ + public static final String REPORT_KEY_NAME_TEST = "test"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * reports the run time in seconds of the current test. + */ + private static final String REPORT_KEY_RUN_TIME = "runtime"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * reports the number of total iterations of the current test. + */ + private static final String REPORT_KEY_NUM_ITERATIONS = "numiterations"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * reports the guessed suite assignment for the current test. + */ + private static final String REPORT_KEY_SUITE_ASSIGNMENT = "suiteassignment"; + /** + * If included in the status or final bundle sent to an IInstrumentationWatcher, this key + * identifies the path to the generated code coverage file. + */ + private static final String REPORT_KEY_COVERAGE_PATH = "coverageFilePath"; + + /** + * The test is starting. + */ + public static final int REPORT_VALUE_RESULT_START = 1; + /** + * The test completed successfully. + */ + public static final int REPORT_VALUE_RESULT_OK = 0; + /** + * The test completed with an error. + */ + public static final int REPORT_VALUE_RESULT_ERROR = -1; + /** + * The test completed with a failure. + */ + public static final int REPORT_VALUE_RESULT_FAILURE = -2; + /** + * If included in the status bundle sent to an IInstrumentationWatcher, this key + * identifies a stack trace describing an error or failure. This is sent with any status + * message describing a specific test being completed. + */ + public static final String REPORT_KEY_STACK = "stack"; + + // Default file name for code coverage + private static final String DEFAULT_COVERAGE_FILE_NAME = "coverage.ec"; + + private static final String LOG_TAG = "InstrumentationTestRunner"; + + private final Bundle mResults = new Bundle(); + private Bundle mArguments; + private AndroidTestRunner mTestRunner; + private boolean mDebug; + private boolean mJustCount; + private boolean mSuiteAssignmentMode; + private int mTestCount; + private String mPackageOfTests; + private boolean mCoverage; + private String mCoverageFilePath; + private int mDelayMsec; + + @Override + public void onCreate(Bundle arguments) { + super.onCreate(arguments); + mArguments = arguments; + + // Apk paths used to search for test classes when using TestSuiteBuilders. + String[] apkPaths = + {getTargetContext().getPackageCodePath(), getContext().getPackageCodePath()}; + ClassPathPackageInfoSource.setApkPaths(apkPaths); + + Predicate testSizePredicate = null; + Predicate testAnnotationPredicate = null; + Predicate testNotAnnotationPredicate = null; + String testClassesArg = null; + boolean logOnly = false; + + if (arguments != null) { + // Test class name passed as an argument should override any meta-data declaration. + testClassesArg = arguments.getString(ARGUMENT_TEST_CLASS); + mDebug = getBooleanArgument(arguments, "debug"); + mJustCount = getBooleanArgument(arguments, "count"); + mSuiteAssignmentMode = getBooleanArgument(arguments, "suiteAssignment"); + mPackageOfTests = arguments.getString(ARGUMENT_TEST_PACKAGE); + testSizePredicate = getSizePredicateFromArg( + arguments.getString(ARGUMENT_TEST_SIZE_PREDICATE)); + testAnnotationPredicate = getAnnotationPredicate( + arguments.getString(ARGUMENT_ANNOTATION)); + testNotAnnotationPredicate = getNotAnnotationPredicate( + arguments.getString(ARGUMENT_NOT_ANNOTATION)); + + logOnly = getBooleanArgument(arguments, ARGUMENT_LOG_ONLY); + mCoverage = getBooleanArgument(arguments, "coverage"); + mCoverageFilePath = arguments.getString("coverageFile"); + + try { + Object delay = arguments.get(ARGUMENT_DELAY_MSEC); // Accept either string or int + if (delay != null) mDelayMsec = Integer.parseInt(delay.toString()); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, "Invalid delay_msec parameter", e); + } + } + + TestSuiteBuilder testSuiteBuilder = new TestSuiteBuilder(getClass().getName(), + getTargetContext().getClassLoader()); + + if (testSizePredicate != null) { + testSuiteBuilder.addRequirements(testSizePredicate); + } + if (testAnnotationPredicate != null) { + testSuiteBuilder.addRequirements(testAnnotationPredicate); + } + if (testNotAnnotationPredicate != null) { + testSuiteBuilder.addRequirements(testNotAnnotationPredicate); + } + + if (testClassesArg == null) { + if (mPackageOfTests != null) { + testSuiteBuilder.includePackages(mPackageOfTests); + } else { + TestSuite testSuite = getTestSuite(); + if (testSuite != null) { + testSuiteBuilder.addTestSuite(testSuite); + } else { + // no package or class bundle arguments were supplied, and no test suite + // provided so add all tests in application + testSuiteBuilder.includePackages(""); + } + } + } else { + parseTestClasses(testClassesArg, testSuiteBuilder); + } + + testSuiteBuilder.addRequirements(getBuilderRequirements()); + + mTestRunner = getAndroidTestRunner(); + mTestRunner.setContext(getTargetContext()); + mTestRunner.setInstrumentation(this); + mTestRunner.setSkipExecution(logOnly); + mTestRunner.setTest(testSuiteBuilder.build()); + mTestCount = mTestRunner.getTestCases().size(); + if (mSuiteAssignmentMode) { + mTestRunner.addTestListener(new SuiteAssignmentPrinter()); + } else { + WatcherResultPrinter resultPrinter = new WatcherResultPrinter(mTestCount); + mTestRunner.addTestListener(new TestPrinter("TestRunner", false)); + mTestRunner.addTestListener(resultPrinter); + } + start(); + } + + /** + * Get the arguments passed to this instrumentation. + * + * @return the Bundle object + */ + public Bundle getArguments() { + return mArguments; + } + + /** + * Add a {@link TestListener} + */ + protected void addTestListener(TestListener listener){ + if(mTestRunner!=null && listener!=null){ + mTestRunner.addTestListener(listener); + } + } + + List> getBuilderRequirements() { + return new ArrayList>(); + } + + /** + * Parses and loads the specified set of test classes + * + * @param testClassArg - comma-separated list of test classes and methods + * @param testSuiteBuilder - builder to add tests to + */ + private void parseTestClasses(String testClassArg, TestSuiteBuilder testSuiteBuilder) { + String[] testClasses = testClassArg.split(","); + for (String testClass : testClasses) { + parseTestClass(testClass, testSuiteBuilder); + } + } + + /** + * Parse and load the given test class and, optionally, method + * + * @param testClassName - full package name of test class and optionally method to add. + * Expected format: com.android.TestClass#testMethod + * @param testSuiteBuilder - builder to add tests to + */ + private void parseTestClass(String testClassName, TestSuiteBuilder testSuiteBuilder) { + int methodSeparatorIndex = testClassName.indexOf('#'); + String testMethodName = null; + + if (methodSeparatorIndex > 0) { + testMethodName = testClassName.substring(methodSeparatorIndex + 1); + testClassName = testClassName.substring(0, methodSeparatorIndex); + } + testSuiteBuilder.addTestClassByName(testClassName, testMethodName, getTargetContext()); + } + + protected AndroidTestRunner getAndroidTestRunner() { + return new AndroidTestRunner(); + } + + private boolean getBooleanArgument(Bundle arguments, String tag) { + String tagString = arguments.getString(tag); + return tagString != null && Boolean.parseBoolean(tagString); + } + + /* + * Returns the size predicate object, corresponding to the "size" argument value. + */ + private Predicate getSizePredicateFromArg(String sizeArg) { + + if (SMALL_SUITE.equals(sizeArg)) { + return SELECT_SMALL; + } else if (MEDIUM_SUITE.equals(sizeArg)) { + return SELECT_MEDIUM; + } else if (LARGE_SUITE.equals(sizeArg)) { + return SELECT_LARGE; + } else { + return null; + } + } + + /** + * Returns the test predicate object, corresponding to the annotation class value provided via + * the {@link #ARGUMENT_ANNOTATION} argument. + * + * @return the predicate or null + */ + private Predicate getAnnotationPredicate(String annotationClassName) { + Class annotationClass = getAnnotationClass(annotationClassName); + if (annotationClass != null) { + return hasAnnotation(annotationClass); + } + return null; + } + + /** + * Returns the negative test predicate object, corresponding to the annotation class value + * provided via the {@link #ARGUMENT_NOT_ANNOTATION} argument. + * + * @return the predicate or null + */ + private Predicate getNotAnnotationPredicate(String annotationClassName) { + Class annotationClass = getAnnotationClass(annotationClassName); + if (annotationClass != null) { + return TestPredicates.not(hasAnnotation(annotationClass)); + } + return null; + } + + /** + * Helper method to return the annotation class with specified name + * + * @param annotationClassName the fully qualified name of the class + * @return the annotation class or null + */ + private Class getAnnotationClass(String annotationClassName) { + if (annotationClassName == null) { + return null; + } + try { + Class annotationClass = Class.forName(annotationClassName); + if (annotationClass.isAnnotation()) { + return (Class)annotationClass; + } else { + Log.e(LOG_TAG, String.format("Provided annotation value %s is not an Annotation", + annotationClassName)); + } + } catch (ClassNotFoundException e) { + Log.e(LOG_TAG, String.format("Could not find class for specified annotation %s", + annotationClassName)); + } + return null; + } + + /** + * Initialize the current thread as a looper. + *

+ * Exposed for unit testing. + */ + void prepareLooper() { + Looper.prepare(); + } + + @Override + public void onStart() { + prepareLooper(); + + if (mJustCount) { + mResults.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID); + mResults.putInt(REPORT_KEY_NUM_TOTAL, mTestCount); + finish(Activity.RESULT_OK, mResults); + } else { + if (mDebug) { + Debug.waitForDebugger(); + } + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + PrintStream writer = new PrintStream(byteArrayOutputStream); + try { + StringResultPrinter resultPrinter = new StringResultPrinter(writer); + + mTestRunner.addTestListener(resultPrinter); + + long startTime = System.currentTimeMillis(); + mTestRunner.runTest(); + long runTime = System.currentTimeMillis() - startTime; + + resultPrinter.printResult(mTestRunner.getTestResult(), runTime); + } catch (Throwable t) { + // catch all exceptions so a more verbose error message can be outputted + writer.println(String.format("Test run aborted due to unexpected exception: %s", + t.getMessage())); + t.printStackTrace(writer); + } finally { + mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\nTest results for %s=%s", + mTestRunner.getTestClassName(), + byteArrayOutputStream.toString())); + + if (mCoverage) { + generateCoverageReport(); + } + writer.close(); + + finish(Activity.RESULT_OK, mResults); + } + } + } + + public TestSuite getTestSuite() { + return getAllTests(); + } + + /** + * Override this to define all of the tests to run in your package. + */ + public TestSuite getAllTests() { + return null; + } + + /** + * Override this to provide access to the class loader of your package. + */ + public ClassLoader getLoader() { + return null; + } + + private void generateCoverageReport() { + // use reflection to call emma dump coverage method, to avoid + // always statically compiling against emma jar + String coverageFilePath = getCoverageFilePath(); + java.io.File coverageFile = new java.io.File(coverageFilePath); + try { + Class emmaRTClass = Class.forName("com.vladium.emma.rt.RT"); + Method dumpCoverageMethod = emmaRTClass.getMethod("dumpCoverageData", + coverageFile.getClass(), boolean.class, boolean.class); + + dumpCoverageMethod.invoke(null, coverageFile, false, false); + // output path to generated coverage file so it can be parsed by a test harness if + // needed + mResults.putString(REPORT_KEY_COVERAGE_PATH, coverageFilePath); + // also output a more user friendly msg + final String currentStream = mResults.getString( + Instrumentation.REPORT_KEY_STREAMRESULT); + mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("%s\nGenerated code coverage data to %s", currentStream, + coverageFilePath)); + } catch (ClassNotFoundException e) { + reportEmmaError("Is emma jar on classpath?", e); + } catch (SecurityException e) { + reportEmmaError(e); + } catch (NoSuchMethodException e) { + reportEmmaError(e); + } catch (IllegalArgumentException e) { + reportEmmaError(e); + } catch (IllegalAccessException e) { + reportEmmaError(e); + } catch (InvocationTargetException e) { + reportEmmaError(e); + } + } + + private String getCoverageFilePath() { + if (mCoverageFilePath == null) { + return getTargetContext().getFilesDir().getAbsolutePath() + File.separator + + DEFAULT_COVERAGE_FILE_NAME; + } else { + return mCoverageFilePath; + } + } + + private void reportEmmaError(Exception e) { + reportEmmaError("", e); + } + + private void reportEmmaError(String hint, Exception e) { + String msg = "Failed to generate emma coverage. " + hint; + Log.e(LOG_TAG, msg, e); + mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: " + msg); + } + + // TODO kill this, use status() and prettyprint model for better output + private class StringResultPrinter extends ResultPrinter { + + public StringResultPrinter(PrintStream writer) { + super(writer); + } + + public synchronized void printResult(TestResult result, long runTime) { + printHeader(runTime); + printFooter(result); + } + } + + /** + * This class sends status reports back to the IInstrumentationWatcher about + * which suite each test belongs. + */ + private class SuiteAssignmentPrinter implements TestListener { + + private Bundle mTestResult; + private long mStartTime; + private long mEndTime; + private boolean mTimingValid; + + public SuiteAssignmentPrinter() { + } + + /** + * send a status for the start of a each test, so long tests can be seen as "running" + */ + public void startTest(Test test) { + mTimingValid = true; + mStartTime = System.currentTimeMillis(); + } + + /** + * @see junit.framework.TestListener#addError(Test, Throwable) + */ + public void addError(Test test, Throwable t) { + mTimingValid = false; + } + + /** + * @see junit.framework.TestListener#addFailure(Test, AssertionFailedError) + */ + public void addFailure(Test test, AssertionFailedError t) { + mTimingValid = false; + } + + /** + * @see junit.framework.TestListener#endTest(Test) + */ + public void endTest(Test test) { + float runTime; + String assignmentSuite; + mEndTime = System.currentTimeMillis(); + mTestResult = new Bundle(); + + if (!mTimingValid || mStartTime < 0) { + assignmentSuite = "NA"; + runTime = -1; + } else { + runTime = mEndTime - mStartTime; + if (runTime < SMALL_SUITE_MAX_RUNTIME + && !InstrumentationTestCase.class.isAssignableFrom(test.getClass())) { + assignmentSuite = SMALL_SUITE; + } else if (runTime < MEDIUM_SUITE_MAX_RUNTIME) { + assignmentSuite = MEDIUM_SUITE; + } else { + assignmentSuite = LARGE_SUITE; + } + } + // Clear mStartTime so that we can verify that it gets set next time. + mStartTime = -1; + + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + test.getClass().getName() + "#" + ((TestCase) test).getName() + + "\nin " + assignmentSuite + " suite\nrunTime: " + + String.valueOf(runTime) + "\n"); + mTestResult.putFloat(REPORT_KEY_RUN_TIME, runTime); + mTestResult.putString(REPORT_KEY_SUITE_ASSIGNMENT, assignmentSuite); + + sendStatus(0, mTestResult); + } + } + + /** + * This class sends status reports back to the IInstrumentationWatcher + */ + private class WatcherResultPrinter implements TestListener { + private final Bundle mResultTemplate; + Bundle mTestResult; + int mTestNum = 0; + int mTestResultCode = 0; + String mTestClass = null; + + public WatcherResultPrinter(int numTests) { + mResultTemplate = new Bundle(); + mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID); + mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, numTests); + } + + /** + * send a status for the start of a each test, so long tests can be seen + * as "running" + */ + public void startTest(Test test) { + String testClass = test.getClass().getName(); + String testName = ((TestCase)test).getName(); + mTestResult = new Bundle(mResultTemplate); + mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass); + mTestResult.putString(REPORT_KEY_NAME_TEST, testName); + mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum); + // pretty printing + if (testClass != null && !testClass.equals(mTestClass)) { + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\n%s:", testClass)); + mTestClass = testClass; + } else { + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ""); + } + + Method testMethod = null; + try { + testMethod = test.getClass().getMethod(testName); + // Report total number of iterations, if test is repetitive + if (testMethod.isAnnotationPresent(RepetitiveTest.class)) { + int numIterations = testMethod.getAnnotation( + RepetitiveTest.class).numIterations(); + mTestResult.putInt(REPORT_KEY_NUM_ITERATIONS, numIterations); + } + } catch (NoSuchMethodException e) { + // ignore- the test with given name does not exist. Will be handled during test + // execution + } + + // The delay_msec parameter is normally used to provide buffers of idle time + // for power measurement purposes. To make sure there is a delay before and after + // every test in a suite, we delay *after* every test (see endTest below) and also + // delay *before* the first test. So, delay test1 delay test2 delay. + + try { + if (mTestNum == 1) Thread.sleep(mDelayMsec); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + sendStatus(REPORT_VALUE_RESULT_START, mTestResult); + mTestResultCode = 0; + } + + /** + * @see junit.framework.TestListener#addError(Test, Throwable) + */ + public void addError(Test test, Throwable t) { + mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t)); + mTestResultCode = REPORT_VALUE_RESULT_ERROR; + // pretty printing + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\nError in %s:\n%s", + ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t))); + } + + /** + * @see junit.framework.TestListener#addFailure(Test, AssertionFailedError) + */ + public void addFailure(Test test, AssertionFailedError t) { + mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t)); + mTestResultCode = REPORT_VALUE_RESULT_FAILURE; + // pretty printing + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, + String.format("\nFailure in %s:\n%s", + ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t))); + } + + /** + * @see junit.framework.TestListener#endTest(Test) + */ + public void endTest(Test test) { + if (mTestResultCode == 0) { + mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "."); + } + sendStatus(mTestResultCode, mTestResult); + + try { // Sleep after every test, if specified + Thread.sleep(mDelayMsec); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + // TODO report the end of the cycle + } +} diff --git a/src/test-runner/android/test/InstrumentationTestSuite.java b/src/test-runner/android/test/InstrumentationTestSuite.java new file mode 100644 index 00000000..a53fa267 --- /dev/null +++ b/src/test-runner/android/test/InstrumentationTestSuite.java @@ -0,0 +1,80 @@ +/* + * 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 junit.framework.TestSuite; +import junit.framework.Test; +import junit.framework.TestResult; + +/** + * A {@link junit.framework.TestSuite} that injects {@link android.app.Instrumentation} into + * {@link InstrumentationTestCase} before running them. + * + * @deprecated Use + * + * InstrumentationRegistry instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class InstrumentationTestSuite extends TestSuite { + + private final Instrumentation mInstrumentation; + + /** + * @param instr The instrumentation that will be injected into each + * test before running it. + */ + public InstrumentationTestSuite(Instrumentation instr) { + mInstrumentation = instr; + } + + + public InstrumentationTestSuite(String name, Instrumentation instr) { + super(name); + mInstrumentation = instr; + } + + /** + * @param theClass Inspected for methods starting with 'test' + * @param instr The instrumentation to inject into each test before + * running. + */ + public InstrumentationTestSuite(final Class theClass, Instrumentation instr) { + super(theClass); + mInstrumentation = instr; + } + + + @Override + public void addTestSuite(Class testClass) { + addTest(new InstrumentationTestSuite(testClass, mInstrumentation)); + } + + + @Override + public void runTest(Test test, TestResult result) { + + if (test instanceof InstrumentationTestCase) { + ((InstrumentationTestCase) test).injectInstrumentation(mInstrumentation); + } + + // run the test as usual + super.runTest(test, result); + } +} diff --git a/src/test-runner/android/test/IsolatedContext.java b/src/test-runner/android/test/IsolatedContext.java new file mode 100644 index 00000000..d5f92a31 --- /dev/null +++ b/src/test-runner/android/test/IsolatedContext.java @@ -0,0 +1,146 @@ +/* + * 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.accounts.AccountManager; +import android.content.AttributionSource; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Process; +import android.test.mock.MockAccountManager; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + + +/** + * A mock context which prevents its users from talking to the rest of the device while + * stubbing enough methods to satify code that tries to talk to other packages. + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class IsolatedContext extends ContextWrapper { + + private ContentResolver mResolver; + private final AccountManager mMockAccountManager; + + private List mBroadcastIntents = new ArrayList<>(); + + public IsolatedContext( + ContentResolver resolver, Context targetContext) { + super(targetContext); + mResolver = resolver; + mMockAccountManager = MockAccountManager.newMockAccountManager(IsolatedContext.this); + } + + /** Returns the list of intents that were broadcast since the last call to this method. */ + public List getAndClearBroadcastIntents() { + List intents = mBroadcastIntents; + mBroadcastIntents = new ArrayList<>(); + return intents; + } + + @Override + public AttributionSource getAttributionSource() { + AttributionSource attributionSource = super.getAttributionSource(); + if (attributionSource == null) { + return new AttributionSource.Builder(Process.myUid()).build(); + } + return attributionSource; + } + + @Override + public ContentResolver getContentResolver() { + // We need to return the real resolver so that MailEngine.makeRight can get to the + // subscribed feeds provider. TODO: mock out subscribed feeds too. + return mResolver; + } + + @Override + public boolean bindService(Intent service, ServiceConnection conn, int flags) { + return false; + } + + @Override + public boolean bindService(Intent service, int flags, Executor executor, + ServiceConnection conn) { + return false; + } + + @Override + public boolean bindIsolatedService(Intent service, int flags, String instanceName, + Executor executor, ServiceConnection conn) { + return false; + } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + return null; + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + // Ignore + } + + @Override + public void sendBroadcast(Intent intent) { + mBroadcastIntents.add(intent); + } + + @Override + public void sendOrderedBroadcast(Intent intent, String receiverPermission) { + mBroadcastIntents.add(intent); + } + + @Override + public int checkUriPermission( + Uri uri, String readPermission, String writePermission, int pid, + int uid, int modeFlags) { + return PackageManager.PERMISSION_GRANTED; + } + + @Override + public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { + return PackageManager.PERMISSION_GRANTED; + } + + @Override + public Object getSystemService(String name) { + if (Context.ACCOUNT_SERVICE.equals(name)) { + return mMockAccountManager; + } + // No other services exist in this context. + return null; + } + + @Override + public File getFilesDir() { + return new File("/dev/null"); + } +} diff --git a/src/test-runner/android/test/LaunchPerformanceBase.java b/src/test-runner/android/test/LaunchPerformanceBase.java new file mode 100644 index 00000000..d87a8119 --- /dev/null +++ b/src/test-runner/android/test/LaunchPerformanceBase.java @@ -0,0 +1,50 @@ +/* + * 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.Intent; +import android.os.Bundle; + + +/** + * Base class for all launch performance Instrumentation classes. + */ +@Deprecated +public class LaunchPerformanceBase extends Instrumentation { + + /** @hide */ + public static final String LOG_TAG = "Launch Performance"; + + protected Bundle mResults; + protected Intent mIntent; + + public LaunchPerformanceBase() { + mResults = new Bundle(); + mIntent = new Intent(Intent.ACTION_MAIN); + mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + setAutomaticPerformanceSnapshots(); + } + + /** + * Launches intent, and waits for idle before returning. + */ + protected void LaunchApp() { + startActivitySync(mIntent); + waitForIdleSync(); + } +} diff --git a/src/test-runner/android/test/LoaderTestCase.java b/src/test-runner/android/test/LoaderTestCase.java new file mode 100644 index 00000000..c8564c2e --- /dev/null +++ b/src/test-runner/android/test/LoaderTestCase.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2010 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.Loader; +import android.content.Loader.OnLoadCompleteListener; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * A convenience class for testing {@link Loader}s. This test case + * provides a simple way to synchronously get the result from a Loader making + * it easy to assert that the Loader returns the expected result. + */ +public class LoaderTestCase extends AndroidTestCase { + static { + // Force class loading of AsyncTask on the main thread so that it's handlers are tied to + // the main thread and responses from the worker thread get delivered on the main thread. + // The tests are run on another thread, allowing them to block waiting on a response from + // the code running on the main thread. The main thread can't block since the AysncTask + // results come in via the event loop. + new AsyncTask() { + @Override + protected Void doInBackground(Void... args) {return null;} + @Override + protected void onPostExecute(Void result) {} + }; + } + + /** + * Runs a Loader synchronously and returns the result of the load. The loader will + * be started, stopped, and destroyed by this method so it cannot be reused. + * + * @param loader The loader to run synchronously + * @return The result from the loader + */ + public T getLoaderResultSynchronously(final Loader loader) { + // The test thread blocks on this queue until the loader puts it's result in + final ArrayBlockingQueue queue = new ArrayBlockingQueue(1); + + // This callback runs on the "main" thread and unblocks the test thread + // when it puts the result into the blocking queue + final OnLoadCompleteListener listener = new OnLoadCompleteListener() { + @Override + public void onLoadComplete(Loader completedLoader, T data) { + // Shut the loader down + completedLoader.unregisterListener(this); + completedLoader.stopLoading(); + completedLoader.reset(); + + // Store the result, unblocking the test thread + queue.add(data); + } + }; + + // This handler runs on the "main" thread of the process since AsyncTask + // is documented as needing to run on the main thread and many Loaders use + // AsyncTask + final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + loader.registerListener(0, listener); + loader.startLoading(); + } + }; + + // Ask the main thread to start the loading process + mainThreadHandler.sendEmptyMessage(0); + + // Block on the queue waiting for the result of the load to be inserted + T result; + while (true) { + try { + result = queue.take(); + break; + } catch (InterruptedException e) { + throw new RuntimeException("waiting thread interrupted", e); + } + } + + return result; + } +} diff --git a/src/test-runner/android/test/MoreAsserts.java b/src/test-runner/android/test/MoreAsserts.java new file mode 100644 index 00000000..d33911a1 --- /dev/null +++ b/src/test-runner/android/test/MoreAsserts.java @@ -0,0 +1,585 @@ +/* + * 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 junit.framework.Assert; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.ArrayList; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Contains additional assertion methods not found in JUnit. + * @deprecated Use + * Hamcrest matchers instead. + */ +@Deprecated +public final class MoreAsserts { + + private MoreAsserts() { } + + /** + * Asserts that the class {@code expected} is assignable from the object + * {@code actual}. This verifies {@code expected} is a parent class or a + * interface that {@code actual} implements. + */ + public static void assertAssignableFrom(Class expected, Object actual) { + assertAssignableFrom(expected, actual.getClass()); + } + + /** + * Asserts that class {@code expected} is assignable from the class + * {@code actual}. This verifies {@code expected} is a parent class or a + * interface that {@code actual} implements. + */ + public static void assertAssignableFrom(Class expected, Class actual) { + Assert.assertTrue( + "Expected " + expected.getCanonicalName() + + " to be assignable from actual class " + actual.getCanonicalName(), + expected.isAssignableFrom(actual)); + } + + /** + * Asserts that {@code actual} is not equal {@code unexpected}, according + * to both {@code ==} and {@link Object#equals}. + */ + public static void assertNotEqual( + String message, Object unexpected, Object actual) { + if (equal(unexpected, actual)) { + failEqual(message, unexpected); + } + } + + /** + * Variant of {@link #assertNotEqual(String,Object,Object)} using a + * generic message. + */ + public static void assertNotEqual(Object unexpected, Object actual) { + assertNotEqual(null, unexpected, actual); + } + + /** + * Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates specific + * element mismatch. + */ + public static void assertEquals( + String message, byte[] expected, byte[] actual) { + if (expected.length != actual.length) { + failWrongLength(message, expected.length, actual.length); + } + for (int i = 0; i < expected.length; i++) { + if (expected[i] != actual[i]) { + failWrongElement(message, i, expected[i], actual[i]); + } + } + } + + /** + * Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates specific + * element mismatch. + */ + public static void assertEquals(byte[] expected, byte[] actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates first + * specific element mismatch. + */ + public static void assertEquals( + String message, int[] expected, int[] actual) { + if (expected.length != actual.length) { + failWrongLength(message, expected.length, actual.length); + } + for (int i = 0; i < expected.length; i++) { + if (expected[i] != actual[i]) { + failWrongElement(message, i, expected[i], actual[i]); + } + } + } + + /** + * Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates first + * specific element mismatch. + */ + public static void assertEquals(int[] expected, int[] actual) { + assertEquals(null, expected, actual); + } + + /** + * @hide Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates first + * specific element mismatch. + */ + public static void assertEquals( + String message, long[] expected, long[] actual) { + if (expected.length != actual.length) { + failWrongLength(message, expected.length, actual.length); + } + for (int i = 0; i < expected.length; i++) { + if (expected[i] != actual[i]) { + failWrongElement(message, i, expected[i], actual[i]); + } + } + } + + /** + * @hide Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates first + * specific element mismatch. + */ + public static void assertEquals(long[] expected, long[] actual) { + assertEquals(null, expected, actual); + } + + + /** + * Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates first + * specific element mismatch. + */ + public static void assertEquals( + String message, double[] expected, double[] actual) { + if (expected.length != actual.length) { + failWrongLength(message, expected.length, actual.length); + } + for (int i = 0; i < expected.length; i++) { + if (expected[i] != actual[i]) { + failWrongElement(message, i, expected[i], actual[i]); + } + } + } + + /** + * Asserts that array {@code actual} is the same size and every element equals + * those in array {@code expected}. On failure, message indicates first + * specific element mismatch. + */ + public static void assertEquals(double[] expected, double[] actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that array {@code actual} is the same size and every element + * is the same as those in array {@code expected}. Note that this uses + * {@code equals()} instead of {@code ==} to compare the objects. + * {@code null} will be considered equal to {@code null} (unlike SQL). + * On failure, message indicates first specific element mismatch. + */ + public static void assertEquals( + String message, Object[] expected, Object[] actual) { + if (expected.length != actual.length) { + failWrongLength(message, expected.length, actual.length); + } + for (int i = 0; i < expected.length; i++) { + Object exp = expected[i]; + Object act = actual[i]; + // The following borrowed from java.util.equals(Object[], Object[]). + if (!((exp==null) ? act==null : exp.equals(act))) { + failWrongElement(message, i, exp, act); + } + } + } + + /** + * Asserts that array {@code actual} is the same size and every element + * is the same as those in array {@code expected}. Note that this uses + * {@code ==} instead of {@code equals()} to compare the objects. + * On failure, message indicates first specific element mismatch. + */ + public static void assertEquals(Object[] expected, Object[] actual) { + assertEquals(null, expected, actual); + } + + /** Asserts that two sets contain the same elements. */ + public static void assertEquals( + String message, Set expected, Set actual) { + Set onlyInExpected = new HashSet(expected); + onlyInExpected.removeAll(actual); + Set onlyInActual = new HashSet(actual); + onlyInActual.removeAll(expected); + if (onlyInExpected.size() != 0 || onlyInActual.size() != 0) { + Set intersection = new HashSet(expected); + intersection.retainAll(actual); + failWithMessage( + message, + "Sets do not match.\nOnly in expected: " + onlyInExpected + + "\nOnly in actual: " + onlyInActual + + "\nIntersection: " + intersection); + } + } + + /** Asserts that two sets contain the same elements. */ + public static void assertEquals(Set expected, Set actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that {@code expectedRegex} exactly matches {@code actual} and + * fails with {@code message} if it does not. The MatchResult is returned + * in case the test needs access to any captured groups. Note that you can + * also use this for a literal string, by wrapping your expected string in + * {@link Pattern#quote}. + */ + public static MatchResult assertMatchesRegex( + String message, String expectedRegex, String actual) { + if (actual == null) { + failNotMatches(message, expectedRegex, actual); + } + Matcher matcher = getMatcher(expectedRegex, actual); + if (!matcher.matches()) { + failNotMatches(message, expectedRegex, actual); + } + return matcher; + } + + /** + * Variant of {@link #assertMatchesRegex(String,String,String)} using a + * generic message. + */ + public static MatchResult assertMatchesRegex( + String expectedRegex, String actual) { + return assertMatchesRegex(null, expectedRegex, actual); + } + + /** + * Asserts that {@code expectedRegex} matches any substring of {@code actual} + * and fails with {@code message} if it does not. The Matcher is returned in + * case the test needs access to any captured groups. Note that you can also + * use this for a literal string, by wrapping your expected string in + * {@link Pattern#quote}. + */ + public static MatchResult assertContainsRegex( + String message, String expectedRegex, String actual) { + if (actual == null) { + failNotContains(message, expectedRegex, actual); + } + Matcher matcher = getMatcher(expectedRegex, actual); + if (!matcher.find()) { + failNotContains(message, expectedRegex, actual); + } + return matcher; + } + + /** + * Variant of {@link #assertContainsRegex(String,String,String)} using a + * generic message. + */ + public static MatchResult assertContainsRegex( + String expectedRegex, String actual) { + return assertContainsRegex(null, expectedRegex, actual); + } + + /** + * Asserts that {@code expectedRegex} does not exactly match {@code actual}, + * and fails with {@code message} if it does. Note that you can also use + * this for a literal string, by wrapping your expected string in + * {@link Pattern#quote}. + */ + public static void assertNotMatchesRegex( + String message, String expectedRegex, String actual) { + Matcher matcher = getMatcher(expectedRegex, actual); + if (matcher.matches()) { + failMatch(message, expectedRegex, actual); + } + } + + /** + * Variant of {@link #assertNotMatchesRegex(String,String,String)} using a + * generic message. + */ + public static void assertNotMatchesRegex( + String expectedRegex, String actual) { + assertNotMatchesRegex(null, expectedRegex, actual); + } + + /** + * Asserts that {@code expectedRegex} does not match any substring of + * {@code actual}, and fails with {@code message} if it does. Note that you + * can also use this for a literal string, by wrapping your expected string + * in {@link Pattern#quote}. + */ + public static void assertNotContainsRegex( + String message, String expectedRegex, String actual) { + Matcher matcher = getMatcher(expectedRegex, actual); + if (matcher.find()) { + failContains(message, expectedRegex, actual); + } + } + + /** + * Variant of {@link #assertNotContainsRegex(String,String,String)} using a + * generic message. + */ + public static void assertNotContainsRegex( + String expectedRegex, String actual) { + assertNotContainsRegex(null, expectedRegex, actual); + } + + /** + * Asserts that {@code actual} contains precisely the elements + * {@code expected}, and in the same order. + */ + public static void assertContentsInOrder( + String message, Iterable actual, Object... expected) { + ArrayList actualList = new ArrayList(); + for (Object o : actual) { + actualList.add(o); + } + Assert.assertEquals(message, Arrays.asList(expected), actualList); + } + + /** + * Variant of assertContentsInOrder(String, Iterable, Object...) + * using a generic message. + */ + public static void assertContentsInOrder( + Iterable actual, Object... expected) { + assertContentsInOrder((String) null, actual, expected); + } + + /** + * Asserts that {@code actual} contains precisely the elements + * {@code expected}, but in any order. + */ + public static void assertContentsInAnyOrder(String message, Iterable actual, + Object... expected) { + HashMap expectedMap = new HashMap(expected.length); + for (Object expectedObj : expected) { + expectedMap.put(expectedObj, expectedObj); + } + + for (Object actualObj : actual) { + if (expectedMap.remove(actualObj) == null) { + failWithMessage(message, "Extra object in actual: (" + actualObj.toString() + ")"); + } + } + + if (expectedMap.size() > 0) { + failWithMessage(message, "Extra objects in expected."); + } + } + + /** + * Variant of assertContentsInAnyOrder(String, Iterable, Object...) + * using a generic message. + */ + public static void assertContentsInAnyOrder(Iterable actual, Object... expected) { + assertContentsInAnyOrder((String)null, actual, expected); + } + + /** + * Asserts that {@code iterable} is empty. + */ + public static void assertEmpty(String message, Iterable iterable) { + if (iterable.iterator().hasNext()) { + failNotEmpty(message, iterable.toString()); + } + } + + /** + * Variant of {@link #assertEmpty(String, Iterable)} using a + * generic message. + */ + public static void assertEmpty(Iterable iterable) { + assertEmpty(null, iterable); + } + + /** + * Asserts that {@code map} is empty. + */ + public static void assertEmpty(String message, Map map) { + if (!map.isEmpty()) { + failNotEmpty(message, map.toString()); + } + } + + /** + * Variant of {@link #assertEmpty(String, Map)} using a generic + * message. + */ + public static void assertEmpty(Map map) { + assertEmpty(null, map); + } + + /** + * Asserts that {@code iterable} is not empty. + */ + public static void assertNotEmpty(String message, Iterable iterable) { + if (!iterable.iterator().hasNext()) { + failEmpty(message); + } + } + + /** + * Variant of assertNotEmpty(String, Iterable) + * using a generic message. + */ + public static void assertNotEmpty(Iterable iterable) { + assertNotEmpty(null, iterable); + } + + /** + * Asserts that {@code map} is not empty. + */ + public static void assertNotEmpty(String message, Map map) { + if (map.isEmpty()) { + failEmpty(message); + } + } + + /** + * Variant of {@link #assertNotEmpty(String, Map)} using a generic + * message. + */ + public static void assertNotEmpty(Map map) { + assertNotEmpty(null, map); + } + + /** + * Utility for testing equals() and hashCode() results at once. + * Tests that lhs.equals(rhs) matches expectedResult, as well as + * rhs.equals(lhs). Also tests that hashCode() return values are + * equal if expectedResult is true. (hashCode() is not tested if + * expectedResult is false, as unequal objects can have equal hashCodes.) + * + * @param lhs An Object for which equals() and hashCode() are to be tested. + * @param rhs As lhs. + * @param expectedResult True if the objects should compare equal, + * false if not. + */ + public static void checkEqualsAndHashCodeMethods( + String message, Object lhs, Object rhs, boolean expectedResult) { + + if ((lhs == null) && (rhs == null)) { + Assert.assertTrue( + "Your check is dubious...why would you expect null != null?", + expectedResult); + return; + } + + if ((lhs == null) || (rhs == null)) { + Assert.assertFalse( + "Your check is dubious...why would you expect an object " + + "to be equal to null?", expectedResult); + } + + if (lhs != null) { + Assert.assertEquals(message, expectedResult, lhs.equals(rhs)); + } + if (rhs != null) { + Assert.assertEquals(message, expectedResult, rhs.equals(lhs)); + } + + if (expectedResult) { + String hashMessage = + "hashCode() values for equal objects should be the same"; + if (message != null) { + hashMessage += ": " + message; + } + Assert.assertTrue(hashMessage, lhs.hashCode() == rhs.hashCode()); + } + } + + /** + * Variant of + * checkEqualsAndHashCodeMethods(String,Object,Object,boolean...)} + * using a generic message. + */ + public static void checkEqualsAndHashCodeMethods(Object lhs, Object rhs, + boolean expectedResult) { + checkEqualsAndHashCodeMethods((String) null, lhs, rhs, expectedResult); + } + + private static Matcher getMatcher(String expectedRegex, String actual) { + Pattern pattern = Pattern.compile(expectedRegex); + return pattern.matcher(actual); + } + + private static void failEqual(String message, Object unexpected) { + failWithMessage(message, "expected not to be:<" + unexpected + ">"); + } + + private static void failWrongLength( + String message, int expected, int actual) { + failWithMessage(message, "expected array length:<" + expected + + "> but was:<" + actual + '>'); + } + + private static void failWrongElement( + String message, int index, Object expected, Object actual) { + failWithMessage(message, "expected array element[" + index + "]:<" + + expected + "> but was:<" + actual + '>'); + } + + private static void failNotMatches( + String message, String expectedRegex, String actual) { + String actualDesc = (actual == null) ? "null" : ('<' + actual + '>'); + failWithMessage(message, "expected to match regex:<" + expectedRegex + + "> but was:" + actualDesc); + } + + private static void failNotContains( + String message, String expectedRegex, String actual) { + String actualDesc = (actual == null) ? "null" : ('<' + actual + '>'); + failWithMessage(message, "expected to contain regex:<" + expectedRegex + + "> but was:" + actualDesc); + } + + private static void failMatch( + String message, String expectedRegex, String actual) { + failWithMessage(message, "expected not to match regex:<" + expectedRegex + + "> but was:<" + actual + '>'); + } + + private static void failContains( + String message, String expectedRegex, String actual) { + failWithMessage(message, "expected not to contain regex:<" + expectedRegex + + "> but was:<" + actual + '>'); + } + + private static void failNotEmpty( + String message, String actual) { + failWithMessage(message, "expected to be empty, but contained: <" + + actual + ">"); + } + + private static void failEmpty(String message) { + failWithMessage(message, "expected not to be empty, but was"); + } + + private static void failWithMessage(String userMessage, String ourMessage) { + Assert.fail((userMessage == null) + ? ourMessage + : userMessage + ' ' + ourMessage); + } + + private static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + +} diff --git a/src/test-runner/android/test/NoExecTestResult.java b/src/test-runner/android/test/NoExecTestResult.java new file mode 100644 index 00000000..a01b6aad --- /dev/null +++ b/src/test-runner/android/test/NoExecTestResult.java @@ -0,0 +1,40 @@ +/* + * 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 junit.framework.TestCase; +import junit.framework.TestResult; + +/** + * A benign test result that does no actually test execution, just runs + * through the motions + * + * {@hide} Not needed for SDK. + */ +@Deprecated +class NoExecTestResult extends TestResult { + + /** + * Override parent to just inform listeners of test, + * and skip test execution. + */ + @Override + protected void run(final TestCase test) { + startTest(test); + endTest(test); + } + +} diff --git a/src/test-runner/android/test/PerformanceTestCase.java b/src/test-runner/android/test/PerformanceTestCase.java new file mode 100644 index 00000000..2584da20 --- /dev/null +++ b/src/test-runner/android/test/PerformanceTestCase.java @@ -0,0 +1,71 @@ +/* + * 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; + +/** + * More complex interface performance for test cases. + * + * If you want your test to be used as a performance test, you must + * implement this interface. + * + * @deprecated Use + * + * AndroidJUnitRunner instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public interface PerformanceTestCase +{ + /** + * Callbacks for {@link PerformanceTestCase}. + */ + public interface Intermediates + { + void setInternalIterations(int count); + void startTiming(boolean realTime); + void addIntermediate(String name); + void addIntermediate(String name, long timeInNS); + void finishTiming(boolean realTime); + } + + /** + * Set up to begin performance tests. The 'intermediates' is a + * communication channel to send back intermediate performance numbers -- + * if you use it, you will probably want to ensure your test is only + * executed once by returning 1. Otherwise, return 0 to allow the test + * harness to decide the number of iterations. + * + *

If you return a non-zero iteration count, you should call + * {@link Intermediates#startTiming intermediates.startTiming} and + * {@link Intermediates#finishTiming intermediates.endTiming} to report the + * duration of the test whose performance should actually be measured. + * + * @param intermediates Callback for sending intermediate results. + * + * @return int Maximum number of iterations to run, or 0 to let the caller + * decide. + */ + int startPerformance(Intermediates intermediates); + + /** + * This method is used to determine what modes this test case can run in. + * + * @return true if this test case can only be run in performance mode. + */ + boolean isPerformanceOnly(); +} + diff --git a/src/test-runner/android/test/ProviderTestCase.java b/src/test-runner/android/test/ProviderTestCase.java new file mode 100644 index 00000000..4108f34f --- /dev/null +++ b/src/test-runner/android/test/ProviderTestCase.java @@ -0,0 +1,114 @@ +/* + * 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.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.test.mock.MockContext; +import android.test.mock.MockContentResolver; +import android.database.DatabaseUtils; + +/** + * If you would like to test a single content provider with an + * {@link InstrumentationTestCase}, this provides some of the boiler plate in {@link #setUp} and + * {@link #tearDown}. + * + * @deprecated this class extends InstrumentationTestCase but should extend AndroidTestCase. Use + * ProviderTestCase2, which corrects this problem, instead. + */ +@Deprecated +public abstract class ProviderTestCase + extends InstrumentationTestCase { + + Class mProviderClass; + String mProviderAuthority; + + private IsolatedContext mProviderContext; + private MockContentResolver mResolver; + + public ProviderTestCase(Class providerClass, String providerAuthority) { + mProviderClass = providerClass; + mProviderAuthority = providerAuthority; + } + + /** + * The content provider that will be set up for use in each test method. + */ + private T mProvider; + + public T getProvider() { + return mProvider; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mResolver = new MockContentResolver(); + final String filenamePrefix = "test."; + RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( + new MockContext(), // The context that most methods are delegated to + getInstrumentation().getTargetContext(), // The context that file methods are delegated to + filenamePrefix); + mProviderContext = new IsolatedContext(mResolver, targetContextWrapper); + + mProvider = ProviderTestCase2.createProviderForTest( + mProviderContext, mProviderClass, mProviderAuthority); + mResolver.addProvider(mProviderAuthority, getProvider()); + } + + /** + * Tears down the environment for the test fixture. + *

+ * Calls {@link android.content.ContentProvider#shutdown()} on the + * {@link android.content.ContentProvider} represented by mProvider. + */ + @Override + protected void tearDown() throws Exception { + mProvider.shutdown(); + super.tearDown(); + } + + public MockContentResolver getMockContentResolver() { + return mResolver; + } + + public IsolatedContext getMockContext() { + return mProviderContext; + } + + public static ContentResolver newResolverWithContentProviderFromSql( + Context targetContext, Class providerClass, String authority, + String databaseName, int databaseVersion, String sql) + throws IllegalAccessException, InstantiationException { + final String filenamePrefix = "test."; + MockContentResolver resolver = new MockContentResolver(); + RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( + new MockContext(), // The context that most methods are delegated to + targetContext, // The context that file methods are delegated to + filenamePrefix); + Context context = new IsolatedContext( + resolver, targetContextWrapper); + DatabaseUtils.createDbFromSqlStatements(context, databaseName, databaseVersion, sql); + + T provider = ProviderTestCase2.createProviderForTest(context, providerClass, authority); + resolver.addProvider(authority, provider); + + return resolver; + } +} diff --git a/src/test-runner/android/test/ProviderTestCase2.java b/src/test-runner/android/test/ProviderTestCase2.java new file mode 100644 index 00000000..be18b530 --- /dev/null +++ b/src/test-runner/android/test/ProviderTestCase2.java @@ -0,0 +1,238 @@ +/* + * 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.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.content.res.Resources; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContext; +import android.test.mock.MockContentResolver; +import android.database.DatabaseUtils; + +import java.io.File; + +/** + * This test case class provides a framework for testing a single + * {@link ContentProvider} and for testing your app code with an + * isolated content provider. Instead of using the system map of + * providers that is based on the manifests of other applications, the test + * case creates its own internal map. It then uses this map to resolve providers + * given an authority. This allows you to inject test providers and to null out + * providers that you do not want to use. + *

+ * This test case also sets up the following mock objects: + *

+ *
    + *
  • + * An {@link android.test.IsolatedContext} that stubs out Context methods that might + * affect the rest of the running system, while allowing tests to do real file and + * database work. + *
  • + *
  • + * A {@link android.test.mock.MockContentResolver} that provides the functionality of a + * regular content resolver, but uses {@link IsolatedContext}. It stubs out + * {@link ContentResolver#notifyChange(Uri, ContentObserver, boolean)} to + * prevent the test from affecting the running system. + *
  • + *
  • + * An instance of the provider under test, running in an {@link IsolatedContext}. + *
  • + *
+ *

+ * This framework is set up automatically by the base class' {@link #setUp()} method. If you + * override this method, you must call the super method as the first statement in + * your override. + *

+ *

+ * In order for their tests to be run, concrete subclasses must provide their own + * constructor with no arguments. This constructor must call + * {@link #ProviderTestCase2(Class, String)} as its first operation. + *

+ * For more information on content provider testing, please see + * Content Provider Testing. + */ +public abstract class ProviderTestCase2 extends AndroidTestCase { + + Class mProviderClass; + String mProviderAuthority; + + private IsolatedContext mProviderContext; + private MockContentResolver mResolver; + + private class MockContext2 extends MockContext { + + @Override + public Resources getResources() { + return getContext().getResources(); + } + + @Override + public File getDir(String name, int mode) { + // name the directory so the directory will be separated from + // one created through the regular Context + return getContext().getDir("mockcontext2_" + name, mode); + } + + @Override + public Context getApplicationContext() { + return this; + } + } + /** + * Constructor. + * + * @param providerClass The class name of the provider under test + * @param providerAuthority The provider's authority string + */ + public ProviderTestCase2(Class providerClass, String providerAuthority) { + mProviderClass = providerClass; + mProviderAuthority = providerAuthority; + } + + private T mProvider; + + /** + * Returns the content provider created by this class in the {@link #setUp()} method. + * @return T An instance of the provider class given as a parameter to the test case class. + */ + public T getProvider() { + return mProvider; + } + + /** + * Sets up the environment for the test fixture. + *

+ * Creates a new + * {@link android.test.mock.MockContentResolver}, a new IsolatedContext + * that isolates the provider's file operations, and a new instance of + * the provider under test within the isolated environment. + *

+ * + * @throws Exception + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + + mResolver = new MockContentResolver(); + final String filenamePrefix = "test."; + RenamingDelegatingContext targetContextWrapper = new + RenamingDelegatingContext( + new MockContext2(), // The context that most methods are + //delegated to + getContext(), // The context that file methods are delegated to + filenamePrefix); + mProviderContext = new IsolatedContext(mResolver, targetContextWrapper); + mProvider = createProviderForTest(mProviderContext, mProviderClass, mProviderAuthority); + mResolver.addProvider(mProviderAuthority, getProvider()); + } + + /** + * Creates and sets up a new instance of the provider. + */ + static T createProviderForTest( + Context context, Class providerClass, String authority) + throws IllegalAccessException, InstantiationException { + T instance = providerClass.newInstance(); + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = authority; + MockContentProvider.attachInfoForTesting(instance, context, providerInfo); + return instance; + } + + /** + * Tears down the environment for the test fixture. + *

+ * Calls {@link android.content.ContentProvider#shutdown()} on the + * {@link android.content.ContentProvider} represented by mProvider. + */ + @Override + protected void tearDown() throws Exception { + mProvider.shutdown(); + super.tearDown(); + } + + /** + * Gets the {@link MockContentResolver} created by this class during initialization. You + * must use the methods of this resolver to access the provider under test. + * + * @return A {@link MockContentResolver} instance. + */ + public MockContentResolver getMockContentResolver() { + return mResolver; + } + + /** + * Gets the {@link IsolatedContext} created by this class during initialization. + * @return The {@link IsolatedContext} instance + */ + public IsolatedContext getMockContext() { + return mProviderContext; + } + + /** + *

+ * Creates a new content provider of the same type as that passed to the test case class, + * with an authority name set to the authority parameter, and using an SQLite database as + * the underlying data source. The SQL statement parameter is used to create the database. + * This method also creates a new {@link MockContentResolver} and adds the provider to it. + *

+ *

+ * Both the new provider and the new resolver are put into an {@link IsolatedContext} + * that uses the targetContext parameter for file operations and a {@link MockContext} + * for everything else. The IsolatedContext prepends the filenamePrefix parameter to + * file, database, and directory names. + *

+ *

+ * This is a convenience method for creating a "mock" provider that can contain test data. + *

+ * + * @param targetContext The context to use as the basis of the IsolatedContext + * @param filenamePrefix A string that is prepended to file, database, and directory names + * @param providerClass The type of the provider being tested + * @param authority The authority string to associated with the test provider + * @param databaseName The name assigned to the database + * @param databaseVersion The version assigned to the database + * @param sql A string containing the SQL statements that are needed to create the desired + * database and its tables. The format is the same as that generated by the + * sqlite3 tool's .dump command. + * @return ContentResolver A new {@link MockContentResolver} linked to the provider + * + * @throws IllegalAccessException + * @throws InstantiationException + */ + public static ContentResolver newResolverWithContentProviderFromSql( + Context targetContext, String filenamePrefix, Class providerClass, String authority, + String databaseName, int databaseVersion, String sql) + throws IllegalAccessException, InstantiationException { + MockContentResolver resolver = new MockContentResolver(); + RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( + new MockContext(), // The context that most methods are delegated to + targetContext, // The context that file methods are delegated to + filenamePrefix); + Context context = new IsolatedContext(resolver, targetContextWrapper); + DatabaseUtils.createDbFromSqlStatements(context, databaseName, databaseVersion, sql); + + T provider = createProviderForTest(context, providerClass, authority); + resolver.addProvider(authority, provider); + + return resolver; + } +} diff --git a/src/test-runner/android/test/RenamingDelegatingContext.java b/src/test-runner/android/test/RenamingDelegatingContext.java new file mode 100644 index 00000000..6a9fa331 --- /dev/null +++ b/src/test-runner/android/test/RenamingDelegatingContext.java @@ -0,0 +1,258 @@ +/* + * 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.content.Context; +import android.content.ContextWrapper; +import android.content.ContentProvider; +import android.database.DatabaseErrorHandler; +import android.database.sqlite.SQLiteDatabase; +//import android.test.mock.MockContentProvider; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +/** + * This is a class which delegates to the given context, but performs database + * and file operations with a renamed database/file name (prefixes default + * names with a given prefix). + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class RenamingDelegatingContext extends ContextWrapper { + + private Context mFileContext; + private String mFilePrefix = null; + private File mCacheDir; + private final Object mSync = new Object(); + + private Set mDatabaseNames = new HashSet<>(); + private Set mFileNames = new HashSet<>(); + + public static T providerWithRenamedContext( + Class contentProvider, Context c, String filePrefix) + throws IllegalAccessException, InstantiationException { + return providerWithRenamedContext(contentProvider, c, filePrefix, false); + } + + public static T providerWithRenamedContext( + Class contentProvider, Context c, String filePrefix, + boolean allowAccessToExistingFilesAndDbs) + throws IllegalAccessException, InstantiationException { + Class mProviderClass = contentProvider; + T mProvider = mProviderClass.newInstance(); + RenamingDelegatingContext mContext = new RenamingDelegatingContext(c, filePrefix); + if (allowAccessToExistingFilesAndDbs) { + mContext.makeExistingFilesAndDbsAccessible(); + } + //MockContentProvider.attachInfoForTesting(mProvider, mContext, null); + return mProvider; + } + + /** + * Makes accessible all files and databases whose names match the filePrefix that was passed to + * the constructor. Normally only files and databases that were created through this context are + * accessible. + */ + public void makeExistingFilesAndDbsAccessible() { + /*String[] databaseList = mFileContext.databaseList(); + for (String diskName : databaseList) { + if (shouldDiskNameBeVisible(diskName)) { + mDatabaseNames.add(publicNameFromDiskName(diskName)); + } + } + String[] fileList = mFileContext.fileList(); + for (String diskName : fileList) { + if (shouldDiskNameBeVisible(diskName)) { + mFileNames.add(publicNameFromDiskName(diskName)); + } + }*/ + } + + /** + * Returns if the given diskName starts with the given prefix or not. + * @param diskName name of the database/file. + */ + boolean shouldDiskNameBeVisible(String diskName) { + return diskName.startsWith(mFilePrefix); + } + + /** + * Returns the public name (everything following the prefix) of the given diskName. + * @param diskName name of the database/file. + */ + String publicNameFromDiskName(String diskName) { + if (!shouldDiskNameBeVisible(diskName)) { + throw new IllegalArgumentException("disk file should not be visible: " + diskName); + } + return diskName.substring(mFilePrefix.length(), diskName.length()); + } + + /** + * @param context : the context that will be delegated. + * @param filePrefix : a prefix with which database and file names will be + * prefixed. + */ + public RenamingDelegatingContext(Context context, String filePrefix) { + super(context); + mFileContext = context; + mFilePrefix = filePrefix; + } + + /** + * @param context : the context that will be delegated. + * @param fileContext : the context that file and db methods will be delegated to + * @param filePrefix : a prefix with which database and file names will be + * prefixed. + */ + public RenamingDelegatingContext(Context context, Context fileContext, String filePrefix) { + super(context); + mFileContext = fileContext; + mFilePrefix = filePrefix; + } + + public String getDatabasePrefix() { + return mFilePrefix; + } + + private String renamedFileName(String name) { + return mFilePrefix + name; + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String name, + int mode, SQLiteDatabase.CursorFactory factory) { + final String internalName = renamedFileName(name); + if (!mDatabaseNames.contains(name)) { + mDatabaseNames.add(name); + //mFileContext.deleteDatabase(internalName); + } + return mFileContext.openOrCreateDatabase(internalName, mode, factory); + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String name, + int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) { + final String internalName = renamedFileName(name); + if (!mDatabaseNames.contains(name)) { + mDatabaseNames.add(name); + //mFileContext.deleteDatabase(internalName); + } + return mFileContext.openOrCreateDatabase(internalName, mode, factory, errorHandler); + } + + //@Override + public boolean deleteDatabase(String name) { + /*if (mDatabaseNames.contains(name)) { + mDatabaseNames.remove(name); + return mFileContext.deleteDatabase(renamedFileName(name)); + } else {*/ + return false; + /*}*/ + } + + @Override + public File getDatabasePath(String name) { + return mFileContext.getDatabasePath(renamedFileName(name)); + } + + //@Override + public String[] databaseList() { + /*return mDatabaseNames.toArray(new String[]{});*/ + return null; + } + + @Override + public FileInputStream openFileInput(String name) + throws FileNotFoundException { + final String internalName = renamedFileName(name); + if (mFileNames.contains(name)) { + return mFileContext.openFileInput(internalName); + } else { + throw new FileNotFoundException(internalName); + } + } + + @Override + public FileOutputStream openFileOutput(String name, int mode) + throws FileNotFoundException { + mFileNames.add(name); + return mFileContext.openFileOutput(renamedFileName(name), mode); + } + + @Override + public File getFileStreamPath(String name) { + return mFileContext.getFileStreamPath(renamedFileName(name)); + } + + //@Override + public boolean deleteFile(String name) { + /*if (mFileNames.contains(name)) { + mFileNames.remove(name); + return mFileContext.deleteFile(renamedFileName(name)); + } else {*/ + return false; + /*}*/ + } + + @Override + public String[] fileList() { + return mFileNames.toArray(new String[]{}); + } + + /** + * In order to support calls to getCacheDir(), we create a temp cache dir (inside the real + * one) and return it instead. This code is basically getCacheDir(), except it uses the real + * cache dir as the parent directory and creates a test cache dir inside that. + */ + @Override + public File getCacheDir() { + synchronized (mSync) { + if (mCacheDir == null) { + mCacheDir = new File(mFileContext.getCacheDir(), renamedFileName("cache")); + } + if (!mCacheDir.exists()) { + if(!mCacheDir.mkdirs()) { + Log.w("RenamingDelegatingContext", "Unable to create cache directory"); + return null; + } + try { + // Give the directory all possible permissions. + Files.setPosixFilePermissions(mCacheDir.toPath(), + EnumSet.allOf(PosixFilePermission.class)); + } catch (IOException e) { + Log.e("RenamingDelegatingContext", + "Could not set permissions of test cacheDir", e); + } + } + } + return mCacheDir; + } +} diff --git a/src/test-runner/android/test/RepetitiveTest.java b/src/test-runner/android/test/RepetitiveTest.java new file mode 100644 index 00000000..13e89d2d --- /dev/null +++ b/src/test-runner/android/test/RepetitiveTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be used on an {@link android.test.InstrumentationTestCase}'s test methods. + * When the annotation is present, the test method is executed the number of times specified by + * numIterations and defaults to 1. + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RepetitiveTest { + /** + * Indicates the number of times a test case should be run. + * + * @return The total number of iterations, the default is 1. + */ + int numIterations() default 1; +} diff --git a/src/test-runner/android/test/ServiceTestCase.java b/src/test-runner/android/test/ServiceTestCase.java new file mode 100644 index 00000000..cd54955f --- /dev/null +++ b/src/test-runner/android/test/ServiceTestCase.java @@ -0,0 +1,338 @@ +/* + * 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.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.test.mock.MockApplication; + +import android.test.mock.MockService; +import java.util.Random; + +/** + * This test case provides a framework in which you can test Service classes in + * a controlled environment. It provides basic support for the lifecycle of a + * Service, and hooks with which you can inject various dependencies and control + * the environment in which your Service is tested. + * + *
+ *

Developer Guides

+ *

For more information about application testing, read the + * Testing developer guide.

+ *
+ * + *

Lifecycle Support. + * A Service is accessed with a specific sequence of + * calls, as described in the + * Services + * document. In order to support the lifecycle of a Service, + * ServiceTestCase enforces this protocol: + * + *

    + *
  • + * The {@link #setUp()} method is called before each test method. The base implementation + * gets the system context. If you override setUp(), you must call + * super.setUp() as the first statement in your override. + *
  • + *
  • + * The test case waits to call {@link android.app.Service#onCreate()} until one of your + * test methods calls {@link #startService} or {@link #bindService}. This gives you an + * opportunity to set up or adjust any additional framework or test logic before you test + * the running service. + *
  • + *
  • + * When one of your test methods calls {@link #startService ServiceTestCase.startService()} + * or {@link #bindService ServiceTestCase.bindService()}, the test case calls + * {@link android.app.Service#onCreate() Service.onCreate()} and then calls either + * {@link android.app.Service#startService(Intent) Service.startService(Intent)} or + * {@link android.app.Service#bindService(Intent, ServiceConnection, int) + * Service.bindService(Intent, ServiceConnection, int)}, as appropriate. It also stores + * values needed to track and support the lifecycle. + *
  • + *
  • + * After each test method finishes, the test case calls the {@link #tearDown} method. This + * method stops and destroys the service with the appropriate calls, depending on how the + * service was started. If you override tearDown(), your must call the + * super.tearDown() as the last statement in your override. + *
  • + *
+ * + *

+ * Dependency Injection. + * A service has two inherent dependencies, its {@link android.content.Context Context} and its + * associated {@link android.app.Application Application}. The ServiceTestCase framework + * allows you to inject modified, mock, or isolated replacements for these dependencies, and + * thus perform unit tests with controlled dependencies in an isolated environment. + *

+ *

+ * By default, the test case is injected with a full system context and a generic + * {@link android.test.mock.MockApplication MockApplication} object. You can inject + * alternatives to either of these by invoking + * {@link AndroidTestCase#setContext(Context) setContext()} or + * {@link #setApplication setApplication()}. You must do this before calling + * startService() or bindService(). The test framework provides a + * number of alternatives for Context, including + * {@link android.test.mock.MockContext MockContext}, + * {@link android.test.RenamingDelegatingContext RenamingDelegatingContext}, + * {@link android.content.ContextWrapper ContextWrapper}, and + * {@link android.test.IsolatedContext}. + * + * @deprecated Use + * + * ServiceTestRule instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public abstract class ServiceTestCase extends AndroidTestCase { + + Class mServiceClass; + + private Context mSystemContext; + private Application mApplication; + + /** + * Constructor + * @param serviceClass The type of the service under test. + */ + public ServiceTestCase(Class serviceClass) { + mServiceClass = serviceClass; + } + + private T mService; + private boolean mServiceAttached = false; + private boolean mServiceCreated = false; + private boolean mServiceStarted = false; + private boolean mServiceBound = false; + private Intent mServiceIntent = null; + private int mServiceId; + + /** + * @return An instance of the service under test. This instance is created automatically when + * a test calls {@link #startService} or {@link #bindService}. + */ + public T getService() { + return mService; + } + + /** + * Gets the current system context and stores it. + * + * Extend this method to do your own test initialization. If you do so, you + * must call super.setUp() as the first statement in your override. The method is + * called before each test method is executed. + */ + @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(); + + } + + /** + * Creates the service under test and attaches all injected dependencies + * (Context, Application) to it. This is called automatically by {@link #startService} or + * by {@link #bindService}. + * If you need to call {@link AndroidTestCase#setContext(Context) setContext()} or + * {@link #setApplication setApplication()}, do so before calling this method. + */ + protected void setupService() { + mService = null; + try { + mService = mServiceClass.newInstance(); + } catch (Exception e) { + assertNotNull(mService); + } + if (getApplication() == null) { + setApplication(new MockApplication()); + } + MockService.attachForTesting( + mService, getContext(), mServiceClass.getName(), getApplication()); + + assertNotNull(mService); + + mServiceId = new Random().nextInt(); + mServiceAttached = true; + } + + /** + * Starts the service under test, in the same way as if it were started by + * {@link android.content.Context#startService(Intent) Context.startService(Intent)} with + * an {@link android.content.Intent} that identifies a service. + * If you use this method to start the service, it is automatically stopped by + * {@link #tearDown}. + * + * @param intent An Intent that identifies a service, of the same form as the Intent passed to + * {@link android.content.Context#startService(Intent) Context.startService(Intent)}. + */ + protected void startService(Intent intent) { + if (!mServiceAttached) { + setupService(); + } + assertNotNull(mService); + + if (!mServiceCreated) { + mService.onCreate(); + mServiceCreated = true; + } + mService.onStartCommand(intent, 0, mServiceId); + + mServiceStarted = true; + } + + /** + *

+ * Starts the service under test, in the same way as if it were started by + * {@link android.content.Context#bindService(Intent, ServiceConnection, int) + * Context.bindService(Intent, ServiceConnection, flags)} with an + * {@link android.content.Intent} that identifies a service. + *

+ *

+ * Notice that the parameters are different. You do not provide a + * {@link android.content.ServiceConnection} object or the flags parameter. Instead, + * you only provide the Intent. The method returns an object whose type is a + * subclass of {@link android.os.IBinder}, or null if the method fails. An IBinder + * object refers to a communication channel between the application and + * the service. The flag is assumed to be {@link android.content.Context#BIND_AUTO_CREATE}. + *

+ *

+ * See Designing a Remote Interface + * Using AIDL for more information about the communication channel object returned + * by this method. + *

+ * Note: To be able to use bindService in a test, the service must implement getService() + * method. An example of this is in the ApiDemos sample application, in the + * LocalService demo. + * + * @param intent An Intent object of the form expected by + * {@link android.content.Context#bindService}. + * + * @return An object whose type is a subclass of IBinder, for making further calls into + * the service. + */ + protected IBinder bindService(Intent intent) { + if (!mServiceAttached) { + setupService(); + } + assertNotNull(mService); + + if (!mServiceCreated) { + mService.onCreate(); + mServiceCreated = true; + } + // no extras are expected by unbind + mServiceIntent = intent.cloneFilter(); + IBinder result = mService.onBind(intent); + + mServiceBound = true; + return result; + } + + /** + * Makes the necessary calls to stop (or unbind) the service under test, and + * calls onDestroy(). Ordinarily this is called automatically (by {@link #tearDown}, but + * you can call it directly from your test in order to check for proper shutdown behavior. + */ + protected void shutdownService() { + if (mServiceStarted) { + mService.stopSelf(); + mServiceStarted = false; + } else if (mServiceBound) { + mService.onUnbind(mServiceIntent); + mServiceBound = false; + } + if (mServiceCreated) { + mService.onDestroy(); + mServiceCreated = false; + } + } + + /** + *

+ * Shuts down the service under test. Ensures all resources are cleaned up and + * garbage collected before moving on to the next test. This method is called after each + * test method. + *

+ *

+ * Subclasses that override this method must call super.tearDown() as their + * last statement. + *

+ * + * @throws Exception + */ + @Override + protected void tearDown() throws Exception { + shutdownService(); + mService = 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(ServiceTestCase.class); + + super.tearDown(); + } + + /** + * Sets the application that is used during the test. If you do not call this method, + * a new {@link android.test.mock.MockApplication MockApplication} object is used. + * + * @param application The Application object that is used by the service under test. + * + * @see #getApplication() + */ + public void setApplication(Application application) { + mApplication = application; + } + + /** + * Returns the Application object in use by the service under test. + * + * @return The application object. + * + * @see #setApplication + */ + public Application getApplication() { + return mApplication; + } + + /** + * Returns the real system context that is saved by {@link #setUp()}. Use it to create + * mock or other types of context objects for the service under test. + * + * @return A normal system context. + */ + public Context getSystemContext() { + return mSystemContext; + } + + /** + * Tests that {@link #setupService()} runs correctly and issues an + * {@link junit.framework.Assert#assertNotNull(String, Object)} if it does. + * You can override this test method if you wish. + * + * @throws Exception + */ + public void testServiceTestCaseSetUpProperly() throws Exception { + setupService(); + assertNotNull("service should be launched successfully", mService); + } +} diff --git a/src/test-runner/android/test/SimpleCache.java b/src/test-runner/android/test/SimpleCache.java new file mode 100644 index 00000000..46143e48 --- /dev/null +++ b/src/test-runner/android/test/SimpleCache.java @@ -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; + +import java.util.HashMap; +import java.util.Map; + +@Deprecated +abstract class SimpleCache { + private Map map = new HashMap(); + + protected abstract V load(K key); + + final V get(K key) { + if (map.containsKey(key)) { + return map.get(key); + } + V value = load(key); + map.put(key, value); + return value; + } +} diff --git a/src/test-runner/android/test/SingleLaunchActivityTestCase.java b/src/test-runner/android/test/SingleLaunchActivityTestCase.java new file mode 100644 index 00000000..af1448e5 --- /dev/null +++ b/src/test-runner/android/test/SingleLaunchActivityTestCase.java @@ -0,0 +1,91 @@ +/* + * 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; + +/** + * If you would like to test a single activity with an + * {@link android.test.InstrumentationTestCase}, this provides some of the boiler plate to + * launch and finish the activity in {@link #setUp} and {@link #tearDown}. + * + * This launches the activity only once for the entire class instead of doing it + * in every setup / teardown call. + * + * @deprecated Use + * + * ActivityTestRule instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public abstract class SingleLaunchActivityTestCase + extends InstrumentationTestCase { + + String mPackage; + Class mActivityClass; + private static int sTestCaseCounter = 0; + private static boolean sActivityLaunchedFlag = false; + + /** + * NOTE: The parameter pkg 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 activityClass The activity to test. + */ + public SingleLaunchActivityTestCase(String pkg, Class activityClass) { + mPackage = pkg; + mActivityClass = activityClass; + sTestCaseCounter ++; + } + + /** + * The activity that will be set up for use in each test method. + */ + private static Activity sActivity; + + public T getActivity() { + return (T) sActivity; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + // If it is the first test case, launch the activity. + if (!sActivityLaunchedFlag) { + // by default, not in touch mode + getInstrumentation().setInTouchMode(false); + sActivity = launchActivity(mPackage, mActivityClass, null); + sActivityLaunchedFlag = true; + } + } + + @Override + protected void tearDown() throws Exception { + // If it is the last test case, call finish on the activity. + sTestCaseCounter --; + if (sTestCaseCounter == 0) { + sActivity.finish(); + } + super.tearDown(); + } + + public void testActivityTestCaseSetUpProperly() throws Exception { + assertNotNull("activity should be launched successfully", sActivity); + } +} diff --git a/src/test-runner/android/test/SyncBaseInstrumentation.java b/src/test-runner/android/test/SyncBaseInstrumentation.java new file mode 100644 index 00000000..de36b4ff --- /dev/null +++ b/src/test-runner/android/test/SyncBaseInstrumentation.java @@ -0,0 +1,85 @@ +/* + * 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.content.ContentResolver; +import android.content.Context; +import android.os.Bundle; +import android.os.SystemClock; +import android.net.Uri; +import android.accounts.Account; + +/** + * If you would like to test sync a single provider with an + * {@link InstrumentationTestCase}, this provides some of the boiler plate in {@link #setUp} and + * {@link #tearDown}. + * + * @deprecated Use + * + * InstrumentationRegistry instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class SyncBaseInstrumentation extends InstrumentationTestCase { + private Context mTargetContext; + ContentResolver mContentResolver; + private static final int MAX_TIME_FOR_SYNC_IN_MINS = 20; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mTargetContext = getInstrumentation().getTargetContext(); + mContentResolver = mTargetContext.getContentResolver(); + } + + /** + * Syncs the specified provider. + * @throws Exception + */ + protected void syncProvider(Uri uri, String accountName, String authority) throws Exception { + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); + Account account = new Account(accountName, "com.google"); + + ContentResolver.requestSync(account, authority, extras); + long startTimeInMillis = SystemClock.elapsedRealtime(); + long endTimeInMillis = startTimeInMillis + MAX_TIME_FOR_SYNC_IN_MINS * 60000; + + int counter = 0; + // Making sure race condition does not occur when en entry have been removed from pending + // and active tables and loaded in memory (therefore sync might be still in progress) + while (counter < 2) { + // Sleep for 1 second. + Thread.sleep(1000); + // Finish test if time to sync has exceeded max time. + if (SystemClock.elapsedRealtime() > endTimeInMillis) { + break; + } + + if (ContentResolver.isSyncActive(account, authority)) { + counter = 0; + continue; + } + counter++; + } + } + + protected void cancelSyncsandDisableAutoSync() { + ContentResolver.setMasterSyncAutomatically(false); + ContentResolver.cancelSync(null /* all accounts */, null /* all authorities */); + } +} diff --git a/src/test-runner/android/test/TestCaseUtil.java b/src/test-runner/android/test/TestCaseUtil.java new file mode 100644 index 00000000..15629099 --- /dev/null +++ b/src/test-runner/android/test/TestCaseUtil.java @@ -0,0 +1,131 @@ +/* + * 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 java.util.ArrayList; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import junit.runner.BaseTestRunner; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @hide - This is part of a framework that is under development and should not be used for + * active development. + */ +@Deprecated +public class TestCaseUtil { + + private TestCaseUtil() { + } + + public static List getTests(Test test, boolean flatten) { + return getTests(test, flatten, new HashSet>()); + } + + private static List getTests(Test test, boolean flatten, + Set> seen) { + List testCases = new ArrayList<>(); + if (test != null) { + + Test workingTest = null; + /* + * If we want to run a single TestCase method only, we must not + * invoke the suite() method, because we will run all test methods + * of the class then. + */ + if (test instanceof TestCase && + ((TestCase)test).getName() == null) { + workingTest = invokeSuiteMethodIfPossible(test.getClass(), + seen); + } + if (workingTest == null) { + workingTest = test; + } + + if (workingTest instanceof TestSuite) { + TestSuite testSuite = (TestSuite) workingTest; + Enumeration enumeration = testSuite.tests(); + while (enumeration.hasMoreElements()) { + Test childTest = (Test) enumeration.nextElement(); + if (flatten) { + testCases.addAll(getTests(childTest, flatten, seen)); + } else { + testCases.add(childTest); + } + } + } else { + testCases.add(workingTest); + } + } + return testCases; + } + + static Test invokeSuiteMethodIfPossible(Class testClass, + Set> seen) { + try { + Method suiteMethod = testClass.getMethod( + BaseTestRunner.SUITE_METHODNAME, new Class[0]); + /* + * Additional check necessary: If a TestCase contains a suite() + * method that returns a TestSuite including the TestCase itself, + * we need to stop the recursion. We use a set of classes to + * remember which classes' suite() methods were already invoked. + */ + if (Modifier.isStatic(suiteMethod.getModifiers()) + && !seen.contains(testClass)) { + seen.add(testClass); + try { + return (Test) suiteMethod.invoke(null, (Object[]) null); + } catch (InvocationTargetException e) { + // do nothing + } catch (IllegalAccessException e) { + // do nothing + } + } + } catch (NoSuchMethodException e) { + // do nothing + } + return null; + } + + static String getTestName(Test test) { + if (test instanceof TestCase) { + TestCase testCase = (TestCase) test; + return testCase.getName(); + } else if (test instanceof TestSuite) { + TestSuite testSuite = (TestSuite) test; + String name = testSuite.getName(); + if (name != null) { + int index = name.lastIndexOf("."); + if (index > -1) { + return name.substring(index + 1); + } else { + return name; + } + } + } + return ""; + } +} diff --git a/src/test-runner/android/test/TestPrinter.java b/src/test-runner/android/test/TestPrinter.java new file mode 100644 index 00000000..01d392da --- /dev/null +++ b/src/test-runner/android/test/TestPrinter.java @@ -0,0 +1,97 @@ +/* + * 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.util.Log; +import junit.framework.Test; +import junit.framework.TestListener; + +import java.util.HashSet; +import java.util.Set; + +/** + * Prints the test progress to stdout. Android includes a default + * implementation and calls these methods to print out test progress; you + * probably will not need to create or extend this class or call its methods manually. + * See the full {@link android.test} package description for information about + * getting test results. + * + * {@hide} Not needed for 1.0 SDK. + */ +@Deprecated +class TestPrinter implements TestListener { + + private String mTag; + private boolean mOnlyFailures; + private Set mFailedTests = new HashSet(); + + + TestPrinter(String tag, boolean onlyFailures) { + mTag = tag; + mOnlyFailures = onlyFailures; + } + + private void started(String className) { + if (!mOnlyFailures) { + Log.i(mTag, "started: " + className); + } + } + + private void finished(String className) { + if (!mOnlyFailures) { + Log.i(mTag, "finished: " + className); + } + } + + private void passed(String className) { + if (!mOnlyFailures) { + Log.i(mTag, "passed: " + className); + } + } + + private void failed(String className, Throwable exception) { + Log.i(mTag, "failed: " + className); + Log.i(mTag, "----- begin exception -----"); + Log.i(mTag, "", exception); + Log.i(mTag, "----- end exception -----"); + } + + private void failed(Test test, Throwable t) { + mFailedTests.add(test.toString()); + failed(test.toString(), t); + } + + public void addError(Test test, Throwable t) { + failed(test, t); + } + + public void addFailure(Test test, junit.framework.AssertionFailedError t) { + failed(test, t); + } + + public void endTest(Test test) { + finished(test.toString()); + if (!mFailedTests.contains(test.toString())) { + passed(test.toString()); + } + mFailedTests.remove(test.toString()); + } + + public void startTest(Test test) { + started(test.toString()); + } +} diff --git a/src/test-runner/android/test/TestSuiteProvider.java b/src/test-runner/android/test/TestSuiteProvider.java new file mode 100644 index 00000000..12cfcb76 --- /dev/null +++ b/src/test-runner/android/test/TestSuiteProvider.java @@ -0,0 +1,33 @@ +/* + * 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 junit.framework.TestSuite; + +/** + * Implementors will know how to get a test suite. + * + * @deprecated Use + * + * AndroidJUnitRunner instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public interface TestSuiteProvider { + + TestSuite getTestSuite(); +} diff --git a/src/test-runner/android/test/TouchUtils.java b/src/test-runner/android/test/TouchUtils.java new file mode 100644 index 00000000..1122cd8f --- /dev/null +++ b/src/test-runner/android/test/TouchUtils.java @@ -0,0 +1,851 @@ +/* + * 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 static android.view.WindowInsets.Type.displayCutout; +import static android.view.WindowInsets.Type.navigationBars; + +import android.app.Activity; +import android.app.Instrumentation; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.SystemClock; +import android.util.Size; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +/** + * Reusable methods for generating touch events. These methods can be used with + * InstrumentationTestCase or ActivityInstrumentationTestCase2 to simulate user interaction with + * the application through a touch screen. + * + * @deprecated Use + * Espresso UI testing + * framework instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class TouchUtils { + + /** + * Simulate touching in the center of the screen and dragging one quarter of the way down + * @param test The test case that is being run + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void dragQuarterScreenDown(ActivityInstrumentationTestCase test) { + dragQuarterScreenDown(test, test.getActivity()); + } + + /** + * Simulate touching in the center of the screen and dragging one quarter of the way down + * @param test The test case that is being run + * @param activity The activity that is in the foreground of the test case + */ + public static void dragQuarterScreenDown(InstrumentationTestCase test, Activity activity) { + WindowManager wm = activity.getWindowManager(); + final Size size = getSizeExcludingNavigationBarAndCutout(wm.getCurrentWindowMetrics()); + + final float x = size.getWidth() / 2.0f; + final float fromY = size.getHeight() * 0.5f; + final float toY = size.getHeight() * 0.75f; + + drag(test, x, x, fromY, toY, 4); + } + + /** + * Simulate touching in the center of the screen and dragging one quarter of the way up + * @param test The test case that is being run + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void dragQuarterScreenUp(ActivityInstrumentationTestCase test) { + dragQuarterScreenUp(test, test.getActivity()); + } + + /** + * Simulate touching in the center of the screen and dragging one quarter of the way up + * @param test The test case that is being run + * @param activity The activity that is in the foreground of the test case + */ + public static void dragQuarterScreenUp(InstrumentationTestCase test, Activity activity) { + WindowManager wm = activity.getWindowManager(); + final Size size = getSizeExcludingNavigationBarAndCutout(wm.getCurrentWindowMetrics()); + + final float x = size.getWidth() / 2.0f; + final float fromY = size.getHeight() * 0.5f; + final float toY = size.getHeight() * 0.25f; + + drag(test, x, x, fromY, toY, 4); + } + + private static Size getSizeExcludingNavigationBarAndCutout(WindowMetrics windowMetrics) { + WindowInsets windowInsets = windowMetrics.getWindowInsets(); + final Insets insetsWithCutout = windowInsets + .getInsetsIgnoringVisibility(navigationBars() | displayCutout()); + final int insetsWidth = insetsWithCutout.left + insetsWithCutout.right; + final int insetsHeight = insetsWithCutout.top + insetsWithCutout.bottom; + + Rect bounds = windowMetrics.getBounds(); + return new Size(bounds.width() - insetsWidth, bounds.height() - insetsHeight); + } + + /** + * Scroll a ViewGroup to the bottom by repeatedly calling + * {@link #dragQuarterScreenUp(InstrumentationTestCase, Activity)} + * + * @param test The test case that is being run + * @param v The ViewGroup that should be dragged + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void scrollToBottom(ActivityInstrumentationTestCase test, ViewGroup v) { + scrollToBottom(test, test.getActivity(), v); + } + + /** + * Scroll a ViewGroup to the bottom by repeatedly calling + * {@link #dragQuarterScreenUp(InstrumentationTestCase, Activity)} + * + * @param test The test case that is being run + * @param activity The activity that is in the foreground of the test case + * @param v The ViewGroup that should be dragged + */ + public static void scrollToBottom(InstrumentationTestCase test, Activity activity, + ViewGroup v) { + ViewStateSnapshot prev; + ViewStateSnapshot next = new ViewStateSnapshot(v); + do { + prev = next; + TouchUtils.dragQuarterScreenUp(test, activity); + next = new ViewStateSnapshot(v); + } while (!prev.equals(next)); + } + + /** + * Scroll a ViewGroup to the top by repeatedly calling + * {@link #dragQuarterScreenDown(InstrumentationTestCase, Activity)} + * + * @param test The test case that is being run + * @param v The ViewGroup that should be dragged + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void scrollToTop(ActivityInstrumentationTestCase test, ViewGroup v) { + scrollToTop(test, test.getActivity(), v); + } + + /** + * Scroll a ViewGroup to the top by repeatedly calling + * {@link #dragQuarterScreenDown(InstrumentationTestCase, Activity)} + * + * @param test The test case that is being run + * @param activity The activity that is in the foreground of the test case + * @param v The ViewGroup that should be dragged + */ + public static void scrollToTop(InstrumentationTestCase test, Activity activity, ViewGroup v) { + ViewStateSnapshot prev; + ViewStateSnapshot next = new ViewStateSnapshot(v); + do { + prev = next; + TouchUtils.dragQuarterScreenDown(test, activity); + next = new ViewStateSnapshot(v); + } while (!prev.equals(next)); + } + + /** + * Simulate touching the center of a view and dragging to the bottom of the screen. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void dragViewToBottom(ActivityInstrumentationTestCase test, View v) { + dragViewToBottom(test, test.getActivity(), v, 4); + } + + /** + * Simulate touching the center of a view and dragging to the bottom of the screen. + * + * @param test The test case that is being run + * @param activity The activity that is in the foreground of the test case + * @param v The view that should be dragged + */ + public static void dragViewToBottom(InstrumentationTestCase test, Activity activity, View v) { + dragViewToBottom(test, activity, v, 4); + } + + /** + * Simulate touching the center of a view and dragging to the bottom of the screen. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param stepCount How many move steps to include in the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void dragViewToBottom(ActivityInstrumentationTestCase test, View v, + int stepCount) { + dragViewToBottom(test, test.getActivity(), v, stepCount); + } + + /** + * Simulate touching the center of a view and dragging to the bottom of the screen. + * + * @param test The test case that is being run + * @param activity The activity that is in the foreground of the test case + * @param v The view that should be dragged + * @param stepCount How many move steps to include in the drag + */ + public static void dragViewToBottom(InstrumentationTestCase test, Activity activity, View v, + int stepCount) { + WindowManager wm = activity.getWindowManager(); + final int screenHeight = getSizeExcludingNavigationBarAndCutout( + wm.getCurrentWindowMetrics()).getHeight(); + + int[] xy = new int[2]; + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + final float x = xy[0] + (viewWidth / 2.0f); + float fromY = xy[1] + (viewHeight / 2.0f); + float toY = screenHeight - 1; + + drag(test, x, x, fromY, toY, stepCount); + } + + /** + * Simulate touching the center of a view and releasing quickly (before the tap timeout). + * + * @param test The test case that is being run + * @param v The view that should be clicked + */ + public static void tapView(InstrumentationTestCase test, View v) { + int[] xy = new int[2]; + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + final float x = xy[0] + (viewWidth / 2.0f); + float y = xy[1] + (viewHeight / 2.0f); + + Instrumentation inst = test.getInstrumentation(); + + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + eventTime = SystemClock.uptimeMillis(); + final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, + x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + } + + /** + * Simulate touching the center of a view and cancelling (so no onClick should + * fire, etc). + * + * @param test The test case that is being run + * @param v The view that should be clicked + */ + public static void touchAndCancelView(InstrumentationTestCase test, View v) { + int[] xy = new int[2]; + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + final float x = xy[0] + (viewWidth / 2.0f); + float y = xy[1] + (viewHeight / 2.0f); + + Instrumentation inst = test.getInstrumentation(); + + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + eventTime = SystemClock.uptimeMillis(); + final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_CANCEL, + x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + } + + /** + * Simulate touching the center of a view and releasing. + * + * @param test The test case that is being run + * @param v The view that should be clicked + */ + public static void clickView(InstrumentationTestCase test, View v) { + int[] xy = new int[2]; + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + final float x = xy[0] + (viewWidth / 2.0f); + float y = xy[1] + (viewHeight / 2.0f); + + Instrumentation inst = test.getInstrumentation(); + + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + + eventTime = SystemClock.uptimeMillis(); + final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, + x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Simulate touching the center of a view, holding until it is a long press, and then releasing. + * + * @param test The test case that is being run + * @param v The view that should be clicked + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void longClickView(ActivityInstrumentationTestCase test, View v) { + longClickView((InstrumentationTestCase) test, v); + } + + /** + * Simulate touching the center of a view, holding until it is a long press, and then releasing. + * + * @param test The test case that is being run + * @param v The view that should be clicked + */ + public static void longClickView(InstrumentationTestCase test, View v) { + int[] xy = new int[2]; + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + final float x = xy[0] + (viewWidth / 2.0f); + float y = xy[1] + (viewHeight / 2.0f); + + Instrumentation inst = test.getInstrumentation(); + + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + eventTime = SystemClock.uptimeMillis(); + final int touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, + x + touchSlop / 2, y + touchSlop / 2, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + + try { + Thread.sleep((long)(ViewConfiguration.getLongPressTimeout() * 1.5f)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + } + + /** + * Simulate touching the center of a view and dragging to the top of the screen. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void dragViewToTop(ActivityInstrumentationTestCase test, View v) { + dragViewToTop((InstrumentationTestCase) test, v, 4); + } + + /** + * Simulate touching the center of a view and dragging to the top of the screen. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param stepCount How many move steps to include in the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void dragViewToTop(ActivityInstrumentationTestCase test, View v, int stepCount) { + dragViewToTop((InstrumentationTestCase) test, v, stepCount); + } + + /** + * Simulate touching the center of a view and dragging to the top of the screen. + * + * @param test The test case that is being run + * @param v The view that should be dragged + */ + public static void dragViewToTop(InstrumentationTestCase test, View v) { + dragViewToTop(test, v, 4); + } + + /** + * Simulate touching the center of a view and dragging to the top of the screen. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param stepCount How many move steps to include in the drag + */ + public static void dragViewToTop(InstrumentationTestCase test, View v, int stepCount) { + int[] xy = new int[2]; + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + final float x = xy[0] + (viewWidth / 2.0f); + float fromY = xy[1] + (viewHeight / 2.0f); + float toY = 0; + + drag(test, x, x, fromY, toY, stepCount); + } + + /** + * Get the location of a view. Use the gravity param to specify which part of the view to + * return. + * + * @param v View to find + * @param gravity A combination of (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, + * RIGHT) + * @param xy Result + */ + private static void getStartLocation(View v, int gravity, int[] xy) { + v.getLocationOnScreen(xy); + + final int viewWidth = v.getWidth(); + final int viewHeight = v.getHeight(); + + switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + break; + case Gravity.CENTER_VERTICAL: + xy[1] += viewHeight / 2; + break; + case Gravity.BOTTOM: + xy[1] += viewHeight - 1; + break; + default: + // Same as top -- do nothing + } + + switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + break; + case Gravity.CENTER_HORIZONTAL: + xy[0] += viewWidth / 2; + break; + case Gravity.RIGHT: + xy[0] += viewWidth - 1; + break; + default: + // Same as left -- do nothing + } + } + + /** + * Simulate touching a view and dragging it by the specified amount. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param deltaX Amount to drag horizontally in pixels + * @param deltaY Amount to drag vertically in pixels + * + * @return distance in pixels covered by the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static int dragViewBy(ActivityInstrumentationTestCase test, View v, int gravity, + int deltaX, int deltaY) { + return dragViewBy((InstrumentationTestCase) test, v, gravity, deltaX, deltaY); + } + + /** + * Simulate touching a view and dragging it by the specified amount. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param deltaX Amount to drag horizontally in pixels + * @param deltaY Amount to drag vertically in pixels + * + * @return distance in pixels covered by the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static int dragViewBy(InstrumentationTestCase test, View v, int gravity, int deltaX, + int deltaY) { + int[] xy = new int[2]; + + getStartLocation(v, gravity, xy); + + final int fromX = xy[0]; + final int fromY = xy[1]; + + int distance = (int) Math.hypot(deltaX, deltaY); + + drag(test, fromX, fromX + deltaX, fromY, fromY + deltaY, distance); + + return distance; + } + + /** + * Simulate touching a view and dragging it to a specified location. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param toX Final location of the view after dragging + * @param toY Final location of the view after dragging + * + * @return distance in pixels covered by the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static int dragViewTo(ActivityInstrumentationTestCase test, View v, int gravity, int toX, + int toY) { + return dragViewTo((InstrumentationTestCase) test, v, gravity, toX, toY); + } + + /** + * Simulate touching a view and dragging it to a specified location. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param toX Final location of the view after dragging + * @param toY Final location of the view after dragging + * + * @return distance in pixels covered by the drag + */ + public static int dragViewTo(InstrumentationTestCase test, View v, int gravity, int toX, + int toY) { + int[] xy = new int[2]; + + getStartLocation(v, gravity, xy); + + final int fromX = xy[0]; + final int fromY = xy[1]; + + int deltaX = fromX - toX; + int deltaY = fromY - toY; + + int distance = (int)Math.hypot(deltaX, deltaY); + drag(test, fromX, toX, fromY, toY, distance); + + return distance; + } + + /** + * Simulate touching a view and dragging it to a specified location. Only moves horizontally. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param toX Final location of the view after dragging + * + * @return distance in pixels covered by the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static int dragViewToX(ActivityInstrumentationTestCase test, View v, int gravity, + int toX) { + return dragViewToX((InstrumentationTestCase) test, v, gravity, toX); + } + + /** + * Simulate touching a view and dragging it to a specified location. Only moves horizontally. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param toX Final location of the view after dragging + * + * @return distance in pixels covered by the drag + */ + public static int dragViewToX(InstrumentationTestCase test, View v, int gravity, int toX) { + int[] xy = new int[2]; + + getStartLocation(v, gravity, xy); + + final int fromX = xy[0]; + final int fromY = xy[1]; + + int deltaX = fromX - toX; + + drag(test, fromX, toX, fromY, fromY, deltaX); + + return deltaX; + } + + /** + * Simulate touching a view and dragging it to a specified location. Only moves vertically. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param toY Final location of the view after dragging + * + * @return distance in pixels covered by the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static int dragViewToY(ActivityInstrumentationTestCase test, View v, int gravity, + int toY) { + return dragViewToY((InstrumentationTestCase) test, v, gravity, toY); + } + + /** + * Simulate touching a view and dragging it to a specified location. Only moves vertically. + * + * @param test The test case that is being run + * @param v The view that should be dragged + * @param gravity Which part of the view to use for the initial down event. A combination of + * (TOP, CENTER_VERTICAL, BOTTOM) and (LEFT, CENTER_HORIZONTAL, RIGHT) + * @param toY Final location of the view after dragging + * + * @return distance in pixels covered by the drag + */ + public static int dragViewToY(InstrumentationTestCase test, View v, int gravity, int toY) { + int[] xy = new int[2]; + + getStartLocation(v, gravity, xy); + + final int fromX = xy[0]; + final int fromY = xy[1]; + + int deltaY = fromY - toY; + + drag(test, fromX, fromX, fromY, toY, deltaY); + + return deltaY; + } + + + /** + * Simulate touching a specific location and dragging to a new location. + * + * @param test The test case that is being run + * @param fromX X coordinate of the initial touch, in screen coordinates + * @param toX Xcoordinate of the drag destination, in screen coordinates + * @param fromY X coordinate of the initial touch, in screen coordinates + * @param toY Y coordinate of the drag destination, in screen coordinates + * @param stepCount How many move steps to include in the drag + * + * @deprecated {@link android.test.ActivityInstrumentationTestCase} is deprecated in favor of + * {@link android.test.ActivityInstrumentationTestCase2}, which provides more options for + * configuring the Activity under test + */ + @Deprecated + public static void drag(ActivityInstrumentationTestCase test, float fromX, float toX, + float fromY, float toY, int stepCount) { + drag((InstrumentationTestCase) test, fromX, toX, fromY, toY, stepCount); + } + + /** + * Simulate touching a specific location and dragging to a new location. + * + * @param test The test case that is being run + * @param fromX X coordinate of the initial touch, in screen coordinates + * @param toX Xcoordinate of the drag destination, in screen coordinates + * @param fromY X coordinate of the initial touch, in screen coordinates + * @param toY Y coordinate of the drag destination, in screen coordinates + * @param stepCount How many move steps to include in the drag + */ + public static void drag(InstrumentationTestCase test, float fromX, float toX, float fromY, + float toY, int stepCount) { + Instrumentation inst = test.getInstrumentation(); + + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + float y = fromY; + float x = fromX; + + float yStep = (toY - fromY) / stepCount; + float xStep = (toX - fromX) / stepCount; + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + inst.sendPointerSync(event); + for (int i = 0; i < stepCount; ++i) { + y += yStep; + x += xStep; + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); + inst.sendPointerSync(event); + } + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + inst.sendPointerSync(event); + inst.waitForIdleSync(); + } + + private static class ViewStateSnapshot { + final View mFirst; + final View mLast; + final int mFirstTop; + final int mLastBottom; + final int mChildCount; + private ViewStateSnapshot(ViewGroup viewGroup) { + mChildCount = viewGroup.getChildCount(); + if (mChildCount == 0) { + mFirst = mLast = null; + mFirstTop = mLastBottom = Integer.MIN_VALUE; + } else { + mFirst = viewGroup.getChildAt(0); + mLast = viewGroup.getChildAt(mChildCount - 1); + mFirstTop = mFirst.getTop(); + mLastBottom = mLast.getBottom(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final ViewStateSnapshot that = (ViewStateSnapshot) o; + return mFirstTop == that.mFirstTop && + mLastBottom == that.mLastBottom && + mFirst == that.mFirst && + mLast == that.mLast && + mChildCount == that.mChildCount; + } + + @Override + public int hashCode() { + int result = mFirst != null ? mFirst.hashCode() : 0; + result = 31 * result + (mLast != null ? mLast.hashCode() : 0); + result = 31 * result + mFirstTop; + result = 31 * result + mLastBottom; + result = 31 * result + mChildCount; + return result; + } + } +} diff --git a/src/test-runner/android/test/UiThreadTest.java b/src/test-runner/android/test/UiThreadTest.java new file mode 100644 index 00000000..cd06ab89 --- /dev/null +++ b/src/test-runner/android/test/UiThreadTest.java @@ -0,0 +1,39 @@ +/* + * 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 InstrumentationTestCase}'s test methods. + * When the annotation is present, the test method is executed on the application's + * main thread (or UI thread.) Note that instrumentation methods may not be used + * when this annotation is present. + * + * @deprecated Use + * + * UiThreadTest instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface UiThreadTest { +} diff --git a/src/test-runner/android/test/ViewAsserts.java b/src/test-runner/android/test/ViewAsserts.java new file mode 100644 index 00000000..00ab4437 --- /dev/null +++ b/src/test-runner/android/test/ViewAsserts.java @@ -0,0 +1,381 @@ +/* + * 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 static junit.framework.Assert.*; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Some useful assertions about views. + * + * @deprecated Use + * Espresso + * View Matchers instead. New test should be written using the + * Android Testing Support Library. + * For more information about UI testing, take the + * Espresso UI testing training. + */ +@Deprecated +public class ViewAsserts { + + private ViewAsserts() {} + + /** + * Assert that view is on the screen. + * @param origin The root view of the screen. + * @param view The view. + */ + static public void assertOnScreen(View origin, View view) { + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + + int[] xyRoot = new int[2]; + origin.getLocationOnScreen(xyRoot); + + int y = xy[1] - xyRoot[1]; + + assertTrue("view should have positive y coordinate on screen", + y >= 0); + + assertTrue("view should have y location on screen less than drawing " + + "height of root view", + y <= view.getRootView().getHeight()); + } + + /** + * Assert that view is below the visible screen. + * @param origin The root view of the screen. + * @param view The view + */ + static public void assertOffScreenBelow(View origin, View view) { + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + + int[] xyRoot = new int[2]; + origin.getLocationOnScreen(xyRoot); + + int y = xy[1] - xyRoot[1]; + + assertTrue("view should have y location on screen greater than drawing " + + "height of origen view (" + y + " is not greater than " + + origin.getHeight() + ")", + y > origin.getHeight()); + } + + /** + * Assert that view is above the visible screen. + * @param origin Te root view of the screen. + * @param view The view + */ + static public void assertOffScreenAbove(View origin, View view) { + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + + int[] xyRoot = new int[2]; + origin.getLocationOnScreen(xyRoot); + + int y = xy[1] - xyRoot[1]; + + assertTrue("view should have y location less than that of origin view", + y < 0); + } + + /** + * Assert that a view has a particular x and y position on the visible screen. + * @param origin The root view of the screen. + * @param view The view. + * @param x The expected x coordinate. + * @param y The expected y coordinate. + */ + static public void assertHasScreenCoordinates(View origin, View view, int x, int y) { + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + + int[] xyRoot = new int[2]; + origin.getLocationOnScreen(xyRoot); + + assertEquals("x coordinate", x, xy[0] - xyRoot[0]); + assertEquals("y coordinate", y, xy[1] - xyRoot[1]); + } + + /** + * Assert that two views are aligned on their baseline, that is that their baselines + * are on the same y location. + * + * @param first The first view + * @param second The second view + */ + static public void assertBaselineAligned(View first, View second) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstTop = xy[1] + first.getBaseline(); + + second.getLocationOnScreen(xy); + int secondTop = xy[1] + second.getBaseline(); + + assertEquals("views are not baseline aligned", firstTop, secondTop); + } + + /** + * Assert that two views are right aligned, that is that their right edges + * are on the same x location. + * + * @param first The first view + * @param second The second view + */ + static public void assertRightAligned(View first, View second) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstRight = xy[0] + first.getMeasuredWidth(); + + second.getLocationOnScreen(xy); + int secondRight = xy[0] + second.getMeasuredWidth(); + + assertEquals("views are not right aligned", firstRight, secondRight); + } + + /** + * Assert that two views are right aligned, that is that their right edges + * are on the same x location, with respect to the specified margin. + * + * @param first The first view + * @param second The second view + * @param margin The margin between the first view and the second view + */ + static public void assertRightAligned(View first, View second, int margin) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstRight = xy[0] + first.getMeasuredWidth(); + + second.getLocationOnScreen(xy); + int secondRight = xy[0] + second.getMeasuredWidth(); + + assertEquals("views are not right aligned", Math.abs(firstRight - secondRight), margin); + } + + /** + * Assert that two views are left aligned, that is that their left edges + * are on the same x location. + * + * @param first The first view + * @param second The second view + */ + static public void assertLeftAligned(View first, View second) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstLeft = xy[0]; + + second.getLocationOnScreen(xy); + int secondLeft = xy[0]; + + assertEquals("views are not left aligned", firstLeft, secondLeft); + } + + /** + * Assert that two views are left aligned, that is that their left edges + * are on the same x location, with respect to the specified margin. + * + * @param first The first view + * @param second The second view + * @param margin The margin between the first view and the second view + */ + static public void assertLeftAligned(View first, View second, int margin) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstLeft = xy[0]; + + second.getLocationOnScreen(xy); + int secondLeft = xy[0]; + + assertEquals("views are not left aligned", Math.abs(firstLeft - secondLeft), margin); + } + + /** + * Assert that two views are bottom aligned, that is that their bottom edges + * are on the same y location. + * + * @param first The first view + * @param second The second view + */ + static public void assertBottomAligned(View first, View second) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstBottom = xy[1] + first.getMeasuredHeight(); + + second.getLocationOnScreen(xy); + int secondBottom = xy[1] + second.getMeasuredHeight(); + + assertEquals("views are not bottom aligned", firstBottom, secondBottom); + } + + /** + * Assert that two views are bottom aligned, that is that their bottom edges + * are on the same y location, with respect to the specified margin. + * + * @param first The first view + * @param second The second view + * @param margin The margin between the first view and the second view + */ + static public void assertBottomAligned(View first, View second, int margin) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstBottom = xy[1] + first.getMeasuredHeight(); + + second.getLocationOnScreen(xy); + int secondBottom = xy[1] + second.getMeasuredHeight(); + + assertEquals("views are not bottom aligned", Math.abs(firstBottom - secondBottom), margin); + } + + /** + * Assert that two views are top aligned, that is that their top edges + * are on the same y location. + * + * @param first The first view + * @param second The second view + */ + static public void assertTopAligned(View first, View second) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstTop = xy[1]; + + second.getLocationOnScreen(xy); + int secondTop = xy[1]; + + assertEquals("views are not top aligned", firstTop, secondTop); + } + + /** + * Assert that two views are top aligned, that is that their top edges + * are on the same y location, with respect to the specified margin. + * + * @param first The first view + * @param second The second view + * @param margin The margin between the first view and the second view + */ + static public void assertTopAligned(View first, View second, int margin) { + int[] xy = new int[2]; + first.getLocationOnScreen(xy); + int firstTop = xy[1]; + + second.getLocationOnScreen(xy); + int secondTop = xy[1]; + + assertEquals("views are not top aligned", Math.abs(firstTop - secondTop), margin); + } + + /** + * Assert that the test view is horizontally center aligned + * with respect to the reference view. + * + * @param reference The reference view + * @param test The view that should be center aligned with the reference view + */ + static public void assertHorizontalCenterAligned(View reference, View test) { + int[] xy = new int[2]; + reference.getLocationOnScreen(xy); + int referenceLeft = xy[0]; + + test.getLocationOnScreen(xy); + int testLeft = xy[0]; + + int center = (reference.getMeasuredWidth() - test.getMeasuredWidth()) / 2; + int delta = testLeft - referenceLeft; + + assertEquals("views are not horizontally center aligned", center, delta); + } + + /** + * Assert that the test view is vertically center aligned + * with respect to the reference view. + * + * @param reference The reference view + * @param test The view that should be center aligned with the reference view + */ + static public void assertVerticalCenterAligned(View reference, View test) { + int[] xy = new int[2]; + reference.getLocationOnScreen(xy); + int referenceTop = xy[1]; + + test.getLocationOnScreen(xy); + int testTop = xy[1]; + + int center = (reference.getMeasuredHeight() - test.getMeasuredHeight()) / 2; + int delta = testTop - referenceTop; + + assertEquals("views are not vertically center aligned", center, delta); + } + + /** + * Assert the specified group's integrity. The children count should be >= 0 and each + * child should be non-null. + * + * @param parent The group whose integrity to check + */ + static public void assertGroupIntegrity(ViewGroup parent) { + final int count = parent.getChildCount(); + assertTrue("child count should be >= 0", count >= 0); + + for (int i = 0; i < count; i++) { + assertNotNull("group should not contain null children", parent.getChildAt(i)); + assertSame(parent, parent.getChildAt(i).getParent()); + } + } + + /** + * Assert that the specified group contains a specific child once and only once. + * + * @param parent The group + * @param child The child that should belong to group + */ + static public void assertGroupContains(ViewGroup parent, View child) { + final int count = parent.getChildCount(); + assertTrue("Child count should be >= 0", count >= 0); + + boolean found = false; + for (int i = 0; i < count; i++) { + if (parent.getChildAt(i) == child) { + if (!found) { + found = true; + } else { + assertTrue("child " + child + " is duplicated in parent", false); + } + } + } + + assertTrue("group does not contain " + child, found); + } + + /** + * Assert that the specified group does not contain a specific child. + * + * @param parent The group + * @param child The child that should not belong to group + */ + static public void assertGroupNotContains(ViewGroup parent, View child) { + final int count = parent.getChildCount(); + assertTrue("Child count should be >= 0", count >= 0); + + for (int i = 0; i < count; i++) { + if (parent.getChildAt(i) == child) { + assertTrue("child " + child + " is found in parent", false); + } + } + } +} diff --git a/src/test-runner/android/test/suitebuilder/AssignableFrom.java b/src/test-runner/android/test/suitebuilder/AssignableFrom.java new file mode 100644 index 00000000..84db0666 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/AssignableFrom.java @@ -0,0 +1,32 @@ +/* + * 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.suitebuilder; + +import com.android.internal.util.Predicate; + +class AssignableFrom implements Predicate { + + private final Class root; + + AssignableFrom(Class root) { + this.root = root; + } + + public boolean apply(TestMethod testMethod) { + return root.isAssignableFrom(testMethod.getEnclosingClass()); + } +} diff --git a/src/test-runner/android/test/suitebuilder/SmokeTestSuiteBuilder.java b/src/test-runner/android/test/suitebuilder/SmokeTestSuiteBuilder.java new file mode 100644 index 00000000..01e7ec6c --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/SmokeTestSuiteBuilder.java @@ -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.suitebuilder; + +/** + * A suite builder that runs smoke tests. + * + * {@hide} Not needed for 1.0 SDK. + */ +public class SmokeTestSuiteBuilder extends TestSuiteBuilder { + + public SmokeTestSuiteBuilder(Class clazz) { + this(clazz.getName(), clazz.getClassLoader()); + } + + + public SmokeTestSuiteBuilder(String name, ClassLoader classLoader) { + super(name, classLoader); + addRequirements(TestPredicates.SELECT_SMOKE); + } +} diff --git a/src/test-runner/android/test/suitebuilder/TestGrouping.java b/src/test-runner/android/test/suitebuilder/TestGrouping.java new file mode 100644 index 00000000..030bc426 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/TestGrouping.java @@ -0,0 +1,229 @@ +/* + * 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.suitebuilder; + +import android.test.ClassPathPackageInfoSource; +import android.util.Log; +import com.android.internal.util.Predicate; +import junit.framework.TestCase; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Represents a collection of test classes present on the classpath. You can add individual classes + * or entire packages. By default sub-packages are included recursively, but methods are + * provided to allow for arbitrary inclusion or exclusion of sub-packages. Typically a + * {@link TestGrouping} will have only one root package, but this is not a requirement. + * + * {@hide} Not needed for 1.0 SDK. + */ +class TestGrouping { + + private static final String LOG_TAG = "TestGrouping"; + + private final SortedSet> testCaseClasses; + + static final Comparator> SORT_BY_SIMPLE_NAME + = new SortBySimpleName(); + + static final Comparator> SORT_BY_FULLY_QUALIFIED_NAME + = new SortByFullyQualifiedName(); + + private final ClassLoader classLoader; + + TestGrouping(Comparator> comparator, ClassLoader classLoader) { + testCaseClasses = new TreeSet>(comparator); + this.classLoader = classLoader; + } + + /** + * @return A list of all tests in the package, including small, medium, large, + * flaky, and suppressed tests. Includes sub-packages recursively. + */ + public List getTests() { + List testMethods = new ArrayList(); + for (Class testCase : testCaseClasses) { + for (Method testMethod : getTestMethods(testCase)) { + testMethods.add(new TestMethod(testMethod, testCase)); + } + } + return testMethods; + } + + private List getTestMethods(Class testCaseClass) { + List methods = Arrays.asList(testCaseClass.getMethods()); + return select(methods, new TestMethodPredicate()); + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestGrouping other = (TestGrouping) o; + if (!this.testCaseClasses.equals(other.testCaseClasses)) { + return false; + } + return this.testCaseClasses.comparator().equals(other.testCaseClasses.comparator()); + } + + public int hashCode() { + return testCaseClasses.hashCode(); + } + + /** + * Include all tests in the given packages and all their sub-packages, unless otherwise + * specified. Each of the given packages must contain at least one test class, either directly + * or in a sub-package. + * + * @param packageNames Names of packages to add. + */ + void addPackagesRecursive(String... packageNames) { + for (String packageName : packageNames) { + List> addedClasses = testCaseClassesInPackage(packageName); + if (addedClasses.isEmpty()) { + Log.w(LOG_TAG, "Invalid Package: '" + packageName + + "' could not be found or has no tests"); + } + testCaseClasses.addAll(addedClasses); + } + } + + /** + * Exclude all tests in the given packages and all their sub-packages, unless otherwise + * specified. + * + * @param packageNames Names of packages to remove. + */ + void removePackagesRecursive(String... packageNames) { + for (String packageName : packageNames) { + testCaseClasses.removeAll(testCaseClassesInPackage(packageName)); + } + } + + private List> testCaseClassesInPackage(String packageName) { + ClassPathPackageInfoSource source = ClassPathPackageInfoSource.forClassPath(classLoader); + + return selectTestClasses(source.getTopLevelClassesRecursive(packageName)); + } + + @SuppressWarnings("unchecked") + private List> selectTestClasses(Set> allClasses) { + List> testClasses = new ArrayList>(); + for (Class testClass : select(allClasses, + new TestCasePredicate())) { + testClasses.add((Class) testClass); + } + return testClasses; + } + + private List select(Collection items, Predicate predicate) { + ArrayList selectedItems = new ArrayList(); + for (T item : items) { + if (predicate.apply(item)) { + selectedItems.add(item); + } + } + return selectedItems; + } + + /** + * Sort classes by their simple names (i.e. without the package prefix), using + * their packages to sort classes with the same name. + */ + private static class SortBySimpleName + implements Comparator>, Serializable { + + public int compare(Class class1, + Class class2) { + int result = class1.getSimpleName().compareTo(class2.getSimpleName()); + if (result != 0) { + return result; + } + return class1.getName().compareTo(class2.getName()); + } + } + + /** + * Sort classes by their fully qualified names (i.e. with the package + * prefix). + */ + private static class SortByFullyQualifiedName + implements Comparator>, Serializable { + + public int compare(Class class1, + Class class2) { + return class1.getName().compareTo(class2.getName()); + } + } + + private static class TestCasePredicate implements Predicate> { + + public boolean apply(Class aClass) { + int modifiers = ((Class) aClass).getModifiers(); + return TestCase.class.isAssignableFrom((Class) aClass) + && Modifier.isPublic(modifiers) + && !Modifier.isAbstract(modifiers) + && hasValidConstructor((Class) aClass); + } + + @SuppressWarnings("unchecked") + private boolean hasValidConstructor(java.lang.Class aClass) { + // The cast below is not necessary with the Java 5 compiler, but necessary with the Java 6 compiler, + // where the return type of Class.getDeclaredConstructors() was changed + // from Constructor[] to Constructor[] + Constructor[] constructors + = (Constructor[]) aClass.getConstructors(); + for (Constructor constructor : constructors) { + if (Modifier.isPublic(constructor.getModifiers())) { + java.lang.Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0 || + (parameterTypes.length == 1 && parameterTypes[0] == String.class)) { + return true; + } + } + } + Log.i(LOG_TAG, String.format( + "TestCase class %s is missing a public constructor with no parameters " + + "or a single String parameter - skipping", + aClass.getName())); + return false; + } + } + + private static class TestMethodPredicate implements Predicate { + + public boolean apply(Method method) { + return ((method.getParameterTypes().length == 0) && + (method.getName().startsWith("test")) && + (method.getReturnType().getSimpleName().equals("void"))); + } + } +} diff --git a/src/test-runner/android/test/suitebuilder/TestMethod.java b/src/test-runner/android/test/suitebuilder/TestMethod.java new file mode 100644 index 00000000..ae1db5e5 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/TestMethod.java @@ -0,0 +1,150 @@ +/* + * 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.suitebuilder; + +import junit.framework.TestCase; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Represents a test to be run. Can be constructed without instantiating the TestCase or even + * loading the class. + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class TestMethod { + + private final String enclosingClassname; + private final String testMethodName; + private final Class enclosingClass; + + public TestMethod(Method method, Class enclosingClass) { + this(method.getName(), enclosingClass); + } + + public TestMethod(String methodName, Class enclosingClass) { + this.enclosingClass = enclosingClass; + this.enclosingClassname = enclosingClass.getName(); + this.testMethodName = methodName; + } + + public TestMethod(TestCase testCase) { + this(testCase.getName(), testCase.getClass()); + } + + public String getName() { + return testMethodName; + } + + public String getEnclosingClassname() { + return enclosingClassname; + } + + public T getAnnotation(Class annotationClass) { + try { + return getEnclosingClass().getMethod(getName()).getAnnotation(annotationClass); + } catch (NoSuchMethodException e) { + return null; + } + } + + @SuppressWarnings("unchecked") + public Class getEnclosingClass() { + return enclosingClass; + } + + public TestCase createTest() + throws InvocationTargetException, IllegalAccessException, InstantiationException { + return instantiateTest(enclosingClass, testMethodName); + } + + @SuppressWarnings("unchecked") + private TestCase instantiateTest(Class testCaseClass, String testName) + throws InvocationTargetException, IllegalAccessException, InstantiationException { + Constructor[] constructors = testCaseClass.getConstructors(); + + if (constructors.length == 0) { + return instantiateTest(testCaseClass.getSuperclass(), testName); + } else { + for (Constructor constructor : constructors) { + Class[] params = constructor.getParameterTypes(); + if (noargsConstructor(params)) { + TestCase test = ((Constructor) constructor).newInstance(); + // JUnit will run just the one test if you call + // {@link TestCase#setName(String)} + test.setName(testName); + return test; + } else if (singleStringConstructor(params)) { + return ((Constructor) constructor) + .newInstance(testName); + } + } + } + throw new RuntimeException("Unable to locate a constructor for " + + testCaseClass.getName()); + } + + private boolean singleStringConstructor(Class[] params) { + return (params.length == 1) && (params[0].equals(String.class)); + } + + private boolean noargsConstructor(Class[] params) { + return params.length == 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TestMethod that = (TestMethod) o; + + if (enclosingClassname != null + ? !enclosingClassname.equals(that.enclosingClassname) + : that.enclosingClassname != null) { + return false; + } + if (testMethodName != null + ? !testMethodName.equals(that.testMethodName) + : that.testMethodName != null) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result; + result = (enclosingClassname != null ? enclosingClassname.hashCode() : 0); + result = 31 * result + (testMethodName != null ? testMethodName.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return enclosingClassname + "." + testMethodName; + } +} diff --git a/src/test-runner/android/test/suitebuilder/TestPredicates.java b/src/test-runner/android/test/suitebuilder/TestPredicates.java new file mode 100644 index 00000000..faf31fdc --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/TestPredicates.java @@ -0,0 +1,81 @@ +/* + * 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.suitebuilder; + +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.Smoke; +import android.test.suitebuilder.annotation.Suppress; + +import com.android.internal.util.Predicate; + +import java.lang.annotation.Annotation; + +/** + * {@hide} Not needed for 1.0 SDK. + */ +public class TestPredicates { + + static final Predicate REJECT_INSTRUMENTATION = + not(new AssignableFrom(InstrumentationTestCase.class)); + + static final Predicate SELECT_SMOKE = hasAnnotation(Smoke.class); + + static final Predicate REJECT_SUPPRESSED = not(hasAnnotation(Suppress.class)); + + /** + * Return a predicate that checks to see if a {@link TestMethod} has an instance of the supplied + * annotation class, either on the method or on the containing class. + */ + public static Predicate hasAnnotation(Class annotationClass) { + return new HasAnnotation(annotationClass); + } + + private static class HasAnnotation implements Predicate { + + private final Class annotationClass; + + private HasAnnotation(Class annotationClass) { + this.annotationClass = annotationClass; + } + + @Override + public boolean apply(TestMethod testMethod) { + return testMethod.getAnnotation(annotationClass) != null || + testMethod.getEnclosingClass().getAnnotation(annotationClass) != null; + } + } + + /** + * Returns a Predicate that evaluates to true iff the given Predicate + * evaluates to false. + */ + public static Predicate not(Predicate predicate) { + return new NotPredicate(predicate); + } + + private static class NotPredicate implements Predicate { + private final Predicate predicate; + + private NotPredicate(Predicate predicate) { + this.predicate = predicate; + } + + public boolean apply(T t) { + return !predicate.apply(t); + } + } +} diff --git a/src/test-runner/android/test/suitebuilder/TestSuiteBuilder.java b/src/test-runner/android/test/suitebuilder/TestSuiteBuilder.java new file mode 100644 index 00000000..2857696e --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/TestSuiteBuilder.java @@ -0,0 +1,278 @@ +/* + * 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.suitebuilder; + +import android.content.Context; +import android.test.AndroidTestRunner; +import android.test.TestCaseUtil; +import android.util.Log; +import com.android.internal.util.Predicate; +import static android.test.suitebuilder.TestGrouping.SORT_BY_FULLY_QUALIFIED_NAME; +import static android.test.suitebuilder.TestPredicates.REJECT_SUPPRESSED; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Build suites based on a combination of included packages, excluded packages, + * and predicates that must be satisfied. + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +public class TestSuiteBuilder { + + private final TestGrouping testGrouping; + private final Set> predicates = new HashSet>(); + private List testCases; + private TestSuite rootSuite; + private TestSuite suiteForCurrentClass; + private String currentClassname; + private String suiteName; + + /** + * The given name is automatically prefixed with the package containing the tests to be run. + * If more than one package is specified, the first is used. + * + * @param clazz Use the class from your .apk. Use the class name for the test suite name. + * Use the class' classloader in order to load classes for testing. + * This is needed when running in the emulator. + */ + public TestSuiteBuilder(Class clazz) { + this(clazz.getName(), clazz.getClassLoader()); + } + + public TestSuiteBuilder(String name, ClassLoader classLoader) { + this.suiteName = name; + this.testGrouping = new TestGrouping(SORT_BY_FULLY_QUALIFIED_NAME, classLoader); + this.testCases = new ArrayList<>(); + addRequirements(REJECT_SUPPRESSED); + } + + /** @hide pending API Council approval */ + public TestSuiteBuilder addTestClassByName(String testClassName, String testMethodName, + Context context) { + + AndroidTestRunner atr = new AndroidTestRunner(); + atr.setContext(context); + atr.setTestClassName(testClassName, testMethodName); + + this.testCases.addAll(atr.getTestCases()); + return this; + } + + /** @hide pending API Council approval */ + public TestSuiteBuilder addTestSuite(TestSuite testSuite) { + for (TestCase testCase : (List) TestCaseUtil.getTests(testSuite, true)) { + this.testCases.add(testCase); + } + return this; + } + + /** + * Include all tests that satisfy the requirements in the given packages and all sub-packages, + * unless otherwise specified. + * + * @param packageNames Names of packages to add. + * @return The builder for method chaining. + */ + public TestSuiteBuilder includePackages(String... packageNames) { + testGrouping.addPackagesRecursive(packageNames); + return this; + } + + /** + * Exclude all tests in the given packages and all sub-packages, unless otherwise specified. + * + * @param packageNames Names of packages to remove. + * @return The builder for method chaining. + */ + public TestSuiteBuilder excludePackages(String... packageNames) { + testGrouping.removePackagesRecursive(packageNames); + return this; + } + + /** + * Exclude tests that fail to satisfy all of the given predicates. + * + * @param predicates Predicates to add to the list of requirements. + * @return The builder for method chaining. + * @hide + */ + public TestSuiteBuilder addRequirements(List> predicates) { + this.predicates.addAll(predicates); + return this; + } + + /** + * Include all junit tests that satisfy the requirements in the calling class' package and all + * sub-packages. + * + * @return The builder for method chaining. + */ + public final TestSuiteBuilder includeAllPackagesUnderHere() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + + String callingClassName = null; + String thisClassName = TestSuiteBuilder.class.getName(); + + // We want to get the package of this method's calling class. This method's calling class + // should be one level below this class in the stack trace. + for (int i = 0; i < stackTraceElements.length; i++) { + StackTraceElement element = stackTraceElements[i]; + if (thisClassName.equals(element.getClassName()) + && "includeAllPackagesUnderHere".equals(element.getMethodName())) { + // We've found this class in the call stack. The calling class must be the + // next class in the stack. + callingClassName = stackTraceElements[i + 1].getClassName(); + break; + } + } + + String packageName = parsePackageNameFromClassName(callingClassName); + return includePackages(packageName); + } + + /** + * Override the default name for the suite being built. This should generally be called if you + * call {@code addRequirements(com.android.internal.util.Predicate[])} to make it clear which + * tests will be included. The name you specify is automatically prefixed with the package + * containing the tests to be run. If more than one package is specified, the first is used. + * + * @param newSuiteName Prefix of name to give the suite being built. + * @return The builder for method chaining. + */ + public TestSuiteBuilder named(String newSuiteName) { + suiteName = newSuiteName; + return this; + } + + /** + * Call this method once you've configured your builder as desired. + * + * @return The suite containing the requested tests. + */ + public final TestSuite build() { + rootSuite = new TestSuite(getSuiteName()); + + // Keep track of current class so we know when to create a new sub-suite. + currentClassname = null; + try { + for (TestMethod test : testGrouping.getTests()) { + if (satisfiesAllPredicates(test)) { + addTest(test); + } + } + if (testCases.size() > 0) { + for (TestCase testCase : testCases) { + if (satisfiesAllPredicates(new TestMethod(testCase))) { + addTest(testCase); + } + } + } + } catch (Exception exception) { + Log.i("TestSuiteBuilder", "Failed to create test.", exception); + TestSuite suite = new TestSuite(getSuiteName()); + suite.addTest(new FailedToCreateTests(exception)); + return suite; + } + return rootSuite; + } + + /** + * Subclasses use this method to determine the name of the suite. + * + * @return The package and suite name combined. + */ + protected String getSuiteName() { + return suiteName; + } + + /** + * Exclude tests that fail to satisfy all of the given predicates. If you call this method, you + * probably also want to call {@link #named(String)} to override the default suite name. + * + * @param predicates Predicates to add to the list of requirements. + * @return The builder for method chaining. + * @hide + */ + public final TestSuiteBuilder addRequirements(Predicate... predicates) { + ArrayList> list = new ArrayList>(); + Collections.addAll(list, predicates); + return addRequirements(list); + } + + /** + * A special {@link junit.framework.TestCase} used to indicate a failure during the build() + * step. + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ + @Deprecated + public static class FailedToCreateTests extends TestCase { + private final Exception exception; + + public FailedToCreateTests(Exception exception) { + super("testSuiteConstructionFailed"); + this.exception = exception; + } + + public void testSuiteConstructionFailed() { + throw new RuntimeException("Exception during suite construction", exception); + } + } + + private boolean satisfiesAllPredicates(TestMethod test) { + for (Predicate predicate : predicates) { + if (!predicate.apply(test)) { + return false; + } + } + return true; + } + + private void addTest(TestMethod testMethod) throws Exception { + addSuiteIfNecessary(testMethod.getEnclosingClassname()); + suiteForCurrentClass.addTest(testMethod.createTest()); + } + + private void addTest(Test test) { + addSuiteIfNecessary(test.getClass().getName()); + suiteForCurrentClass.addTest(test); + } + + private void addSuiteIfNecessary(String parentClassname) { + if (!parentClassname.equals(currentClassname)) { + currentClassname = parentClassname; + suiteForCurrentClass = new TestSuite(parentClassname); + rootSuite.addTest(suiteForCurrentClass); + } + } + + private static String parsePackageNameFromClassName(String className) { + return className.substring(0, className.lastIndexOf('.')); + } +} diff --git a/src/test-runner/android/test/suitebuilder/UnitTestSuiteBuilder.java b/src/test-runner/android/test/suitebuilder/UnitTestSuiteBuilder.java new file mode 100644 index 00000000..ea7405b3 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/UnitTestSuiteBuilder.java @@ -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.suitebuilder; + +/** + * A suite builder that finds unit tests. + * + * @deprecated android.test.runner is obsolete + */ +@Deprecated +public class UnitTestSuiteBuilder extends TestSuiteBuilder { + + public UnitTestSuiteBuilder(Class clazz) { + this(clazz.getName(), clazz.getClassLoader()); + } + + + public UnitTestSuiteBuilder(String name, ClassLoader classLoader) { + super(name, classLoader); + addRequirements(TestPredicates.REJECT_INSTRUMENTATION); + } +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/HasAnnotation.java b/src/test-runner/android/test/suitebuilder/annotation/HasAnnotation.java new file mode 100644 index 00000000..240bd588 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/HasAnnotation.java @@ -0,0 +1,44 @@ +/* + * 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.suitebuilder.annotation; + +import com.android.internal.util.Predicates; +import com.android.internal.util.Predicate; +import android.test.suitebuilder.TestMethod; + +import java.lang.annotation.Annotation; + +/** + * A predicate that checks to see if a {@link TestMethod} has a specific annotation, either on the + * method or on the containing class. + * + * {@hide} Not needed for 1.0 SDK. + */ +public class HasAnnotation implements Predicate { + + private Predicate hasMethodOrClassAnnotation; + + public HasAnnotation(Class annotationClass) { + this.hasMethodOrClassAnnotation = Predicates.or( + new HasMethodAnnotation(annotationClass), + new HasClassAnnotation(annotationClass)); + } + + public boolean apply(TestMethod testMethod) { + return hasMethodOrClassAnnotation.apply(testMethod); + } +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/HasClassAnnotation.java b/src/test-runner/android/test/suitebuilder/annotation/HasClassAnnotation.java new file mode 100644 index 00000000..ac76f4cb --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/HasClassAnnotation.java @@ -0,0 +1,41 @@ +/* + * 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.suitebuilder.annotation; + +import java.lang.annotation.Annotation; + +import android.test.suitebuilder.TestMethod; +import com.android.internal.util.Predicate; + +/** + * A predicate that checks to see if a {@link android.test.suitebuilder.TestMethod} has a specific annotation on the + * containing class. Consider using the public {@link HasAnnotation} class instead of this class. + * + * {@hide} Not needed for 1.0 SDK. + */ +class HasClassAnnotation implements Predicate { + + private Class annotationClass; + + public HasClassAnnotation(Class annotationClass) { + this.annotationClass = annotationClass; + } + + public boolean apply(TestMethod testMethod) { + return testMethod.getEnclosingClass().getAnnotation(annotationClass) != null; + } +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/HasMethodAnnotation.java b/src/test-runner/android/test/suitebuilder/annotation/HasMethodAnnotation.java new file mode 100644 index 00000000..96bd9227 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/HasMethodAnnotation.java @@ -0,0 +1,41 @@ +/* + * 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.suitebuilder.annotation; + +import com.android.internal.util.Predicate; +import android.test.suitebuilder.TestMethod; + +import java.lang.annotation.Annotation; + +/** + * A predicate that checks to see if a the method represented by {@link TestMethod} has a certain + * annotation on it. Consider using the public {@link HasAnnotation} class instead of this class. + * + * {@hide} Not needed for 1.0 SDK. + */ +class HasMethodAnnotation implements Predicate { + + private final Class annotationClass; + + public HasMethodAnnotation(Class annotationClass) { + this.annotationClass = annotationClass; + } + + public boolean apply(TestMethod testMethod) { + return testMethod.getAnnotation(annotationClass) != null; + } +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/LargeTest.java b/src/test-runner/android/test/suitebuilder/annotation/LargeTest.java new file mode 100644 index 00000000..dc77ee6b --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/LargeTest.java @@ -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.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the large tests. + * + * @deprecated Use + * + * LargeTest instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface LargeTest { +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/MediumTest.java b/src/test-runner/android/test/suitebuilder/annotation/MediumTest.java new file mode 100644 index 00000000..b941da03 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/MediumTest.java @@ -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.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the medium tests. + * + * @deprecated Use + * + * MediumTest instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface MediumTest { +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/SmallTest.java b/src/test-runner/android/test/suitebuilder/annotation/SmallTest.java new file mode 100644 index 00000000..d3c74f01 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/SmallTest.java @@ -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.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the small tests. + * + * @deprecated Use + * + * SmallTest instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface SmallTest { +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/Smoke.java b/src/test-runner/android/test/suitebuilder/annotation/Smoke.java new file mode 100644 index 00000000..34563717 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/Smoke.java @@ -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.suitebuilder.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test that should run as part of the smoke tests. + * The android.test.suitebuilder.SmokeTestSuiteBuilder + * will run all tests with this annotation. + * + * @deprecated New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface Smoke { +} diff --git a/src/test-runner/android/test/suitebuilder/annotation/Suppress.java b/src/test-runner/android/test/suitebuilder/annotation/Suppress.java new file mode 100644 index 00000000..629a3cf4 --- /dev/null +++ b/src/test-runner/android/test/suitebuilder/annotation/Suppress.java @@ -0,0 +1,38 @@ +/* + * 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.suitebuilder.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * Use this annotation on test classes or test methods that should not be included in a test + * suite. If the annotation appears on the class then no tests in that class will be included. If + * the annotation appears only on a test method then only that method will be excluded. + * + * @deprecated Use + * + * Suppress instead. New tests should be written using the + * Android Testing Support Library. + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface Suppress { +} diff --git a/src/test-runner/com/android/internal/util/Predicate.java b/src/test-runner/com/android/internal/util/Predicate.java new file mode 100644 index 00000000..e87f489f --- /dev/null +++ b/src/test-runner/com/android/internal/util/Predicate.java @@ -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 com.android.internal.util; + +/** + * A Predicate can determine a true or false value for any input of its + * parameterized type. For example, a {@code RegexPredicate} might implement + * {@code Predicate}, and return true for any String that matches its + * given regular expression. + *

+ *

+ * Implementors of Predicate which may cause side effects upon evaluation are + * strongly encouraged to state this fact clearly in their API documentation. + * + * @deprecated Use {@code java.util.function.Predicate} instead. + * This must not be used outside frameworks/base/test-runner. + */ +@Deprecated +public interface Predicate { + + boolean apply(T t); +} diff --git a/src/test-runner/com/android/internal/util/Predicates.java b/src/test-runner/com/android/internal/util/Predicates.java new file mode 100644 index 00000000..fe7c84b6 --- /dev/null +++ b/src/test-runner/com/android/internal/util/Predicates.java @@ -0,0 +1,117 @@ +/* + * 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 com.android.internal.util; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +/** + * Predicates contains static methods for creating the standard set of + * {@code Predicate} objects. + */ +public class Predicates { + private Predicates() { + } + + private static List defensiveCopy(T... array) { + return defensiveCopy(Arrays.asList(array)); + } + + static List defensiveCopy(Iterable iterable) { + ArrayList list = new ArrayList<>(); + for (T element : iterable) { + list.add(/*checkNotNull(*/element/*)*/); + } + return list; + } + + private static String toStringHelper(String methodName, Iterable components) { + StringBuilder builder = new StringBuilder("Predicates.").append(methodName).append('('); + boolean first = true; + for (Object o : components) { + if (!first) { + builder.append(','); + } + builder.append(o); + first = false; + } + return builder.append(')').toString(); + } + + private static class OrPredicate + implements Predicate, Serializable { + private final List> components; + + private OrPredicate(List> components) { + this.components = components; + } + + @Override + public boolean apply(/*@ParametricNullness*/ T t) { + // Avoid using the Iterator to avoid generating garbage (issue 820). + for (int i = 0; i < components.size(); i++) { + if (components.get(i).apply(t)) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + // add a random number to avoid collisions with AndPredicate + return components.hashCode() + 0x053c91cf; + } + + @Override + public boolean equals(/*@CheckForNull*/ Object obj) { + if (obj instanceof OrPredicate) { + OrPredicate that = (OrPredicate) obj; + return components.equals(that.components); + } + return false; + } + + @Override + public String toString() { + return toStringHelper("or", components); + } + + private static final long serialVersionUID = 0; + } + + public static Predicate or(Predicate... components) { + return new OrPredicate(defensiveCopy(components)); + } + + /** + * Returns a Predicate that evaluates to true iff the given Predicate + * evaluates to false. + */ + public static Predicate not(Predicate predicate) { + return new NotPredicate(predicate); + } + + private static class NotPredicate implements Predicate { + private final Predicate predicate; + private NotPredicate(Predicate predicate) { + this.predicate = predicate; + } + public boolean apply(T t) { + return !predicate.apply(t); + } + } +} diff --git a/src/test-runner/dalvik/annotation/BrokenTest.java b/src/test-runner/dalvik/annotation/BrokenTest.java new file mode 100644 index 00000000..a334d108 --- /dev/null +++ b/src/test-runner/dalvik/annotation/BrokenTest.java @@ -0,0 +1,33 @@ +/* + * 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 dalvik.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Marks a test case as broken. This means the test case should be fixed. + * + * @hide + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface BrokenTest { + /** + * Plain text reason for adding this annotation. + */ + String value(); +} diff --git a/src/test-runner/dalvik/annotation/SideEffect.java b/src/test-runner/dalvik/annotation/SideEffect.java new file mode 100644 index 00000000..2f68c735 --- /dev/null +++ b/src/test-runner/dalvik/annotation/SideEffect.java @@ -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 dalvik.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Marks a test-case as either having a side-effect that other tests might + * notice or suffering from such a side effect. Such tests should be run in an + * isolated manner. + * + * @hide + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SideEffect { + /** + * Plain text reason for adding this annotation. + */ + String value(); +} diff --git a/src/test-runner/meson.build b/src/test-runner/meson.build new file mode 100644 index 00000000..effa7753 --- /dev/null +++ b/src/test-runner/meson.build @@ -0,0 +1,67 @@ +test_runner_jar = jar('test_runner', [ + # test-runner + 'android/test/ActivityInstrumentationTestCase2.java', + 'android/test/ActivityInstrumentationTestCase.java', + 'android/test/ActivityTestCase.java', + 'android/test/ActivityUnitTestCase.java', + 'android/test/AndroidTestRunner.java', + 'android/test/ApplicationTestCase.java', + 'android/test/AssertionFailedError.java', + 'android/test/ClassPathPackageInfoSource.java', + 'android/test/ComparisonFailure.java', + 'android/test/InstrumentationTestRunner.java', +# 'android/test/IsolatedContext.java', + 'android/test/LaunchPerformanceBase.java', +# 'android/test/LoaderTestCase.java', + 'android/test/MoreAsserts.java', + 'android/test/NoExecTestResult.java', +# 'android/test/ProviderTestCase2.java', +# 'android/test/ProviderTestCase.java', + 'android/test/RenamingDelegatingContext.java', +# 'android/test/ServiceTestCase.java', + 'android/test/SimpleCache.java', +# 'android/test/SingleLaunchActivityTestCase.java', + 'android/test/SyncBaseInstrumentation.java', + 'android/test/TestCaseUtil.java', + 'android/test/TestPrinter.java', + 'android/test/TestSuiteProvider.java', +# 'android/test/TouchUtils.java', + 'android/test/ViewAsserts.java', + 'android/test/suitebuilder/AssignableFrom.java', + 'android/test/suitebuilder/SmokeTestSuiteBuilder.java', + 'android/test/suitebuilder/TestGrouping.java', + 'android/test/suitebuilder/TestMethod.java', + 'android/test/suitebuilder/TestPredicates.java', + 'android/test/suitebuilder/TestSuiteBuilder.java', + 'android/test/suitebuilder/UnitTestSuiteBuilder.java', + # old stuff (needed for older CTS versions) + 'android/test/suitebuilder/annotation/HasAnnotation.java', + 'android/test/suitebuilder/annotation/HasClassAnnotation.java', + 'android/test/suitebuilder/annotation/HasMethodAnnotation.java', + 'com/android/internal/util/Predicates.java', + # test-base + 'android/test/AndroidTestCase.java', + 'android/test/FlakyTest.java', + 'android/test/InstrumentationTestCase.java', + 'android/test/InstrumentationTestSuite.java', + 'android/test/PerformanceTestCase.java', + 'android/test/RepetitiveTest.java', + 'android/test/UiThreadTest.java', + 'android/test/suitebuilder/annotation/LargeTest.java', + 'android/test/suitebuilder/annotation/MediumTest.java', + 'android/test/suitebuilder/annotation/SmallTest.java', + 'android/test/suitebuilder/annotation/Smoke.java', + 'android/test/suitebuilder/annotation/Suppress.java', + 'com/android/internal/util/Predicate.java', + # this was part of dalvik but got removed at some point + 'dalvik/annotation/BrokenTest.java', + 'dalvik/annotation/SideEffect.java', + ], + link_with: [hax_jar], + java_args: [ + '-bootclasspath', bootclasspath, + '-source', '1.8', '-target', '1.8', + '-encoding', 'UTF-8', + '-Xlint:-deprecation', # we implement deprecated APIs + '-h', join_paths(dir_base, 'src/api-impl-jni/generated_headers') + ])