1 // Copyright 2014 The Chromium Authors. All rights reserved.
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.annotation.SuppressLint;
8 import android.app.Activity;
9 import android.app.ActivityManager;
10 import android.app.Instrumentation;
11 import android.content.ComponentName;
12 import android.content.Context;
13 import android.content.Intent;
14 import android.os.Bundle;
15 import android.os.Environment;
16 import android.os.Handler;
17 import android.os.Process;
18 import android.util.SparseArray;
19 
20 import org.chromium.base.Log;
21 import org.chromium.test.reporter.TestStatusReceiver;
22 
23 import java.io.BufferedReader;
24 import java.io.File;
25 import java.io.FileReader;
26 import java.io.IOException;
27 import java.util.ArrayDeque;
28 import java.util.ArrayList;
29 import java.util.Queue;
30 import java.util.concurrent.atomic.AtomicBoolean;
31 
32 /**
33  *  An Instrumentation that runs tests based on NativeTest.
34  */
35 public class NativeTestInstrumentationTestRunner extends Instrumentation {
36 
37     public static final String EXTRA_NATIVE_TEST_ACTIVITY =
38             "org.chromium.native_test.NativeTestInstrumentationTestRunner.NativeTestActivity";
39     public static final String EXTRA_SHARD_NANO_TIMEOUT =
40             "org.chromium.native_test.NativeTestInstrumentationTestRunner.ShardNanoTimeout";
41     public static final String EXTRA_SHARD_SIZE_LIMIT =
42             "org.chromium.native_test.NativeTestInstrumentationTestRunner.ShardSizeLimit";
43     public static final String EXTRA_STDOUT_FILE =
44             "org.chromium.native_test.NativeTestInstrumentationTestRunner.StdoutFile";
45     public static final String EXTRA_TEST_LIST_FILE =
46             "org.chromium.native_test.NativeTestInstrumentationTestRunner.TestList";
47     public static final String EXTRA_TEST =
48             "org.chromium.native_test.NativeTestInstrumentationTestRunner.Test";
49 
50     private static final String TAG = "NativeTest";
51 
52     private static final long DEFAULT_SHARD_NANO_TIMEOUT = 60 * 1000000000L;
53     // Default to no size limit.
54     private static final int DEFAULT_SHARD_SIZE_LIMIT = 0;
55     private static final String DEFAULT_NATIVE_TEST_ACTIVITY =
56             "org.chromium.native_test.NativeUnitTestActivity";
57 
58     private Handler mHandler = new Handler();
59     private Bundle mLogBundle = new Bundle();
60     private SparseArray<ShardMonitor> mMonitors = new SparseArray<ShardMonitor>();
61     private String mNativeTestActivity;
62     private TestStatusReceiver mReceiver;
63     private Queue<ArrayList<String>> mShards = new ArrayDeque<ArrayList<String>>();
64     private long mShardNanoTimeout = DEFAULT_SHARD_NANO_TIMEOUT;
65     private int mShardSizeLimit = DEFAULT_SHARD_SIZE_LIMIT;
66     private File mStdoutFile;
67     private Bundle mTransparentArguments;
68 
69     @Override
onCreate(Bundle arguments)70     public void onCreate(Bundle arguments) {
71         mTransparentArguments = new Bundle(arguments);
72 
73         mNativeTestActivity = arguments.getString(EXTRA_NATIVE_TEST_ACTIVITY);
74         if (mNativeTestActivity == null) mNativeTestActivity = DEFAULT_NATIVE_TEST_ACTIVITY;
75         mTransparentArguments.remove(EXTRA_NATIVE_TEST_ACTIVITY);
76 
77         String shardNanoTimeout = arguments.getString(EXTRA_SHARD_NANO_TIMEOUT);
78         if (shardNanoTimeout != null) mShardNanoTimeout = Long.parseLong(shardNanoTimeout);
79         mTransparentArguments.remove(EXTRA_SHARD_NANO_TIMEOUT);
80 
81         String shardSizeLimit = arguments.getString(EXTRA_SHARD_SIZE_LIMIT);
82         if (shardSizeLimit != null) mShardSizeLimit = Integer.parseInt(shardSizeLimit);
83         mTransparentArguments.remove(EXTRA_SHARD_SIZE_LIMIT);
84 
85         String stdoutFile = arguments.getString(EXTRA_STDOUT_FILE);
86         if (stdoutFile != null) {
87             mStdoutFile = new File(stdoutFile);
88         } else {
89             try {
90                 mStdoutFile = File.createTempFile(
91                         ".temp_stdout_", ".txt", Environment.getExternalStorageDirectory());
92                 Log.i(TAG, "stdout file created: %s", mStdoutFile.getAbsolutePath());
93             } catch (IOException e) {
94                 Log.e(TAG, "Unable to create temporary stdout file.", e);
95                 finish(Activity.RESULT_CANCELED, new Bundle());
96                 return;
97             }
98         }
99 
100         mTransparentArguments.remove(EXTRA_STDOUT_FILE);
101 
102         String singleTest = arguments.getString(EXTRA_TEST);
103         if (singleTest != null) {
104             ArrayList<String> shard = new ArrayList<>(1);
105             shard.add(singleTest);
106             mShards.add(shard);
107         }
108 
109         String testListFilePath = arguments.getString(EXTRA_TEST_LIST_FILE);
110         if (testListFilePath != null) {
111             File testListFile = new File(testListFilePath);
112             try {
113                 BufferedReader testListFileReader =
114                         new BufferedReader(new FileReader(testListFile));
115 
116                 String test;
117                 ArrayList<String> workingShard = new ArrayList<String>();
118                 while ((test = testListFileReader.readLine()) != null) {
119                     workingShard.add(test);
120                     if (workingShard.size() == mShardSizeLimit) {
121                         mShards.add(workingShard);
122                         workingShard = new ArrayList<String>();
123                     }
124                 }
125 
126                 if (!workingShard.isEmpty()) {
127                     mShards.add(workingShard);
128                 }
129 
130                 testListFileReader.close();
131             } catch (IOException e) {
132                 Log.e(TAG, "Error reading %s", testListFile.getAbsolutePath(), e);
133             }
134         }
135         mTransparentArguments.remove(EXTRA_TEST_LIST_FILE);
136 
137         start();
138     }
139 
140     @Override
141     @SuppressLint("DefaultLocale")
onStart()142     public void onStart() {
143         super.onStart();
144 
145         mReceiver = new TestStatusReceiver();
146         mReceiver.register(getContext());
147         mReceiver.registerCallback(new TestStatusReceiver.TestRunCallback() {
148             @Override
149             public void testRunStarted(int pid) {
150                 if (pid != Process.myPid()) {
151                     ShardMonitor m = new ShardMonitor(
152                             pid, System.nanoTime() + mShardNanoTimeout);
153                     mMonitors.put(pid, m);
154                     mHandler.post(m);
155                 }
156             }
157 
158             @Override
159             public void testRunFinished(int pid) {
160                 ShardMonitor m = mMonitors.get(pid);
161                 if (m != null) {
162                     m.stopped();
163                     mMonitors.remove(pid);
164                 }
165                 mHandler.post(new ShardEnder(pid));
166             }
167 
168             @Override
169             public void uncaughtException(int pid, String stackTrace) {
170                 mLogBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
171                         String.format("Uncaught exception in test process (pid: %d)%n%s%n",
172                                 pid, stackTrace));
173                 sendStatus(0, mLogBundle);
174             }
175         });
176 
177         mHandler.post(new ShardStarter());
178     }
179 
180     /** Monitors a test shard's execution. */
181     private class ShardMonitor implements Runnable {
182         private static final int MONITOR_FREQUENCY_MS = 1000;
183 
184         private long mExpirationNanoTime;
185         private int mPid;
186         private AtomicBoolean mStopped;
187 
ShardMonitor(int pid, long expirationNanoTime)188         public ShardMonitor(int pid, long expirationNanoTime) {
189             mPid = pid;
190             mExpirationNanoTime = expirationNanoTime;
191             mStopped = new AtomicBoolean(false);
192         }
193 
stopped()194         public void stopped() {
195             mStopped.set(true);
196         }
197 
198         @Override
run()199         public void run() {
200             if (mStopped.get()) {
201                 return;
202             }
203 
204             if (isAppProcessAlive(getContext(), mPid)) {
205                 if (System.nanoTime() > mExpirationNanoTime) {
206                     Log.e(TAG, "Test process %d timed out.", mPid);
207                     mHandler.post(new ShardEnder(mPid));
208                     return;
209                 } else {
210                     mHandler.postDelayed(this, MONITOR_FREQUENCY_MS);
211                     return;
212                 }
213             }
214 
215             Log.e(TAG, "Test process %d died unexpectedly.", mPid);
216             mHandler.post(new ShardEnder(mPid));
217         }
218 
219     }
220 
isAppProcessAlive(Context context, int pid)221     private static boolean isAppProcessAlive(Context context, int pid) {
222         ActivityManager activityManager =
223                 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
224         for (ActivityManager.RunningAppProcessInfo processInfo :
225                 activityManager.getRunningAppProcesses()) {
226             if (processInfo.pid == pid) return true;
227         }
228         return false;
229     }
230 
createShardMainIntent()231     protected Intent createShardMainIntent() {
232         Intent i = new Intent(Intent.ACTION_MAIN);
233         i.setComponent(new ComponentName(getContext().getPackageName(), mNativeTestActivity));
234         i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
235         i.putExtras(mTransparentArguments);
236         if (mShards != null && !mShards.isEmpty()) {
237             ArrayList<String> shard = mShards.remove();
238             i.putStringArrayListExtra(NativeTest.EXTRA_SHARD, shard);
239         }
240         i.putExtra(NativeTest.EXTRA_STDOUT_FILE, mStdoutFile.getAbsolutePath());
241         return i;
242     }
243 
244     /** Starts the NativeTest Activity.
245      */
246     private class ShardStarter implements Runnable {
247         @Override
run()248         public void run() {
249             getContext().startActivity(createShardMainIntent());
250         }
251     }
252 
253     private class ShardEnder implements Runnable {
254         private static final int WAIT_FOR_DEATH_MILLIS = 10;
255 
256         private int mPid;
257 
ShardEnder(int pid)258         public ShardEnder(int pid) {
259             mPid = pid;
260         }
261 
262         @Override
run()263         public void run() {
264             if (mPid != Process.myPid()) {
265                 Process.killProcess(mPid);
266                 try {
267                     while (isAppProcessAlive(getContext(), mPid)) {
268                         Thread.sleep(WAIT_FOR_DEATH_MILLIS);
269                     }
270                 } catch (InterruptedException e) {
271                     Log.e(TAG, "%d may still be alive.", mPid, e);
272                 }
273             }
274             if (mShards != null && !mShards.isEmpty()) {
275                 mHandler.post(new ShardStarter());
276             } else {
277                 finish(Activity.RESULT_OK, new Bundle());
278             }
279         }
280     }
281 }
282