Commit bf0c6ae1 by Peter Kotwicz Committed by Commit Bot

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/+/2745535Reviewed-by: 's avatarJamie Madill <jmadill@chromium.org> Reviewed-by: 's avatarGeoff Lang <geofflang@chromium.org> Commit-Queue: Jamie Madill <jmadill@chromium.org>
parent 2ef1e0fc
...@@ -99,8 +99,8 @@ angle_libs_suffix = "_ANGLE_DEV" ...@@ -99,8 +99,8 @@ angle_libs_suffix = "_ANGLE_DEV"
``` ```
All All
[NativeTest](https://chromium.googlesource.com/chromium/src/+/master/testing/android/native_test/java/src/org/chromium/native_test/NativeTest.java) [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, `org.chromium.native_test.NativeUnitTestNativeActivity`. 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. 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, 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 `./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. ...@@ -109,14 +109,14 @@ launcher once.
In GAPID's "Capture Trace" dialog, "Package / Action:" should be: 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 The mandatory [extra intent
argument](https://developer.android.com/studio/command-line/adb.html#IntentSpec) for starting the 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 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 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: GAPID's "Capture Trace" dialog:
``` ```
...@@ -126,7 +126,7 @@ GAPID's "Capture Trace" dialog: ...@@ -126,7 +126,7 @@ GAPID's "Capture Trace" dialog:
or 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 ## Running ANGLE under RenderDoc
......
...@@ -420,8 +420,13 @@ if (angle_standalone || build_with_chromium) { ...@@ -420,8 +420,13 @@ if (angle_standalone || build_with_chromium) {
if (is_android) { if (is_android) {
public_configs += [ "$angle_root:build_id_config" ] public_configs += [ "$angle_root:build_id_config" ]
if (build_with_chromium) { if (build_with_chromium) {
use_native_activity = true
configs -= [ "//build/config/android:hide_all_but_jni" ] 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 { } else {
use_raw_android_executable = true use_raw_android_executable = true
} }
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "string_utils.h" #include "string_utils.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
using namespace angle; using namespace angle;
...@@ -92,6 +93,46 @@ TEST(StringUtilsTest, SplitString_WhitespaceAndResultType) ...@@ -92,6 +93,46 @@ TEST(StringUtilsTest, SplitString_WhitespaceAndResultType)
ASSERT_TRUE(r.empty()); 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 // Tests for TrimString
TEST(StringUtilsTest, TrimString) TEST(StringUtilsTest, TrimString)
{ {
......
...@@ -16,6 +16,28 @@ declare_args() { ...@@ -16,6 +16,28 @@ declare_args() {
is_win || is_linux || is_chromeos || is_android || is_apple || is_fuchsia 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") { angle_test("test_utils_unittest_helper") {
sources = [ sources = [
"../../util/test_utils_unittest_helper.cpp", "../../util/test_utils_unittest_helper.cpp",
......
// 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]);
}
<?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>
// 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);
}
// 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);
}
}
// 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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment