SkQP: add junit app

Change-Id: Ic32eaec6cce1509f07e7cf610717d3b12d335c89
Reviewed-on: https://skia-review.googlesource.com/83921
Reviewed-by: Hal Canary <halcanary@google.com>
Commit-Queue: Hal Canary <halcanary@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index b03f111..96c7623 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1729,6 +1729,19 @@
       ]
     }
   }
+  if (is_android && skia_enable_gpu) {
+    test_app("skqp_app") {
+      is_shared_library = true
+      sources = [
+        "tools/skqp/jni/org_skia_skqp_SkQPRunner.cpp",
+      ]
+      deps = [
+        ":skia",
+        ":skqp_lib",
+      ]
+      libs = [ "android" ]
+    }
+  }
 
   if (skia_enable_gpu) {
     test_lib("sk_app") {
diff --git a/platform_tools/android/apps/settings.gradle b/platform_tools/android/apps/settings.gradle
index 75ce0a6..a8c2cb3 100644
--- a/platform_tools/android/apps/settings.gradle
+++ b/platform_tools/android/apps/settings.gradle
@@ -1 +1,2 @@
 include ':viewer'
+include ':skqp'
diff --git a/platform_tools/android/apps/skqp/build.gradle b/platform_tools/android/apps/skqp/build.gradle
new file mode 100644
index 0000000..0a883ca
--- /dev/null
+++ b/platform_tools/android/apps/skqp/build.gradle
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+apply plugin: 'com.android.application'
+
+dependencies {
+    compile 'com.android.support:support-annotations:24.0.0'
+    compile 'com.android.support.test:runner:0.5'
+    compile group: 'junit', name: 'junit', version: '4.+'
+}
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion "22.0.1"
+    defaultConfig {
+        applicationId "org.skia.skqp"
+        minSdkVersion 19
+        targetSdkVersion 19
+        versionCode 1
+        versionName "1.0"
+        signingConfig signingConfigs.debug
+    }
+    sourceSets.main.jni.srcDirs = []
+    sourceSets.main.jniLibs.srcDir "src/main/libs"
+    productFlavors { arm {}; arm64 {}; x86 {}; x64 {}; arm64vulkan{}; }
+    setupSkiaLibraryBuild(project, applicationVariants, "libskqp_app")
+}
diff --git a/platform_tools/android/apps/skqp/src/main/AndroidManifest.xml b/platform_tools/android/apps/skqp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..30028fb
--- /dev/null
+++ b/platform_tools/android/apps/skqp/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="org.skia.skqp"
+          android:versionCode="1"
+          android:versionName="1.0">
+  <application><uses-library android:name="android.test.runner" /></application>
+  <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                   android:targetPackage="org.skia.skqp"></instrumentation>
+</manifest>
diff --git a/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQP.java b/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQP.java
new file mode 100644
index 0000000..c5843f0
--- /dev/null
+++ b/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQP.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package org.skia.skqp;
+
+import org.junit.runner.RunWith;
+
+@RunWith(SkQPRunner.class)
+public class SkQP {}
+
diff --git a/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPException.java b/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPException.java
new file mode 100644
index 0000000..a7c8721
--- /dev/null
+++ b/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPException.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package org.skia.skqp;
+
+public class SkQPException extends Exception {
+    public SkQPException(String m) { super(m); }
+}
diff --git a/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPRunner.java b/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPRunner.java
new file mode 100644
index 0000000..1cf3aef
--- /dev/null
+++ b/platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPRunner.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+package org.skia.skqp;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+
+public class SkQPRunner extends Runner {
+    private native void nInit(AssetManager assetManager, String dataDir);
+    private native float nExecuteGM(int gm, int backend) throws SkQPException;
+    private native String[] nExecuteUnitTest(int test);
+
+    private AssetManager mAssetManager;
+    private String[] mGMs;
+    private String[] mBackends;
+    private String[] mUnitTests;
+
+    private static boolean sOnceFlag = false;
+    private static final String kSkiaGM = "SkiaGM_";
+    private static final String kSkiaUnitTests = "Skia_UnitTests";
+
+    private Description mDescription;
+
+    private static void DeleteDirectoryContents(File f) throws IOException {
+        for (File s : f.listFiles()) {
+            if (s.isDirectory()) {
+                SkQPRunner.DeleteDirectoryContents(s);
+            }
+            s.delete();
+        }
+    }
+
+    private static void Fail(Description desc, RunNotifier notifier, String failure) {
+        notifier.fireTestFailure(new Failure(desc, new Throwable(failure)));
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+
+    public SkQPRunner(Class testClass) {
+        synchronized (SkQPRunner.class) {
+            if (sOnceFlag) {
+                throw new IllegalStateException("Error multiple SkQPs defined");
+            }
+            sOnceFlag = true;
+        }
+        System.loadLibrary("skqp_app");
+
+        Context context = InstrumentationRegistry.getTargetContext();
+        File filesDir = context.getFilesDir();
+        try {
+            SkQPRunner.DeleteDirectoryContents(filesDir);
+        } catch (IOException e) {
+            Log.w("org.skis.skqp", "DeleteDirectoryContents: " + e.getMessage());
+        }
+
+        Resources resources = context.getResources();
+        mAssetManager = resources.getAssets();
+        this.nInit(mAssetManager, filesDir.getAbsolutePath());
+
+        mDescription = Description.createSuiteDescription(testClass);
+        Annotation annots[] = new Annotation[0];
+        for (int backend = 0; backend < mBackends.length; backend++) {
+            String classname = kSkiaGM + mBackends[backend];
+            for (int gm = 0; gm < mGMs.length; gm++) {
+                mDescription.addChild(Description.createTestDescription(classname, mGMs[gm], annots));
+            }
+        }
+        for (int unitTest = 0; unitTest < mUnitTests.length; unitTest++) {
+            mDescription.addChild(Description.createTestDescription(kSkiaUnitTests,
+                        mUnitTests[unitTest], annots));
+        }
+    }
+
+    @Override
+    public Description getDescription() { return mDescription; }
+
+    @Override
+    public int testCount() { return mUnitTests.length + mGMs.length * mBackends.length; }
+
+    @Override
+    public void run(RunNotifier notifier) {
+        Annotation annots[] = new Annotation[0];
+        for (int backend = 0; backend < mBackends.length; backend++) {
+            String classname = kSkiaGM + mBackends[backend];
+            for (int gm = 0; gm < mGMs.length; gm++) {
+                Description desc = Description.createTestDescription(classname, mGMs[gm], annots);
+                notifier.fireTestStarted(desc);
+                float value = java.lang.Float.MAX_VALUE;
+                String error = null;
+                try {
+                    value = this.nExecuteGM(gm, backend);
+                } catch (SkQPException exept) {
+                    error = exept.getMessage();
+                }
+                if (error != null) {
+                    SkQPRunner.Fail(desc, notifier, String.format("Exception: %s", error));
+                } else if (value != 0) {
+                    SkQPRunner.Fail(desc, notifier, String.format(
+                                "Image mismatch: max channel diff = %f", value));
+                }
+                notifier.fireTestFinished(desc);
+            }
+        }
+        for (int unitTest = 0; unitTest < mUnitTests.length; unitTest++) {
+            Description desc = Description.createTestDescription(
+                    kSkiaUnitTests, mUnitTests[unitTest], annots);
+            notifier.fireTestStarted(desc);
+            String[] errors = this.nExecuteUnitTest(unitTest);
+            if (errors != null && errors.length > 0) {
+                for (String error : errors) {
+                    SkQPRunner.Fail(desc, notifier, error);
+                }
+            }
+            notifier.fireTestFinished(desc);
+        }
+    }
+}
+
diff --git a/tools/skqp/README.md b/tools/skqp/README.md
index beac911..d3156a0 100644
--- a/tools/skqp/README.md
+++ b/tools/skqp/README.md
@@ -63,4 +63,20 @@
 Run as an APK
 -------------
 
-[TODO]
+1.  Build the skqp.apk, load it on the device, and run the tests
+
+        platform_tools/android/bin/android_build_app -C out/${arch}-rel skqp
+        adb install -r out/${arch}-rel/skqp.apk
+        adb shell am instrument -w \
+            org.skia.skqp/android.support.test.runner.AndroidJUnitRunner
+
+2.  Retrieve the report if there are any errors:
+
+        rm -rf /tmp/skqp
+        mkdir /tmp/skqp
+        adb backup -f /tmp/skqp/backup.ab org.skia.skqp
+        dd if=/tmp/skqp/backup.ab bs=24 skip=1 | tools/skqp/inflate.py | \
+            ( cd /tmp/skqp; tar x )
+        rm /tmp/skqp/backup.ab
+        tools/skqp/make_report.py /tmp/skqp/apps/org.skia.skqp/f
+
diff --git a/tools/skqp/inflate.py b/tools/skqp/inflate.py
new file mode 100755
index 0000000..97cc3a4
--- /dev/null
+++ b/tools/skqp/inflate.py
@@ -0,0 +1,8 @@
+#! /usr/bin/env python2
+# Copyright 2017 Google Inc.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import sys
+import zlib
+sys.stdout.write(zlib.decompress(sys.stdin.read()))
+
diff --git a/tools/skqp/jni/org_skia_skqp_SkQPRunner.cpp b/tools/skqp/jni/org_skia_skqp_SkQPRunner.cpp
new file mode 100644
index 0000000..9c9c9df
--- /dev/null
+++ b/tools/skqp/jni/org_skia_skqp_SkQPRunner.cpp
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <mutex>
+#include <vector>
+
+#include <jni.h>
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+
+#include "gm_runner.h"
+#include "skqp_asset_manager.h"
+#include "SkStream.h"
+
+////////////////////////////////////////////////////////////////////////////////
+extern "C" {
+JNIEXPORT void JNICALL Java_org_skia_skqp_SkQPRunner_nInit(JNIEnv*, jobject, jobject, jstring);
+JNIEXPORT jfloat JNICALL Java_org_skia_skqp_SkQPRunner_nExecuteGM(JNIEnv*, jobject, jint, jint);
+JNIEXPORT jobjectArray JNICALL Java_org_skia_skqp_SkQPRunner_nExecuteUnitTest(JNIEnv*, jobject,
+                                                                              jint);
+}  // extern "C"
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+struct AndroidAssetManager : public skqp::AssetManager {
+    AAssetManager* fMgr = nullptr;
+    std::unique_ptr<SkStreamAsset> open(const char* path) override {
+        struct AAStrm : public SkStreamAsset {
+            AAssetManager* fMgr;
+            std::string fPath;
+            AAsset* fAsset;
+            AAStrm(AAssetManager* m, std::string p, AAsset* a)
+                : fMgr(m), fPath(std::move(p)), fAsset(a) {}
+            ~AAStrm() override { AAsset_close(fAsset); }
+            size_t read(void* buffer, size_t size) override {
+                size_t r = SkTMin(size, SkToSizeT(AAsset_getRemainingLength(fAsset)));
+                if (buffer) {
+                    return SkToSizeT(AAsset_read(fAsset, buffer, r));
+                } else {
+                    this->move(SkTo<long>(r));
+                    return r;
+                }
+            }
+            size_t getLength() const  override { return SkToSizeT(AAsset_getLength(fAsset)); }
+            size_t peek(void* buffer, size_t size) const override {
+                size_t r = const_cast<AAStrm*>(this)->read(buffer, size);
+                const_cast<AAStrm*>(this)->move(-(long)r);
+                return r;
+            }
+            bool isAtEnd() const override { return 0 == AAsset_getRemainingLength(fAsset); }
+            bool rewind() override { return this->seek(0); }
+            size_t getPosition() const override {
+                return SkToSizeT(AAsset_seek(fAsset, 0, SEEK_CUR));
+            }
+            bool seek(size_t position) override {
+                return -1 != AAsset_seek(fAsset, SkTo<off_t>(position), SEEK_SET);
+            }
+            bool move(long offset) override {
+                return -1 != AAsset_seek(fAsset, SkTo<off_t>(offset), SEEK_CUR);
+            }
+            SkStreamAsset* onDuplicate() const override {
+                AAsset* dupAsset = AndroidAssetManager::OpenAsset(fMgr, fPath.c_str());
+                return dupAsset ? new AAStrm(fMgr, fPath, dupAsset) : nullptr;
+            }
+            SkStreamAsset* onFork() const override {
+                SkStreamAsset* dup = this->onDuplicate();
+                if (dup) { (void)dup->seek(this->getPosition()); }
+                return dup;
+            }
+        };
+        AAsset* asset = AndroidAssetManager::OpenAsset(fMgr, path);
+        return asset ? std::unique_ptr<SkStreamAsset>(new AAStrm(fMgr, std::string(path), asset))
+                     : nullptr;
+    }
+    static AAsset* OpenAsset(AAssetManager* mgr, const char* path) {
+        std::string fullPath = std::string("gmkb/") + path;
+        return mgr ? AAssetManager_open(mgr, fullPath.c_str(), AASSET_MODE_STREAMING) : nullptr;
+    }
+};
+}
+
+static void set_string_array_element(JNIEnv* env, jobjectArray a, const char* s, unsigned i) {
+    jstring jstr = env->NewStringUTF(s);
+    env->SetObjectArrayElement(a, (jsize)i, jstr);
+    env->DeleteLocalRef(jstr);
+}
+
+#define jassert(env, cond) do { if (!(cond)) { \
+    (env)->ThrowNew((env)->FindClass("java/lang/Exception"), \
+                    __FILE__ ": assert(" #cond ") failed."); } } while (0)
+
+////////////////////////////////////////////////////////////////////////////////
+
+static std::mutex gMutex;
+static std::vector<gm_runner::SkiaBackend> gBackends;
+static std::vector<gm_runner::GMFactory> gGMs;
+static std::vector<gm_runner::UnitTest> gUnitTests;
+static AndroidAssetManager gAssetManager;
+static std::string gReportDirectory;
+static jclass gStringClass = nullptr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <typename T, typename F>
+jobjectArray to_java_string_array(JNIEnv* env,
+                                  const std::vector<T>& array,
+                                  F toString) {
+    jobjectArray jarray = env->NewObjectArray((jint)array.size(), gStringClass, nullptr);
+    for (unsigned i = 0; i < array.size(); ++i) {
+        set_string_array_element(env, jarray, std::string(toString(array[i])).c_str(), i);
+    }
+    return jarray;
+}
+
+void Java_org_skia_skqp_SkQPRunner_nInit(JNIEnv* env, jobject object, jobject assetManager,
+                                         jstring dataDir) {
+    jclass clazz = env->GetObjectClass(object);
+    jassert(env, assetManager);
+
+    gm_runner::InitSkia();
+
+    std::lock_guard<std::mutex> lock(gMutex);
+    gAssetManager.fMgr = AAssetManager_fromJava(env, assetManager);
+    jassert(env, gAssetManager.fMgr);
+
+    const char* dataDirString = env->GetStringUTFChars(dataDir, nullptr);
+    gReportDirectory = dataDirString;
+    env->ReleaseStringUTFChars(dataDir, dataDirString);
+
+    gBackends = gm_runner::GetSupportedBackends();
+    gGMs = gm_runner::GetGMFactories(&gAssetManager);
+    gUnitTests = gm_runner::GetUnitTests();
+    gStringClass = env->FindClass("java/lang/String");
+
+    constexpr char stringArrayType[] = "[Ljava/lang/String;";
+    env->SetObjectField(object, env->GetFieldID(clazz, "mBackends", stringArrayType),
+                        to_java_string_array(env, gBackends, gm_runner::GetBackendName));
+    env->SetObjectField(object, env->GetFieldID(clazz, "mUnitTests", stringArrayType),
+                        to_java_string_array(env, gUnitTests, gm_runner::GetUnitTestName));
+    env->SetObjectField(object, env->GetFieldID(clazz, "mGMs", stringArrayType),
+                        to_java_string_array(env, gGMs, gm_runner::GetGMName));
+}
+
+jfloat Java_org_skia_skqp_SkQPRunner_nExecuteGM(JNIEnv* env,
+                                                jobject object,
+                                                jint gmIndex,
+                                                jint backendIndex) {
+    jassert(env, gmIndex < (jint)gGMs.size());
+    jassert(env, backendIndex < (jint)gBackends.size());
+    gm_runner::GMFactory gm;
+    gm_runner::SkiaBackend backend;
+    std::string reportDirectoryPath;
+    {
+        std::lock_guard<std::mutex> lock(gMutex);
+        backend = gBackends[backendIndex];
+        gm = gGMs[gmIndex];
+        reportDirectoryPath = gReportDirectory;
+    }
+    float result;
+    gm_runner::Error error;
+    std::tie(result, error) = gm_runner::EvaluateGM(backend, gm, &gAssetManager,
+                                                    reportDirectoryPath.c_str());
+    if (error != gm_runner::Error::None) {
+        (void)env->ThrowNew(env->FindClass("org/skia/skqp/SkQPException"),
+                            gm_runner::GetErrorString(error));
+    }
+    return result;
+}
+
+jobjectArray Java_org_skia_skqp_SkQPRunner_nExecuteUnitTest(JNIEnv* env,
+                                                            jobject object,
+                                                            jint index) {
+    jassert(env, index < (jint)gUnitTests.size());
+    gm_runner::UnitTest test;
+    {
+        std::lock_guard<std::mutex> lock(gMutex);
+        test = gUnitTests[index];
+    }
+    std::vector<std::string> errors = gm_runner::ExecuteTest(test);
+    if (errors.size() == 0) {
+        return nullptr;
+    }
+    jobjectArray array = env->NewObjectArray(errors.size(), gStringClass, nullptr);
+    for (unsigned i = 0; i < errors.size(); ++i) {
+        set_string_array_element(env, array, errors[i].c_str(), i);
+    }
+    return (jobjectArray)env->NewGlobalRef(array);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+