blob: 3e085a5a3b459d535737cb46aeeeb90a057f8d9e [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.util;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.util.SubprocessEventHelper.BaseTestEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.FailedTestEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.InvocationFailedEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.TestEndedEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.TestLogEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.TestRunEndedEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.TestRunFailedEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.TestRunStartedEventInfo;
import com.android.tradefed.util.SubprocessEventHelper.TestStartedEventInfo;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Extends {@link FileOutputStream} to parse the output before writing to the file so we can
* generate the test events on the launcher side.
*/
public class SubprocessTestResultsParser implements Closeable {
private ITestInvocationListener mListener;
private TestIdentifier currentTest = null;
private Pattern mPattern = null;
private Map<String, EventHandler> mHandlerMap = null;
private EventReceiverThread mEventReceiver = null;
private IInvocationContext mContext = null;
/** Relevant test status keys. */
public static class StatusKeys {
public static final String INVOCATION_FAILED = "INVOCATION_FAILED";
public static final String TEST_ASSUMPTION_FAILURE = "TEST_ASSUMPTION_FAILURE";
public static final String TEST_ENDED = "TEST_ENDED";
public static final String TEST_FAILED = "TEST_FAILED";
public static final String TEST_IGNORED = "TEST_IGNORED";
public static final String TEST_STARTED = "TEST_STARTED";
public static final String TEST_RUN_ENDED = "TEST_RUN_ENDED";
public static final String TEST_RUN_FAILED = "TEST_RUN_FAILED";
public static final String TEST_RUN_STARTED = "TEST_RUN_STARTED";
public static final String TEST_LOG = "TEST_LOG";
public static final String TEST_TAG = "TEST_TAG";
}
/**
* Internal receiver thread class with a socket.
*/
private class EventReceiverThread extends Thread {
private ServerSocket mSocket;
private CountDownLatch mCountDown;
public EventReceiverThread() throws IOException {
super("EventReceiverThread");
mSocket = new ServerSocket(0);
mCountDown = new CountDownLatch(1);
}
protected int getLocalPort() {
return mSocket.getLocalPort();
}
protected CountDownLatch getCountDown() {
return mCountDown;
}
public void cancel() throws IOException {
if (mSocket != null) {
mSocket.close();
}
}
@Override
public void run() {
Socket client = null;
BufferedReader in = null;
try {
client = mSocket.accept();
in = new BufferedReader(new InputStreamReader(client.getInputStream()));
String event = null;
while ((event = in.readLine()) != null) {
try {
CLog.i("received event: '%s'", event);
parse(event);
} catch (JSONException e) {
CLog.e(e);
}
}
} catch (IOException e) {
CLog.e(e);
} finally {
StreamUtil.close(in);
mCountDown.countDown();
}
CLog.d("EventReceiverThread done.");
}
}
/**
* If the event receiver is being used, ensure that we wait for it to terminate.
* @param millis timeout in milliseconds.
* @return True if receiver thread terminate before timeout, False otherwise.
*/
public boolean joinReceiver(long millis) {
if (mEventReceiver != null) {
try {
CLog.i("Waiting for events to finish being processed.");
if (!mEventReceiver.getCountDown().await(millis, TimeUnit.MILLISECONDS)) {
CLog.e("Event receiver thread did not complete. Some events may be missing.");
return false;
}
} catch (InterruptedException e) {
CLog.e(e);
throw new RuntimeException(e);
}
}
return true;
}
/**
* Returns the socket receiver that was open. -1 if none.
*/
public int getSocketServerPort() {
if (mEventReceiver != null) {
return mEventReceiver.getLocalPort();
}
return -1;
}
@Override
public void close() throws IOException {
if (mEventReceiver != null) {
mEventReceiver.cancel();
}
}
/**
* Constructor for the result parser
*
* @param listener {@link ITestInvocationListener} where to report the results
* @param streaming if True, a socket receiver will be open to receive results.
* @param context a {@link IInvocationContext} information about the invocation
*/
public SubprocessTestResultsParser(
ITestInvocationListener listener, boolean streaming, IInvocationContext context)
throws IOException {
this(listener, context);
if (streaming) {
mEventReceiver = new EventReceiverThread();
mEventReceiver.start();
}
}
/**
* Constructor for the result parser
*
* @param listener {@link ITestInvocationListener} where to report the results
* @param context a {@link IInvocationContext} information about the invocation
*/
public SubprocessTestResultsParser(
ITestInvocationListener listener, IInvocationContext context) {
mListener = listener;
mContext = context;
StringBuilder sb = new StringBuilder();
sb.append(StatusKeys.INVOCATION_FAILED).append("|");
sb.append(StatusKeys.TEST_ASSUMPTION_FAILURE).append("|");
sb.append(StatusKeys.TEST_ENDED).append("|");
sb.append(StatusKeys.TEST_FAILED).append("|");
sb.append(StatusKeys.TEST_IGNORED).append("|");
sb.append(StatusKeys.TEST_STARTED).append("|");
sb.append(StatusKeys.TEST_RUN_ENDED).append("|");
sb.append(StatusKeys.TEST_RUN_FAILED).append("|");
sb.append(StatusKeys.TEST_RUN_STARTED).append("|");
sb.append(StatusKeys.TEST_LOG).append("|");
sb.append(StatusKeys.TEST_TAG);
String patt = String.format("(.*)(%s)( )(.*)", sb.toString());
mPattern = Pattern.compile(patt);
// Create Handler map for each event
mHandlerMap = new HashMap<String, EventHandler>();
mHandlerMap.put(StatusKeys.INVOCATION_FAILED, new InvocationFailedEventHandler());
mHandlerMap.put(StatusKeys.TEST_ASSUMPTION_FAILURE,
new TestAssumptionFailureEventHandler());
mHandlerMap.put(StatusKeys.TEST_ENDED, new TestEndedEventHandler());
mHandlerMap.put(StatusKeys.TEST_FAILED, new TestFailedEventHandler());
mHandlerMap.put(StatusKeys.TEST_IGNORED, new TestIgnoredEventHandler());
mHandlerMap.put(StatusKeys.TEST_STARTED, new TestStartedEventHandler());
mHandlerMap.put(StatusKeys.TEST_RUN_ENDED, new TestRunEndedEventHandler());
mHandlerMap.put(StatusKeys.TEST_RUN_FAILED, new TestRunFailedEventHandler());
mHandlerMap.put(StatusKeys.TEST_RUN_STARTED, new TestRunStartedEventHandler());
mHandlerMap.put(StatusKeys.TEST_LOG, new TestLogEventHandler());
mHandlerMap.put(StatusKeys.TEST_TAG, new TestTagEventHandler());
}
public void parseFile(File file) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
} catch (FileNotFoundException e) {
CLog.e(e);
throw new RuntimeException(e);
}
ArrayList<String> listString = new ArrayList<String>();
String line = null;
try {
while ((line = reader.readLine()) != null) {
listString.add(line);
}
reader.close();
} catch (IOException e) {
CLog.e(e);
throw new RuntimeException(e);
}
processNewLines(listString.toArray(new String[listString.size()]));
}
/**
* call parse on each line of the array to extract the events if any.
*/
public void processNewLines(String[] lines) {
for (String line : lines) {
try {
parse(line);
} catch (JSONException e) {
CLog.e("Exception while parsing");
CLog.e(e);
throw new RuntimeException(e);
}
}
}
/**
* Parse a line, if it matches one of the events, handle it.
*/
private void parse(String line) throws JSONException {
Matcher matcher = mPattern.matcher(line);
if (matcher.find()) {
EventHandler handler = mHandlerMap.get(matcher.group(2));
if (handler != null) {
handler.handleEvent(matcher.group(4));
} else {
CLog.w("No handler found matching: %s", matcher.group(2));
}
}
}
private void checkCurrentTestId(String className, String testName) {
if (currentTest == null) {
currentTest = new TestIdentifier(className, testName);
CLog.w("Calling a test event without having called testStarted.");
}
}
/**
* Interface for event handling
*/
interface EventHandler {
public void handleEvent(String eventJson) throws JSONException;
}
private class TestRunStartedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
TestRunStartedEventInfo rsi = new TestRunStartedEventInfo(new JSONObject(eventJson));
mListener.testRunStarted(rsi.mRunName, rsi.mTestCount);
}
}
private class TestRunFailedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
TestRunFailedEventInfo rfi = new TestRunFailedEventInfo(new JSONObject(eventJson));
mListener.testRunFailed(rfi.mReason);
}
}
private class TestRunEndedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
try {
TestRunEndedEventInfo rei = new TestRunEndedEventInfo(new JSONObject(eventJson));
mListener.testRunEnded(rei.mTime, rei.mRunMetrics);
} finally {
currentTest = null;
}
}
}
private class InvocationFailedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
InvocationFailedEventInfo ifi =
new InvocationFailedEventInfo(new JSONObject(eventJson));
mListener.invocationFailed(ifi.mCause);
}
}
private class TestStartedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
TestStartedEventInfo bti = new TestStartedEventInfo(new JSONObject(eventJson));
currentTest = new TestIdentifier(bti.mClassName, bti.mTestName);
if (bti.mStartTime != null) {
mListener.testStarted(currentTest, bti.mStartTime);
} else {
mListener.testStarted(currentTest);
}
}
}
private class TestFailedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
FailedTestEventInfo fti = new FailedTestEventInfo(new JSONObject(eventJson));
checkCurrentTestId(fti.mClassName, fti.mTestName);
mListener.testFailed(currentTest, fti.mTrace);
}
}
private class TestEndedEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
try {
TestEndedEventInfo tei = new TestEndedEventInfo(new JSONObject(eventJson));
checkCurrentTestId(tei.mClassName, tei.mTestName);
if (tei.mEndTime != null) {
mListener.testEnded(currentTest, tei.mEndTime, tei.mRunMetrics);
} else {
mListener.testEnded(currentTest, tei.mRunMetrics);
}
} finally {
currentTest = null;
}
}
}
private class TestIgnoredEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
BaseTestEventInfo baseTestIgnored = new BaseTestEventInfo(new JSONObject(eventJson));
checkCurrentTestId(baseTestIgnored.mClassName, baseTestIgnored.mTestName);
mListener.testIgnored(currentTest);
}
}
private class TestAssumptionFailureEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
FailedTestEventInfo FailedAssumption =
new FailedTestEventInfo(new JSONObject(eventJson));
checkCurrentTestId(FailedAssumption.mClassName, FailedAssumption.mTestName);
mListener.testAssumptionFailure(currentTest, FailedAssumption.mTrace);
}
}
private class TestLogEventHandler implements EventHandler {
@Override
public void handleEvent(String eventJson) throws JSONException {
TestLogEventInfo logInfo = new TestLogEventInfo(new JSONObject(eventJson));
String name = String.format("subprocess-%s", logInfo.mDataName);
try {
InputStreamSource data = new FileInputStreamSource(logInfo.mDataFile);
mListener.testLog(name, logInfo.mLogType, data);
} finally {
FileUtil.deleteFile(logInfo.mDataFile);
}
}
}
/*
* The parent process should use the test tag of the subprocess if it's not set yet.
*/
private class TestTagEventHandler implements EventHandler {
@Override
public void handleEvent(String testTag) {
if (mContext.getTestTag() == null || "stub".equals(mContext.getTestTag())) {
mContext.setTestTag(testTag);
}
}
}
}