1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.util; 17 18 import com.android.ddmlib.testrunner.TestIdentifier; 19 import com.android.tradefed.invoker.IInvocationContext; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.result.FileInputStreamSource; 22 import com.android.tradefed.result.ITestInvocationListener; 23 import com.android.tradefed.result.InputStreamSource; 24 import com.android.tradefed.util.SubprocessEventHelper.BaseTestEventInfo; 25 import com.android.tradefed.util.SubprocessEventHelper.FailedTestEventInfo; 26 import com.android.tradefed.util.SubprocessEventHelper.InvocationFailedEventInfo; 27 import com.android.tradefed.util.SubprocessEventHelper.InvocationStartedEventInfo; 28 import com.android.tradefed.util.SubprocessEventHelper.TestEndedEventInfo; 29 import com.android.tradefed.util.SubprocessEventHelper.TestLogEventInfo; 30 import com.android.tradefed.util.SubprocessEventHelper.TestRunEndedEventInfo; 31 import com.android.tradefed.util.SubprocessEventHelper.TestRunFailedEventInfo; 32 import com.android.tradefed.util.SubprocessEventHelper.TestRunStartedEventInfo; 33 import com.android.tradefed.util.SubprocessEventHelper.TestStartedEventInfo; 34 35 import org.json.JSONException; 36 import org.json.JSONObject; 37 38 import java.io.BufferedReader; 39 import java.io.Closeable; 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.FileReader; 44 import java.io.IOException; 45 import java.io.InputStreamReader; 46 import java.net.ServerSocket; 47 import java.net.Socket; 48 import java.util.ArrayList; 49 import java.util.HashMap; 50 import java.util.Map; 51 import java.util.concurrent.CountDownLatch; 52 import java.util.concurrent.TimeUnit; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 /** 57 * Extends {@link FileOutputStream} to parse the output before writing to the file so we can 58 * generate the test events on the launcher side. 59 */ 60 public class SubprocessTestResultsParser implements Closeable { 61 62 private ITestInvocationListener mListener; 63 private TestIdentifier currentTest = null; 64 private Pattern mPattern = null; 65 private Map<String, EventHandler> mHandlerMap = null; 66 private EventReceiverThread mEventReceiver = null; 67 private IInvocationContext mContext = null; 68 private Long mStartTime = null; 69 70 /** Relevant test status keys. */ 71 public static class StatusKeys { 72 public static final String INVOCATION_FAILED = "INVOCATION_FAILED"; 73 public static final String TEST_ASSUMPTION_FAILURE = "TEST_ASSUMPTION_FAILURE"; 74 public static final String TEST_ENDED = "TEST_ENDED"; 75 public static final String TEST_FAILED = "TEST_FAILED"; 76 public static final String TEST_IGNORED = "TEST_IGNORED"; 77 public static final String TEST_STARTED = "TEST_STARTED"; 78 public static final String TEST_RUN_ENDED = "TEST_RUN_ENDED"; 79 public static final String TEST_RUN_FAILED = "TEST_RUN_FAILED"; 80 public static final String TEST_RUN_STARTED = "TEST_RUN_STARTED"; 81 public static final String TEST_LOG = "TEST_LOG"; 82 public static final String INVOCATION_STARTED = "INVOCATION_STARTED"; 83 } 84 85 /** 86 * Internal receiver thread class with a socket. 87 */ 88 private class EventReceiverThread extends Thread { 89 private ServerSocket mSocket; 90 private CountDownLatch mCountDown; 91 92 public EventReceiverThread() throws IOException { 93 super("EventReceiverThread"); 94 mSocket = new ServerSocket(0); 95 mCountDown = new CountDownLatch(1); 96 } 97 98 protected int getLocalPort() { 99 return mSocket.getLocalPort(); 100 } 101 102 protected CountDownLatch getCountDown() { 103 return mCountDown; 104 } 105 106 public void cancel() throws IOException { 107 if (mSocket != null) { 108 mSocket.close(); 109 } 110 } 111 112 @Override 113 public void run() { 114 Socket client = null; 115 BufferedReader in = null; 116 try { 117 client = mSocket.accept(); 118 in = new BufferedReader(new InputStreamReader(client.getInputStream())); 119 String event = null; 120 while ((event = in.readLine()) != null) { 121 try { 122 CLog.i("received event: '%s'", event); 123 parse(event); 124 } catch (JSONException e) { 125 CLog.e(e); 126 } 127 } 128 } catch (IOException e) { 129 CLog.e(e); 130 } finally { 131 StreamUtil.close(in); 132 mCountDown.countDown(); 133 } 134 CLog.d("EventReceiverThread done."); 135 } 136 } 137 138 /** 139 * If the event receiver is being used, ensure that we wait for it to terminate. 140 * @param millis timeout in milliseconds. 141 * @return True if receiver thread terminate before timeout, False otherwise. 142 */ 143 public boolean joinReceiver(long millis) { 144 if (mEventReceiver != null) { 145 try { 146 CLog.i("Waiting for events to finish being processed."); 147 if (!mEventReceiver.getCountDown().await(millis, TimeUnit.MILLISECONDS)) { 148 CLog.e("Event receiver thread did not complete. Some events may be missing."); 149 return false; 150 } 151 } catch (InterruptedException e) { 152 CLog.e(e); 153 throw new RuntimeException(e); 154 } 155 } 156 return true; 157 } 158 159 /** 160 * Returns the socket receiver that was open. -1 if none. 161 */ 162 public int getSocketServerPort() { 163 if (mEventReceiver != null) { 164 return mEventReceiver.getLocalPort(); 165 } 166 return -1; 167 } 168 169 @Override 170 public void close() throws IOException { 171 if (mEventReceiver != null) { 172 mEventReceiver.cancel(); 173 } 174 } 175 176 /** 177 * Constructor for the result parser 178 * 179 * @param listener {@link ITestInvocationListener} where to report the results 180 * @param streaming if True, a socket receiver will be open to receive results. 181 * @param context a {@link IInvocationContext} information about the invocation 182 */ 183 public SubprocessTestResultsParser( 184 ITestInvocationListener listener, boolean streaming, IInvocationContext context) 185 throws IOException { 186 this(listener, context); 187 if (streaming) { 188 mEventReceiver = new EventReceiverThread(); 189 mEventReceiver.start(); 190 } 191 } 192 193 /** 194 * Constructor for the result parser 195 * 196 * @param listener {@link ITestInvocationListener} where to report the results 197 * @param context a {@link IInvocationContext} information about the invocation 198 */ 199 public SubprocessTestResultsParser( 200 ITestInvocationListener listener, IInvocationContext context) { 201 mListener = listener; 202 mContext = context; 203 StringBuilder sb = new StringBuilder(); 204 sb.append(StatusKeys.INVOCATION_FAILED).append("|"); 205 sb.append(StatusKeys.TEST_ASSUMPTION_FAILURE).append("|"); 206 sb.append(StatusKeys.TEST_ENDED).append("|"); 207 sb.append(StatusKeys.TEST_FAILED).append("|"); 208 sb.append(StatusKeys.TEST_IGNORED).append("|"); 209 sb.append(StatusKeys.TEST_STARTED).append("|"); 210 sb.append(StatusKeys.TEST_RUN_ENDED).append("|"); 211 sb.append(StatusKeys.TEST_RUN_FAILED).append("|"); 212 sb.append(StatusKeys.TEST_RUN_STARTED).append("|"); 213 sb.append(StatusKeys.TEST_LOG).append("|"); 214 sb.append(StatusKeys.INVOCATION_STARTED); 215 String patt = String.format("(.*)(%s)( )(.*)", sb.toString()); 216 mPattern = Pattern.compile(patt); 217 218 // Create Handler map for each event 219 mHandlerMap = new HashMap<String, EventHandler>(); 220 mHandlerMap.put(StatusKeys.INVOCATION_FAILED, new InvocationFailedEventHandler()); 221 mHandlerMap.put(StatusKeys.TEST_ASSUMPTION_FAILURE, 222 new TestAssumptionFailureEventHandler()); 223 mHandlerMap.put(StatusKeys.TEST_ENDED, new TestEndedEventHandler()); 224 mHandlerMap.put(StatusKeys.TEST_FAILED, new TestFailedEventHandler()); 225 mHandlerMap.put(StatusKeys.TEST_IGNORED, new TestIgnoredEventHandler()); 226 mHandlerMap.put(StatusKeys.TEST_STARTED, new TestStartedEventHandler()); 227 mHandlerMap.put(StatusKeys.TEST_RUN_ENDED, new TestRunEndedEventHandler()); 228 mHandlerMap.put(StatusKeys.TEST_RUN_FAILED, new TestRunFailedEventHandler()); 229 mHandlerMap.put(StatusKeys.TEST_RUN_STARTED, new TestRunStartedEventHandler()); 230 mHandlerMap.put(StatusKeys.TEST_LOG, new TestLogEventHandler()); 231 mHandlerMap.put(StatusKeys.INVOCATION_STARTED, new InvocationStartedEventHandler()); 232 } 233 234 public void parseFile(File file) { 235 BufferedReader reader = null; 236 try { 237 reader = new BufferedReader(new FileReader(file)); 238 } catch (FileNotFoundException e) { 239 CLog.e(e); 240 throw new RuntimeException(e); 241 } 242 ArrayList<String> listString = new ArrayList<String>(); 243 String line = null; 244 try { 245 while ((line = reader.readLine()) != null) { 246 listString.add(line); 247 } 248 reader.close(); 249 } catch (IOException e) { 250 CLog.e(e); 251 throw new RuntimeException(e); 252 } 253 processNewLines(listString.toArray(new String[listString.size()])); 254 } 255 256 /** 257 * call parse on each line of the array to extract the events if any. 258 */ 259 public void processNewLines(String[] lines) { 260 for (String line : lines) { 261 try { 262 parse(line); 263 } catch (JSONException e) { 264 CLog.e("Exception while parsing"); 265 CLog.e(e); 266 throw new RuntimeException(e); 267 } 268 } 269 } 270 271 /** 272 * Parse a line, if it matches one of the events, handle it. 273 */ 274 private void parse(String line) throws JSONException { 275 Matcher matcher = mPattern.matcher(line); 276 if (matcher.find()) { 277 EventHandler handler = mHandlerMap.get(matcher.group(2)); 278 if (handler != null) { 279 handler.handleEvent(matcher.group(4)); 280 } else { 281 CLog.w("No handler found matching: %s", matcher.group(2)); 282 } 283 } 284 } 285 286 private void checkCurrentTestId(String className, String testName) { 287 if (currentTest == null) { 288 currentTest = new TestIdentifier(className, testName); 289 CLog.w("Calling a test event without having called testStarted."); 290 } 291 } 292 293 /** 294 * Interface for event handling 295 */ 296 interface EventHandler { 297 public void handleEvent(String eventJson) throws JSONException; 298 } 299 300 private class TestRunStartedEventHandler implements EventHandler { 301 @Override 302 public void handleEvent(String eventJson) throws JSONException { 303 TestRunStartedEventInfo rsi = new TestRunStartedEventInfo(new JSONObject(eventJson)); 304 mListener.testRunStarted(rsi.mRunName, rsi.mTestCount); 305 } 306 } 307 308 private class TestRunFailedEventHandler implements EventHandler { 309 @Override 310 public void handleEvent(String eventJson) throws JSONException { 311 TestRunFailedEventInfo rfi = new TestRunFailedEventInfo(new JSONObject(eventJson)); 312 mListener.testRunFailed(rfi.mReason); 313 } 314 } 315 316 private class TestRunEndedEventHandler implements EventHandler { 317 @Override 318 public void handleEvent(String eventJson) throws JSONException { 319 try { 320 TestRunEndedEventInfo rei = new TestRunEndedEventInfo(new JSONObject(eventJson)); 321 mListener.testRunEnded(rei.mTime, rei.mRunMetrics); 322 } finally { 323 currentTest = null; 324 } 325 } 326 } 327 328 private class InvocationFailedEventHandler implements EventHandler { 329 @Override 330 public void handleEvent(String eventJson) throws JSONException { 331 InvocationFailedEventInfo ifi = 332 new InvocationFailedEventInfo(new JSONObject(eventJson)); 333 mListener.invocationFailed(ifi.mCause); 334 } 335 } 336 337 private class TestStartedEventHandler implements EventHandler { 338 @Override 339 public void handleEvent(String eventJson) throws JSONException { 340 TestStartedEventInfo bti = new TestStartedEventInfo(new JSONObject(eventJson)); 341 currentTest = new TestIdentifier(bti.mClassName, bti.mTestName); 342 if (bti.mStartTime != null) { 343 mListener.testStarted(currentTest, bti.mStartTime); 344 } else { 345 mListener.testStarted(currentTest); 346 } 347 } 348 } 349 350 private class TestFailedEventHandler implements EventHandler { 351 @Override 352 public void handleEvent(String eventJson) throws JSONException { 353 FailedTestEventInfo fti = new FailedTestEventInfo(new JSONObject(eventJson)); 354 checkCurrentTestId(fti.mClassName, fti.mTestName); 355 mListener.testFailed(currentTest, fti.mTrace); 356 } 357 } 358 359 private class TestEndedEventHandler implements EventHandler { 360 @Override 361 public void handleEvent(String eventJson) throws JSONException { 362 try { 363 TestEndedEventInfo tei = new TestEndedEventInfo(new JSONObject(eventJson)); 364 checkCurrentTestId(tei.mClassName, tei.mTestName); 365 if (tei.mEndTime != null) { 366 mListener.testEnded(currentTest, tei.mEndTime, tei.mRunMetrics); 367 } else { 368 mListener.testEnded(currentTest, tei.mRunMetrics); 369 } 370 } finally { 371 currentTest = null; 372 } 373 } 374 } 375 376 private class TestIgnoredEventHandler implements EventHandler { 377 @Override 378 public void handleEvent(String eventJson) throws JSONException { 379 BaseTestEventInfo baseTestIgnored = new BaseTestEventInfo(new JSONObject(eventJson)); 380 checkCurrentTestId(baseTestIgnored.mClassName, baseTestIgnored.mTestName); 381 mListener.testIgnored(currentTest); 382 } 383 } 384 385 private class TestAssumptionFailureEventHandler implements EventHandler { 386 @Override 387 public void handleEvent(String eventJson) throws JSONException { 388 FailedTestEventInfo FailedAssumption = 389 new FailedTestEventInfo(new JSONObject(eventJson)); 390 checkCurrentTestId(FailedAssumption.mClassName, FailedAssumption.mTestName); 391 mListener.testAssumptionFailure(currentTest, FailedAssumption.mTrace); 392 } 393 } 394 395 private class TestLogEventHandler implements EventHandler { 396 @Override 397 public void handleEvent(String eventJson) throws JSONException { 398 TestLogEventInfo logInfo = new TestLogEventInfo(new JSONObject(eventJson)); 399 String name = String.format("subprocess-%s", logInfo.mDataName); 400 try { 401 InputStreamSource data = new FileInputStreamSource(logInfo.mDataFile); 402 mListener.testLog(name, logInfo.mLogType, data); 403 } finally { 404 FileUtil.deleteFile(logInfo.mDataFile); 405 } 406 } 407 } 408 409 private class InvocationStartedEventHandler implements EventHandler { 410 @Override 411 public void handleEvent(String eventJson) throws JSONException { 412 InvocationStartedEventInfo eventStart = 413 new InvocationStartedEventInfo(new JSONObject(eventJson)); 414 if (mContext.getTestTag() == null || "stub".equals(mContext.getTestTag())) { 415 mContext.setTestTag(eventStart.mTestTag); 416 } 417 mStartTime = eventStart.mStartTime; 418 } 419 } 420 421 /** 422 * Returns the start time associated with the invocation start event from the subprocess 423 * invocation. 424 */ 425 public Long getStartTime() { 426 return mStartTime; 427 } 428 } 429