/* * Copyright 2024 The WebRTC 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 in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ // Based on Chromium's https://source.chromium.org/chromium/chromium/src/+/main:testing/android/native_test/java/src/org/chromium/native_test/NativeTest.java // and Angle's https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/tests/test_utils/runner/android/java/src/com/android/angle/test/AngleNativeTest.java package org.webrtc.native_test; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Process; import android.system.ErrnoException; import android.system.Os; import android.util.Log; import org.jni_zero.JNINamespace; import org.jni_zero.JniType; import org.jni_zero.NativeMethods; import org.webrtc.native_test.StrictModeContext; import org.chromium.build.gtest_apk.NativeTestIntent; import org.chromium.test.reporter.TestStatusReporter; import java.io.File; /** Helper to run tests inside Activity or NativeActivity. */ @JNINamespace("webrtc::test::android") public class NativeTestWebrtc { private static final String TAG = "NativeTestWebrtc"; private String mCommandLineFilePath; private StringBuilder mCommandLineFlags = new StringBuilder(); private TestStatusReporter mReporter; private boolean mRunInSubThread; 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 preCreate(Activity activity) { String coverageDeviceFile = activity.getIntent().getStringExtra(NativeTestIntent.EXTRA_COVERAGE_DEVICE_FILE); if (coverageDeviceFile != null) { try { Os.setenv("LLVM_PROFILE_FILE", coverageDeviceFile, true); } catch (ErrnoException e) { Log.w(TAG, "failed to set LLVM_PROFILE_FILE" + e.toString()); } } // Set TMPDIR to make perfetto_unittests not to use /data/local/tmp as a tmp directory. try { Os.setenv("TMPDIR", activity.getApplicationContext().getCacheDir().getPath(), false); } catch (ErrnoException e) { Log.w(TAG, "failed to set TMPDIR" + e.toString()); } } 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); mRunInSubThread = intent.hasExtra(NativeTestIntent.EXTRA_RUN_IN_SUB_THREAD); String gtestFilter = intent.getStringExtra(NativeTestIntent.EXTRA_GTEST_FILTER); if (gtestFilter != null) { appendCommandLineFlags("--gtest_filter=" + gtestFilter); } mStdoutFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_STDOUT_FILE); } public void appendCommandLineFlags(String flags) { mCommandLineFlags.append(" ").append(flags); } public void postStart(final Activity activity, boolean forceRunInSubThread) { final Runnable runTestsTask = new Runnable() { @Override public void run() { runTests(activity); } }; if (mRunInSubThread || forceRunInSubThread) { // Post a task that posts a task that creates a new thread and runs tests on it. // 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); } else { // Post a task to run the tests. This allows us to not block // onCreate and still run tests on the main thread. new Handler().post(runTestsTask); } } private void runTests(Activity activity) { Natives jni = NativeTestWebrtcJni.get(); String isolated_test_root = NativeTestWebrtc.getIsolatedTestRoot(); Log.i(TAG, "Calling into native code"); jni.runTests( mCommandLineFlags.toString(), mCommandLineFilePath, mStdoutFilePath, isolated_test_root); Log.i(TAG, "Call into native code returned"); activity.finish(); mReporter.testRunFinished(Process.myPid()); } // @CalledByNative public static String getIsolatedTestRoot() { try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { return Environment.getExternalStorageDirectory().getAbsolutePath() + "/chromium_tests_root"; } } // 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. @SuppressWarnings("UnusedMethod") private void nativeTestFailed() { Log.e(TAG, "[ RUNNER_FAILED ] could not load native library"); } @NativeMethods interface Natives { void runTests( @JniType("std::string") String commandLineFlags, @JniType("std::string") String commandLineFilePath, @JniType("std::string") String stdoutFilePath, @JniType("std::string") String testDataDir); } }