• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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