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