Improve Angle Android gtest support

This CL makes angle_test() use an angle-specific gtest launcher -
AngleUnitTestActivity.java. This enables building standalone Android
angle without //base

Chromium-Bug: 1149922
Bug: None
Change-Id: Id84f3e2bd84c5017ed1988ca07534f424ebfa596
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/2745535
Reviewed-by: Jamie Madill <jmadill@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Jamie Madill <jmadill@chromium.org>
This commit is contained in:
Peter Kotwicz
2021-03-09 13:21:58 -05:00
committed by Commit Bot
parent 2ef1e0fc7d
commit bf0c6ae1ee
9 changed files with 552 additions and 6 deletions

View File

@@ -99,8 +99,8 @@ angle_libs_suffix = "_ANGLE_DEV"
```
All
[NativeTest](https://chromium.googlesource.com/chromium/src/+/master/testing/android/native_test/java/src/org/chromium/native_test/NativeTest.java)
based tests share the same activity name, `org.chromium.native_test.NativeUnitTestNativeActivity`.
[AngleNativeTest](https://chromium.googlesource.com/chromium/src/+/master/third_party/angle/src/tests/test_utils/runner/android/java/src/com/android/angle/test/AngleNativeTest.java)
based tests share the same activity name, `com.android.angle.test.AngleUnitTestActivity`.
Thus, prior to capturing your test trace, the specific test APK must be installed on the device.
When you build the test, a test launcher is generated, for example,
`./out/Release/bin/run_angle_end2end_tests`. The best way to install the APK is to run this test
@@ -109,14 +109,14 @@ launcher once.
In GAPID's "Capture Trace" dialog, "Package / Action:" should be:
```
android.intent.action.MAIN:org.chromium.native_test/org.chromium.native_test.NativeUnitTestNativeActivity
android.intent.action.MAIN:com.android.angle.test/com.android.angle.test.AngleUnitTestActivity
```
The mandatory [extra intent
argument](https://developer.android.com/studio/command-line/adb.html#IntentSpec) for starting the
activity is `org.chromium.native_test.NativeTest.StdoutFile`. Without it the test APK crashes. Test
filters can be specified via either the `org.chromium.native_test.NativeTest.CommandLineFlags` or
the `org.chromium.native_test.NativeTest.Shard` argument. Example "Intent Arguments:" values in
the `org.chromium.native_test.NativeTest.GtestFilter` argument. Example "Intent Arguments:" values in
GAPID's "Capture Trace" dialog:
```
@@ -126,7 +126,7 @@ GAPID's "Capture Trace" dialog:
or
```
-e org.chromium.native_test.NativeTest.StdoutFile /sdcard/chromium_tests_root/out.txt --esal org.chromium.native_test.NativeTest.Shard RendererTest.SimpleOperation/ES2_VULKAN,SimpleOperationTest.DrawWithTexture/ES2_VULKAN
-e org.chromium.native_test.NativeTest.StdoutFile /sdcard/chromium_tests_root/out.txt --e org.chromium.native_test.NativeTest.GtestFilter RendererTest.SimpleOperation/ES2_VULKAN:SimpleOperationTest.DrawWithTexture/ES2_VULKAN
```
## Running ANGLE under RenderDoc

View File

