1 // Copyright 2012 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.native_test; 6 7 import android.app.Activity; 8 import android.content.Context; 9 import android.content.Intent; 10 import android.net.http.HttpEngine; 11 import android.os.Build; 12 import android.os.Bundle; 13 import android.os.Environment; 14 import android.os.Handler; 15 import android.os.Looper; 16 import android.os.Process; 17 import android.system.ErrnoException; 18 import android.system.Os; 19 20 import org.chromium.base.JNIUtils; 21 import org.chromium.base.Log; 22 import org.chromium.base.annotations.JNINamespace; 23 import org.chromium.base.annotations.NativeMethods; 24 import org.chromium.base.test.util.UrlUtils; 25 import org.chromium.build.gtest_apk.NativeTestIntent; 26 import org.chromium.test.reporter.TestStatusReporter; 27 28 import java.io.File; 29 30 /** 31 * Helper to run tests inside Activity or NativeActivity. 32 */ 33 @JNINamespace("testing::android") 34 public class NativeTest { 35 private static final String TAG = "NativeTest"; 36 37 private String mCommandLineFilePath; 38 private StringBuilder mCommandLineFlags = new StringBuilder(); 39 private TestStatusReporter mReporter; 40 private boolean mRunInSubThread; 41 private String mStdoutFilePath; 42 private static final String LOG_TAG = "NativeTestRunner"; 43 // Signal used to trigger a dump of Clang coverage information. 44 private final int COVERAGE_SIGNAL = 37; 45 private boolean mDumpCoverage = false; 46 private static final String DUMP_COVERAGE = 47 "org.chromium.native_test.NativeTestInstrumentationTestRunner.DumpCoverage"; 48 49 private static class ReportingUncaughtExceptionHandler 50 implements Thread.UncaughtExceptionHandler { 51 52 private TestStatusReporter mReporter; 53 private Thread.UncaughtExceptionHandler mWrappedHandler; 54 ReportingUncaughtExceptionHandler(TestStatusReporter reporter, Thread.UncaughtExceptionHandler wrappedHandler)55 public ReportingUncaughtExceptionHandler(TestStatusReporter reporter, 56 Thread.UncaughtExceptionHandler wrappedHandler) { 57 mReporter = reporter; 58 mWrappedHandler = wrappedHandler; 59 } 60 61 @Override uncaughtException(Thread thread, Throwable ex)62 public void uncaughtException(Thread thread, Throwable ex) { 63 mReporter.uncaughtException(Process.myPid(), ex); 64 if (mWrappedHandler != null) mWrappedHandler.uncaughtException(thread, ex); 65 } 66 } 67 68 /** 69 * This method is called on cronet so it needs to support at least Kitkat (API 19). See this 70 * CL for context: https://crrev.com/c/3198091. 71 */ preCreate(Activity activity)72 public void preCreate(Activity activity) { 73 String coverageDeviceFile = 74 activity.getIntent().getStringExtra(NativeTestIntent.EXTRA_COVERAGE_DEVICE_FILE); 75 if (coverageDeviceFile != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 76 try { 77 Os.setenv("LLVM_PROFILE_FILE", coverageDeviceFile, true); 78 } catch (Exception e) { 79 Log.w(TAG, "failed to set LLVM_PROFILE_FILE", e); 80 } 81 } 82 // To use Os.setenv, need to check Android API level, because it requires API level 21 and 83 // Kitkat (API 19) doesn't match. See crbug.com/1042122. 84 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 85 // Set TMPDIR to make perfetto_unittests not to use /data/local/tmp as a tmp directory. 86 try { 87 Os.setenv( 88 "TMPDIR", activity.getApplicationContext().getCacheDir().getPath(), false); 89 } catch (Exception e) { 90 // Need to use Exception for Android Kitkat, because Kitkat doesn't know 91 // ErrnoException is an exception class. When dalvikvm(Kitkat) verifies preCreate 92 // method, it finds that unknown method:Os.setenv is used without any exception 93 // class. So dalvikvm rejects preCreate method and also rejects NativeClass. All 94 // native tests will crash. The verification is executed before running preCreate. 95 // The above Build.VERSION check doesn't work to avoid the crash. 96 Log.w(TAG, "failed to set TMPDIR", e); 97 } 98 } 99 } 100 postCreate(Activity activity)101 public void postCreate(Activity activity) { 102 parseArgumentsFromIntent(activity, activity.getIntent()); 103 mReporter = new TestStatusReporter(activity); 104 mReporter.testRunStarted(Process.myPid()); 105 Thread.setDefaultUncaughtExceptionHandler( 106 new ReportingUncaughtExceptionHandler(mReporter, 107 Thread.getDefaultUncaughtExceptionHandler())); 108 } 109 parseArgumentsFromIntent(Activity activity, Intent intent)110 private void parseArgumentsFromIntent(Activity activity, Intent intent) { 111 Log.i(TAG, "Extras:"); 112 Bundle extras = intent.getExtras(); 113 if (extras != null) { 114 for (String s : extras.keySet()) { 115 Log.i(TAG, " %s", s); 116 } 117 } 118 119 mCommandLineFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FILE); 120 if (mCommandLineFilePath == null) { 121 mCommandLineFilePath = ""; 122 } else { 123 File commandLineFile = new File(mCommandLineFilePath); 124 if (!commandLineFile.isAbsolute()) { 125 mCommandLineFilePath = Environment.getExternalStorageDirectory() + "/" 126 + mCommandLineFilePath; 127 } 128 Log.i(TAG, "command line file path: %s", mCommandLineFilePath); 129 } 130 131 String commandLineFlags = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FLAGS); 132 if (commandLineFlags != null) mCommandLineFlags.append(commandLineFlags); 133 134 mRunInSubThread = intent.hasExtra(NativeTestIntent.EXTRA_RUN_IN_SUB_THREAD); 135 136 String gtestFilter = intent.getStringExtra(NativeTestIntent.EXTRA_GTEST_FILTER); 137 if (gtestFilter != null) { 138 appendCommandLineFlags("--gtest_filter=" + gtestFilter); 139 } 140 141 mStdoutFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_STDOUT_FILE); 142 mDumpCoverage = intent.hasExtra(DUMP_COVERAGE); 143 } 144 appendCommandLineFlags(String flags)145 public void appendCommandLineFlags(String flags) { 146 mCommandLineFlags.append(" ").append(flags); 147 } 148 postStart(final Activity activity, boolean forceRunInSubThread)149 public void postStart(final Activity activity, boolean forceRunInSubThread) { 150 final Runnable runTestsTask = new Runnable() { 151 @Override 152 public void run() { 153 runTests(activity); 154 } 155 }; 156 157 if (mRunInSubThread || forceRunInSubThread) { 158 // Post a task that posts a task that creates a new thread and runs tests on it. 159 160 // On L and M, the system posts a task to the main thread that prints to stdout 161 // from android::Layout (https://goo.gl/vZA38p). Chaining the subthread creation 162 // through multiple tasks executed on the main thread ensures that this task 163 // runs before we start running tests s.t. its output doesn't interfere with 164 // the test output. See crbug.com/678146 for additional context. 165 166 final Handler handler = new Handler(); 167 final Runnable startTestThreadTask = new Runnable() { 168 @Override 169 public void run() { 170 new Thread(runTestsTask).start(); 171 } 172 }; 173 final Runnable postTestStarterTask = new Runnable() { 174 @Override 175 public void run() { 176 handler.post(startTestThreadTask); 177 } 178 }; 179 handler.post(postTestStarterTask); 180 } else { 181 // Post a task to run the tests. This allows us to not block 182 // onCreate and still run tests on the main thread. 183 new Handler().post(runTestsTask); 184 } 185 } 186 runTests(Activity activity)187 private void runTests(Activity activity) { 188 // Chromium pushes data to "chrome/test/data/chromium_test_root". But in AOSP, it's more 189 // common to push testing data to data/local/tmp. 190 // This has to be consistent with the AndroidTest configuration file. 191 NativeTestJni.get().runTests(mCommandLineFlags.toString(), mCommandLineFilePath, 192 mStdoutFilePath, activity.getApplicationContext(), "/data/local/tmp"); 193 if (mDumpCoverage) { 194 new Handler(Looper.getMainLooper()).post(() -> { 195 maybeDumpNativeCoverage(); 196 activity.finish(); 197 mReporter.testRunFinished(Process.myPid()); 198 }); 199 } else { 200 activity.finish(); 201 mReporter.testRunFinished(Process.myPid()); 202 } 203 } 204 205 /** 206 * If this test process is instrumented for native coverage, then trigger a dump 207 * of the coverage data and wait until either we detect the dumping has finished or 60 seconds, 208 * whichever is shorter. 209 * 210 * Background: Coverage builds install a signal handler for signal 37 which flushes coverage 211 * data to disk, which may take a few seconds. Tests running as an app process will get 212 * killed with SIGKILL once the app code exits, even if the coverage handler is still running. 213 * 214 * Method: If a handler is installed for signal 37, then assume this is a coverage run and 215 * send signal 37. The handler is non-reentrant and so signal 37 will then be blocked until 216 * the handler completes. So after we send the signal, we loop checking the blocked status 217 * for signal 37 until we hit the 60 second deadline. If the signal is blocked then sleep for 218 * 2 seconds, and if it becomes unblocked then the handler exitted so we can return early. 219 * If the signal is not blocked at the start of the loop then most likely the handler has 220 * not yet been invoked. This should almost never happen as it should get blocked on delivery 221 * when we call {@code Os.kill()}, so sleep for a shorter duration (100ms) and try again. There 222 * is a race condition here where the handler is delayed but then runs for less than 100ms and 223 * gets missed, in which case this method will loop with 100ms sleeps until the deadline. 224 * 225 * In the case where the handler runs for more than 60 seconds, the test process will be allowed 226 * to exit so coverage information may be incomplete. 227 * 228 * There is no API for determining signal dispositions, so this method uses the 229 * {@link SignalMaskInfo} class to read the data from /proc. If there is an error parsing 230 * the /proc data then this method will also loop until the 60s deadline passes. 231 */ maybeDumpNativeCoverage()232 private void maybeDumpNativeCoverage() { 233 SignalMaskInfo siginfo = new SignalMaskInfo(); 234 if (!siginfo.isValid()) { 235 Log.e(LOG_TAG, "Invalid signal info"); 236 return; 237 } 238 239 if (!siginfo.isCaught(COVERAGE_SIGNAL)) { 240 // Process is not instrumented for coverage 241 Log.i(LOG_TAG, "Not dumping coverage, no handler installed"); 242 return; 243 } 244 245 Log.i(LOG_TAG, 246 String.format("Sending coverage dump signal %d to pid %d uid %d", COVERAGE_SIGNAL, 247 Os.getpid(), Os.getuid())); 248 try { 249 Os.kill(Os.getpid(), COVERAGE_SIGNAL); 250 } catch (ErrnoException e) { 251 Log.e(LOG_TAG, "Unable to send coverage signal", e); 252 return; 253 } 254 255 long start = System.currentTimeMillis(); 256 long deadline = start + 60 * 1000L; 257 while (System.currentTimeMillis() < deadline) { 258 siginfo.refresh(); 259 try { 260 if (siginfo.isValid() && siginfo.isBlocked(COVERAGE_SIGNAL)) { 261 // Signal is currently blocked so assume a handler is running 262 Thread.sleep(2000L); 263 siginfo.refresh(); 264 if (siginfo.isValid() && !siginfo.isBlocked(COVERAGE_SIGNAL)) { 265 // Coverage handler exited while we were asleep 266 Log.i(LOG_TAG, 267 String.format("Coverage dump detected finished after %dms", 268 System.currentTimeMillis() - start)); 269 break; 270 } 271 } else { 272 // Coverage signal handler not yet started or invalid siginfo 273 Thread.sleep(100L); 274 } 275 } catch (InterruptedException e) { 276 // ignored 277 } 278 } 279 } 280 281 @NativeMethods 282 interface Natives { runTests(String commandLineFlags, String commandLineFilePath, String stdoutFilePath, Context appContext, String testDataDir)283 void runTests(String commandLineFlags, String commandLineFilePath, String stdoutFilePath, 284 Context appContext, String testDataDir); 285 } 286 } 287