From 28f89389f026379aad6f90254ba37f4d37272bcb Mon Sep 17 00:00:00 2001 From: Hal Canary Date: Tue, 12 Dec 2017 09:42:14 -0500 Subject: SkQP: add junit app Change-Id: Ic32eaec6cce1509f07e7cf610717d3b12d335c89 Reviewed-on: https://skia-review.googlesource.com/83921 Reviewed-by: Hal Canary Commit-Queue: Hal Canary --- BUILD.gn | 13 ++ platform_tools/android/apps/settings.gradle | 1 + platform_tools/android/apps/skqp/build.gradle | 30 ++++ .../android/apps/skqp/src/main/AndroidManifest.xml | 9 + .../skqp/src/main/java/org/skia/skqp/SkQP.java | 14 ++ .../src/main/java/org/skia/skqp/SkQPException.java | 12 ++ .../src/main/java/org/skia/skqp/SkQPRunner.java | 133 ++++++++++++++ tools/skqp/README.md | 18 +- tools/skqp/inflate.py | 8 + tools/skqp/jni/org_skia_skqp_SkQPRunner.cpp | 195 +++++++++++++++++++++ 10 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 platform_tools/android/apps/skqp/build.gradle create mode 100644 platform_tools/android/apps/skqp/src/main/AndroidManifest.xml create mode 100644 platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQP.java create mode 100644 platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPException.java create mode 100644 platform_tools/android/apps/skqp/src/main/java/org/skia/skqp/SkQPRunner.java create mode 100755 tools/skqp/inflate.py create mode 100644 tools/skqp/jni/org_skia_skqp_SkQPRunner.cpp diff --git a/BUILD.gn b/BUILD.gn index b03f1117d9..96c76235f7 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -1729,6 +1729,19 @@ if (skia_enable_tools) { ] } } + 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 75ce0a6db1..a8c2cb3fb4 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 0000000000..0a883ca869 --- /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 0000000000..30028fb436 --- /dev/null +++ b/platform_tools/android/apps/skqp/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + 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 0000000000..c5843f013e --- /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 0000000000..a7c8721bfb --- /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 0000000000..1cf3aef9e2 --- /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 beac911ba0..d3156a0573 100644 --- a/tools/skqp/README.md +++ b/tools/skqp/README.md @@ -63,4 +63,20 @@ Run as an executable 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 0000000000..97cc3a405d --- /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 0000000000..9c9c9dfa10 --- /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 +#include + +#include +#include +#include + +#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 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(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(this)->read(buffer, size); + const_cast(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(position), SEEK_SET); + } + bool move(long offset) override { + return -1 != AAsset_seek(fAsset, SkTo(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(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 gBackends; +static std::vector gGMs; +static std::vector gUnitTests; +static AndroidAssetManager gAssetManager; +static std::string gReportDirectory; +static jclass gStringClass = nullptr; + +//////////////////////////////////////////////////////////////////////////////// + +template +jobjectArray to_java_string_array(JNIEnv* env, + const std::vector& 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 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 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 lock(gMutex); + test = gUnitTests[index]; + } + std::vector 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); +} + +//////////////////////////////////////////////////////////////////////////////// + -- cgit v1.2.3