@@ -420,8 +420,13 @@ if (angle_standalone || build_with_chromium) {
if (is_android) {
public_configs += [ "$angle_root:build_id_config" ]
if (build_with_chromium) {
use_native_activity = true
configs -= [ "//build/config/android:hide_all_but_jni" ]
use_default_launcher = false
android_manifest_template = "$angle_root/src/tests/test_utils/runner/android/java/AndroidManifest.xml.jinja2"
deps += [
"$angle_root/src/tests:native_test_java",
"$angle_root/src/tests:native_test_support_android",
]
} else {
use_raw_android_executable = true
}

View File

@@ -9,6 +9,7 @@
#include "string_utils.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace angle;
@@ -92,6 +93,46 @@ TEST(StringUtilsTest, SplitString_WhitespaceAndResultType)
ASSERT_TRUE(r.empty());
}
// Tests for SplitStringAlongWhitespace
TEST(StringUtilsTest, SplitStringAlongWhitespace)
{
{
// No whitespace.
std::vector<std::string> r;
SplitStringAlongWhitespace("abcd", &r);
ASSERT_THAT(r, testing::ElementsAre("abcd"));
}
{
// Just whitespace.
std::vector<std::string> r;
SplitStringAlongWhitespace(" \t", &r);
ASSERT_THAT(r, testing::ElementsAre());
}
{
// Consecutive whitespace of same type.
std::vector<std::string> r;
SplitStringAlongWhitespace("a b", &r);
ASSERT_THAT(r, testing::ElementsAre("a", "b"));
}
{
// Consecutive whitespace of different types.
std::vector<std::string> r;
SplitStringAlongWhitespace("ab \tcd", &r);
ASSERT_THAT(r, testing::ElementsAre("ab", "cd"));
}
{
// Non-empty output std::vector.
std::vector<std::string> r;
r.push_back("z");
SplitStringAlongWhitespace("abc", &r);
ASSERT_THAT(r, testing::ElementsAre("z", "abc"));
}
}
// Tests for TrimString
TEST(StringUtilsTest, TrimString)
{

View File

@@ -16,6 +16,28 @@ declare_args() {
is_win || is_linux || is_chromeos || is_android || is_apple || is_fuchsia
}
if (is_android && build_with_chromium) {
android_library("native_test_java") {
testonly = true
sources = [
"test_utils/runner/android/java/src/com/android/angle/test/AngleNativeTest.java",
"test_utils/runner/android/java/src/com/android/angle/test/AngleUnitTestActivity.java",
"test_utils/runner/android/java/src/com/android/angle/test/TestStatusReporter.java",
]
deps = [
"//build/android:native_libraries_java",
"//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
]
}
angle_source_set("native_test_support_android") {
testonly = true
sources = [ "test_utils/runner/android/AngleNativeTest.cpp" ]
deps = [ "$angle_root:angle_common" ]
}
}
angle_test("test_utils_unittest_helper") {
sources = [
"../../util/test_utils_unittest_helper.cpp",

View File

@@ -0,0 +1,160 @@
// Copyright 2021 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// angle_native_test:
// Contains native implementation for com.android.angle.test.AngleNativeTest.
#include <jni.h>
#include <vector>
#include <android/log.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include "common/string_utils.h"
// The main function of the program to be wrapped as a test apk.
extern int main(int argc, char **argv);
namespace
{
const char kLogTag[] = "chromium";
const char kCrashedMarker[] = "[ CRASHED ]\n";
// The list of signals which are considered to be crashes.
const int kExceptionSignals[] = {SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, -1};
struct sigaction g_old_sa[NSIG];
class ScopedMainEntryLogger
{
public:
ScopedMainEntryLogger() { printf(">>ScopedMainEntryLogger\n"); }
~ScopedMainEntryLogger()
{
printf("<<ScopedMainEntryLogger\n");
fflush(stdout);
fflush(stderr);
}
};
// This function runs in a compromised context. It should not allocate memory.
void SignalHandler(int sig, siginfo_t *info, void *reserved)
{
// Output the crash marker.
write(STDOUT_FILENO, kCrashedMarker, sizeof(kCrashedMarker) - 1);
g_old_sa[sig].sa_sigaction(sig, info, reserved);
}
std::string ASCIIJavaStringToUTF8(JNIEnv *env, jstring str)
{
if (!str)
{
return "";
}
const jsize length = env->GetStringLength(str);
if (!length)
{
return "";
}
// JNI's GetStringUTFChars() returns strings in Java "modified" UTF8, so
// instead get the String in UTF16. As the input is ASCII, drop the higher
// bytes.
const jchar *jchars = env->GetStringChars(str, NULL);
const char16_t *chars = reinterpret_cast<const char16_t *>(jchars);
std::string out(chars, chars + length);
env->ReleaseStringChars(str, jchars);
return out;
}
size_t ArgsToArgv(const std::vector<std::string> &args, std::vector<char *> *argv)
{
// We need to pass in a non-const char**.
size_t argc = args.size();
argv->resize(argc + 1);
for (size_t i = 0; i < argc; ++i)
{
(*argv)[i] = const_cast<char *>(args[i].c_str());
}
(*argv)[argc] = NULL; // argv must be NULL terminated.
return argc;
}
void InstallExceptionHandlers()
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_SIGINFO;
for (unsigned int i = 0; kExceptionSignals[i] != -1; ++i)
{
sigaction(kExceptionSignals[i], &sa, &g_old_sa[kExceptionSignals[i]]);
}
}
void AndroidLog(int priority, const char *format, ...)
{
va_list args;
va_start(args, format);
__android_log_vprint(priority, kLogTag, format, args);
va_end(args);
}
} // anonymous namespace
extern "C" JNIEXPORT void JNICALL
Java_com_android_angle_test_AngleNativeTest_nativeRunTests(JNIEnv *env,
jclass clazz,
jstring jcommandLineFlags,
jstring jcommandLineFilePath,
jstring jstdoutFilePath)
{
InstallExceptionHandlers();
const std::string commandLineFlags(ASCIIJavaStringToUTF8(env, jcommandLineFlags));
const std::string commandLineFilePath(ASCIIJavaStringToUTF8(env, jcommandLineFilePath));
const std::string stdoutFilePath(ASCIIJavaStringToUTF8(env, jstdoutFilePath));
std::vector<std::string> args;
if (commandLineFilePath.empty())
{
args.push_back("_");
}
else
{
std::string commandLineString;
if (angle::ReadFileToString(commandLineFilePath, &commandLineString))
{
angle::SplitStringAlongWhitespace(commandLineString, &args);
}
}
angle::SplitStringAlongWhitespace(commandLineFlags, &args);
// A few options, such "--gtest_list_tests", will just use printf directly
// Always redirect stdout to a known file.
if (freopen(stdoutFilePath.c_str(), "a+", stdout) == NULL)
{
AndroidLog(ANDROID_LOG_ERROR, "Failed to redirect stream to file: %s: %s\n",
stdoutFilePath.c_str(), strerror(errno));
exit(EXIT_FAILURE);
}
dup2(STDOUT_FILENO, STDERR_FILENO);
std::vector<char *> argv;
size_t argc = ArgsToArgv(args, &argv);
ScopedMainEntryLogger scoped_main_entry_logger;
main(static_cast<int>(argc), &argv[0]);
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The ANGLE Project Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.angle.test"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SET_TIME_ZONE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Explicitly set the attribute requestLegacyExternalStorage to "true"
since it is "false" by default on apps targeting Android 10, and that
breaks test listing. See
https://developer.android.com/training/data-storage#scoped-storage and
https://developer.android.com/training/data-storage/compatibility. -->
<application android:label="NativeTests"
android:requestLegacyExternalStorage="true">
<uses-library android:name="android.test.runner"/>
<activity android:name=".AngleUnitTestActivity"
android:label="NativeTest"
android:configChanges="orientation|keyboardHidden"
android:process=":test_process">
{% if is_component_build == 'true' %}
<meta-data android:name="android.app.lib_name"
android:value="{{ native_library_name }}.cr" />
{% else %}
<meta-data android:name="android.app.lib_name"
android:value="{{ native_library_name }}" />
{% endif %}
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<instrumentation android:name="org.chromium.build.gtest_apk.NativeTestInstrumentationTestRunner"
android:targetPackage="com.android.angle.test"
android:label="Instrumentation entry point for com.android.angle.test"
chromium-junit3="true"/>
</manifest>

View File

@@ -0,0 +1,165 @@
// Copyright 2021 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// AngleNativeTest:
// Helper to run Angle tests inside NativeActivity.
package com.android.angle.test;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Process;
import android.system.Os;
import android.util.Log;
import org.chromium.build.gtest_apk.NativeTestIntent;
import java.io.File;
public class AngleNativeTest
{
private static final String TAG = "NativeTest";
private String mCommandLineFilePath;
private StringBuilder mCommandLineFlags = new StringBuilder();
private TestStatusReporter mReporter;
private String mStdoutFilePath;
private static class ReportingUncaughtExceptionHandler
implements Thread.UncaughtExceptionHandler
{
private TestStatusReporter mReporter;
private Thread.UncaughtExceptionHandler mWrappedHandler;
public ReportingUncaughtExceptionHandler(
TestStatusReporter reporter, Thread.UncaughtExceptionHandler wrappedHandler)
{
mReporter = reporter;
mWrappedHandler = wrappedHandler;
}
@Override
public void uncaughtException(Thread thread, Throwable ex)
{
mReporter.uncaughtException(Process.myPid(), ex);
if (mWrappedHandler != null) mWrappedHandler.uncaughtException(thread, ex);
}
}
public void postCreate(Activity activity)
{
parseArgumentsFromIntent(activity, activity.getIntent());
mReporter = new TestStatusReporter(activity);
mReporter.testRunStarted(Process.myPid());
Thread.setDefaultUncaughtExceptionHandler(new ReportingUncaughtExceptionHandler(
mReporter, Thread.getDefaultUncaughtExceptionHandler()));
}
private void parseArgumentsFromIntent(Activity activity, Intent intent)
{
Log.i(TAG, "Extras:");
Bundle extras = intent.getExtras();
if (extras != null)
{
for (String s : extras.keySet())
{
Log.i(TAG, " " + s);
}
}
mCommandLineFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FILE);
if (mCommandLineFilePath == null)
{
mCommandLineFilePath = "";
}
else
{
File commandLineFile = new File(mCommandLineFilePath);
if (!commandLineFile.isAbsolute())
{
mCommandLineFilePath =
Environment.getExternalStorageDirectory() + "/" + mCommandLineFilePath;
}
Log.i(TAG, "command line file path: " + mCommandLineFilePath);
}
String commandLineFlags = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FLAGS);
if (commandLineFlags != null) mCommandLineFlags.append(commandLineFlags);
String gtestFilter = intent.getStringExtra(NativeTestIntent.EXTRA_GTEST_FILTER);
if (gtestFilter != null)
{
appendCommandLineFlags("--gtest_filter=" + gtestFilter);
}
mStdoutFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_STDOUT_FILE);
}
private void appendCommandLineFlags(String flags)
{
mCommandLineFlags.append(" ").append(flags);
}
public void postStart(final Activity activity)
{
final Runnable runTestsTask = new Runnable() {
@Override
public void run()
{
runTests(activity);
}
};
// Post a task that posts a task that creates a new thread and runs tests on it.
// This is needed because NativeActivity processes Looper messages in native code code,
// which makes invoking the test runner Handler problematic.
// On L and M, the system posts a task to the main thread that prints to stdout
// from android::Layout (https://goo.gl/vZA38p). Chaining the subthread creation
// through multiple tasks executed on the main thread ensures that this task
// runs before we start running tests s.t. its output doesn't interfere with
// the test output. See crbug.com/678146 for additional context.
final Handler handler = new Handler();
final Runnable startTestThreadTask = new Runnable() {
@Override
public void run()
{
new Thread(runTestsTask).start();
}
};
final Runnable postTestStarterTask = new Runnable() {
@Override
public void run()
{
handler.post(startTestThreadTask);
}
};
handler.post(postTestStarterTask);
}
private void runTests(Activity activity)
{
nativeRunTests(mCommandLineFlags.toString(), mCommandLineFilePath, mStdoutFilePath);
activity.finish();
mReporter.testRunFinished(Process.myPid());
}
// Signal a failure of the native test loader to python scripts
// which run tests. For example, we look for
// RUNNER_FAILED build/android/test_package.py.
private void nativeTestFailed()
{
Log.e(TAG, "[ RUNNER_FAILED ] could not load native library");
}
private native void nativeRunTests(
String commandLineFlags, String commandLineFilePath, String stdoutFilePath);
}

View File

@@ -0,0 +1,45 @@
// Copyright 2021 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// AngleUnitTestActivity:
// A {@link android.app.NativeActivity} for running angle gtests.
package com.android.angle.test;
import android.app.NativeActivity;
import android.os.Bundle;
import android.util.Log;
import org.chromium.build.NativeLibraries;
public class AngleUnitTestActivity extends NativeActivity
{
private static final String TAG = "NativeTest";
private AngleNativeTest mTest = new AngleNativeTest();
@Override
public void onCreate(Bundle savedInstanceState)
{
// For NativeActivity based tests,
// dependency libraries must be loaded before NativeActivity::OnCreate,
// otherwise loading android.app.lib_name will fail
for (String library : NativeLibraries.LIBRARIES)
{
Log.i(TAG, "loading: " + library);
System.loadLibrary(library);
Log.i(TAG, "loaded: " + library);
}
super.onCreate(savedInstanceState);
mTest.postCreate(this);
}
@Override
public void onStart()
{
super.onStart();
mTest.postStart(this);
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2021 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// TestStatusReporter:
// Broadcasts test status to any listening {@link org.chromium.test.reporter.TestStatusReceiver}.
package com.android.angle.test;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.chromium.build.gtest_apk.TestStatusIntent;
public class TestStatusReporter
{
private final Context mContext;
public TestStatusReporter(Context c)
{
mContext = c;
}
public void testRunStarted(int pid)
{
sendTestRunBroadcast(TestStatusIntent.ACTION_TEST_RUN_STARTED, pid);
}
public void testRunFinished(int pid)
{
sendTestRunBroadcast(TestStatusIntent.ACTION_TEST_RUN_FINISHED, pid);
}
private void sendTestRunBroadcast(String action, int pid)
{
Intent i = new Intent(action);
i.setType(TestStatusIntent.DATA_TYPE_RESULT);
i.putExtra(TestStatusIntent.EXTRA_PID, pid);
mContext.sendBroadcast(i);
}
public void uncaughtException(int pid, Throwable ex)
{
Intent i = new Intent(TestStatusIntent.ACTION_UNCAUGHT_EXCEPTION);
i.setType(TestStatusIntent.DATA_TYPE_RESULT);
i.putExtra(TestStatusIntent.EXTRA_PID, pid);
i.putExtra(TestStatusIntent.EXTRA_STACK_TRACE, Log.getStackTraceString(ex));
mContext.sendBroadcast(i);
}
}