blob: d070fcc42b7f15cd4c667c9d10bd9d5c837acb4c [file] [log] [blame]
/*
* Copyright (C) 2018 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.result.proto;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ILogSaverListener;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogFile;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.proto.LogFileProto.LogFileInfo;
import com.android.tradefed.result.proto.TestRecordProto.ChildReference;
import com.android.tradefed.result.proto.TestRecordProto.DebugInfo;
import com.android.tradefed.result.proto.TestRecordProto.DebugInfoContext;
import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
import com.android.tradefed.result.proto.TestRecordProto.TestStatus;
import com.android.tradefed.result.retry.ISupportGranularResults;
import com.android.tradefed.testtype.suite.ModuleDefinition;
import com.android.tradefed.util.SerializationUtil;
import com.android.tradefed.util.StreamUtil;
import com.google.common.base.Strings;
import com.google.protobuf.Any;
import com.google.protobuf.Timestamp;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.UUID;
/**
* Result reporter build a {@link TestRecord} protobuf with all the results inside. Should be
* extended to handle what to do with the final proto in {@link #processFinalProto(TestRecord)}.
*/
@OptionClass(alias = "proto-reporter")
public abstract class ProtoResultReporter
implements ITestInvocationListener, ILogSaverListener, ISupportGranularResults {
@Option(
name = "enable-granular-attempts",
description =
"Whether or not to allow this reporter receiving granular attempts. Feature flag."
)
private boolean mReportGranularResults = true;
private Stack<TestRecord.Builder> mLatestChild;
private TestRecord.Builder mInvocationRecordBuilder;
private long mInvocationStartTime;
private IInvocationContext mContext;
private Throwable mInvocationFailure = null;
private FailureDescription mInvocationFailureDescription = null;
/** Whether or not a testModuleStart had currently been called. */
private boolean mModuleInProgress = false;
@Override
public boolean supportGranularResults() {
return mReportGranularResults;
}
/**
* Handling of the partial invocation test record proto after {@link
* #invocationStarted(IInvocationContext)} occurred.
*
* @param invocationStartRecord The partial proto populated after the invocationStart.
* @param invocationContext The invocation {@link IInvocationContext}.
*/
public void processStartInvocation(
TestRecord invocationStartRecord, IInvocationContext invocationContext) {}
/**
* Handling of the final proto with all results.
*
* @param finalRecord The finalized proto with all the invocation results.
*/
public void processFinalProto(TestRecord finalRecord) {}
/**
* Handling of the partial module record proto after {@link
* #testModuleStarted(IInvocationContext)} occurred.
*
* @param moduleStartRecord The partial proto representing the module.
*/
public void processTestModuleStarted(TestRecord moduleStartRecord) {}
/**
* Handling of the finalized module record proto after {@link #testModuleEnded()} occurred.
*
* @param moduleRecord The finalized proto representing the module.
*/
public void processTestModuleEnd(TestRecord moduleRecord) {}
/**
* Handling of the partial test run record proto after {@link #testRunStarted(String, int)}
* occurred.
*
* @param runStartedRecord The partial proto representing the run.
*/
public void processTestRunStarted(TestRecord runStartedRecord) {}
/**
* Handling of the finalized run record proto after {@link #testRunEnded(long, HashMap)}
* occurred.
*
* @param runRecord The finalized proto representing the run.
* @param moduleInProgress whether or not a module is in progress.
*/
public void processTestRunEnded(TestRecord runRecord, boolean moduleInProgress) {}
/**
* Handling of the partial test case record proto after {@link #testStarted(TestDescription,
* long)} occurred.
*
* @param testCaseStartedRecord The partial proto representing the test case.
*/
public void processTestCaseStarted(TestRecord testCaseStartedRecord) {}
/**
* Handling of the finalized test case record proto after {@link #testEnded(TestDescription,
* long, HashMap)} occurred.
*
* @param testCaseRecord The finalized proto representing a test case.
*/
public void processTestCaseEnded(TestRecord testCaseRecord) {}
// Invocation events
@Override
public final void invocationStarted(IInvocationContext context) {
mLatestChild = new Stack<>();
mInvocationRecordBuilder = TestRecord.newBuilder();
// Set invocation unique id
mInvocationRecordBuilder.setTestRecordId(UUID.randomUUID().toString());
// Populate start time of invocation
mInvocationStartTime = System.currentTimeMillis();
Timestamp startTime = createTimeStamp(mInvocationStartTime);
mInvocationRecordBuilder.setStartTime(startTime);
mInvocationRecordBuilder.setDescription(Any.pack(context.toProto()));
mContext = context;
// Put the invocation record at the bottom of the stack
mLatestChild.add(mInvocationRecordBuilder);
// Send the invocation proto with the currently set information to indicate the beginning
// of the invocation.
TestRecord startInvocationProto = mInvocationRecordBuilder.build();
try {
processStartInvocation(startInvocationProto, context);
} catch (RuntimeException e) {
CLog.e("Failed to process invocation started:");
CLog.e(e);
}
}
@Override
public void invocationFailed(Throwable cause) {
mInvocationFailure = cause;
}
@Override
public void invocationFailed(FailureDescription failure) {
mInvocationFailureDescription = failure;
}
@Override
public final void invocationEnded(long elapsedTime) {
if (mModuleInProgress) {
// If we had a module in progress, and a new module start occurs, complete the call
testModuleEnded();
}
// Populate end time of invocation
Timestamp endTime = createTimeStamp(mInvocationStartTime + elapsedTime);
mInvocationRecordBuilder.setEndTime(endTime);
// Update the context in case it changed
mInvocationRecordBuilder.setDescription(Any.pack(mContext.toProto()));
DebugInfo invocationFailure = handleInvocationFailure();
if (invocationFailure != null) {
mInvocationRecordBuilder.setDebugInfo(invocationFailure);
}
// Finalize the protobuf handling: where to put the results.
TestRecord record = mInvocationRecordBuilder.build();
try {
processFinalProto(record);
} catch (RuntimeException e) {
CLog.e("Failed to process invocation ended:");
CLog.e(e);
}
}
// Module events (optional when there is no suite)
@Override
public final void testModuleStarted(IInvocationContext moduleContext) {
if (mModuleInProgress) {
// If we had a module in progress, and a new module start occurs, complete the call
testModuleEnded();
}
TestRecord.Builder moduleBuilder = TestRecord.newBuilder();
moduleBuilder.setParentTestRecordId(mInvocationRecordBuilder.getTestRecordId());
moduleBuilder.setTestRecordId(
moduleContext.getAttributes().get(ModuleDefinition.MODULE_ID).get(0));
moduleBuilder.setStartTime(createTimeStamp(System.currentTimeMillis()));
moduleBuilder.setDescription(Any.pack(moduleContext.toProto()));
mLatestChild.add(moduleBuilder);
mModuleInProgress = true;
try {
processTestModuleStarted(moduleBuilder.build());
} catch (RuntimeException e) {
CLog.e("Failed to process invocation ended:");
CLog.e(e);
}
}
@Override
public final void testModuleEnded() {
TestRecord.Builder moduleBuilder = mLatestChild.pop();
mModuleInProgress = false;
moduleBuilder.setEndTime(createTimeStamp(System.currentTimeMillis()));
TestRecord.Builder parentBuilder = mLatestChild.peek();
// Finalize the module and track it in the child
TestRecord moduleRecord = moduleBuilder.build();
parentBuilder.addChildren(createChildReference(moduleRecord));
try {
processTestModuleEnd(moduleRecord);
} catch (RuntimeException e) {
CLog.e("Failed to process test module end:");
CLog.e(e);
}
}
// Run events
@Override
public final void testRunStarted(String runName, int testCount) {
testRunStarted(runName, testCount, 0);
}
@Override
public void testRunStarted(String runName, int testCount, int attemptNumber) {
testRunStarted(runName, testCount, attemptNumber, System.currentTimeMillis());
}
@Override
public void testRunStarted(String runName, int testCount, int attemptNumber, long startTime) {
TestRecord.Builder runBuilder = TestRecord.newBuilder();
TestRecord.Builder parent = mLatestChild.peek();
runBuilder.setParentTestRecordId(parent.getTestRecordId());
runBuilder.setTestRecordId(runName);
runBuilder.setNumExpectedChildren(testCount);
runBuilder.setStartTime(createTimeStamp(startTime));
runBuilder.setAttemptId(attemptNumber);
mLatestChild.add(runBuilder);
try {
processTestRunStarted(runBuilder.build());
} catch (RuntimeException e) {
CLog.e("Failed to process invocation ended:");
CLog.e(e);
}
}
@Override
public final void testRunFailed(String errorMessage) {
TestRecord.Builder current = mLatestChild.peek();
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
debugBuilder.setErrorMessage(errorMessage);
if (TestStatus.UNKNOWN.equals(current.getStatus())) {
current.setDebugInfo(debugBuilder.build());
} else {
// We are in a test case and we need the run parent.
TestRecord.Builder test = mLatestChild.pop();
TestRecord.Builder run = mLatestChild.peek();
run.setDebugInfo(debugBuilder.build());
// Re-add the test
mLatestChild.add(test);
}
}
@Override
public final void testRunFailed(FailureDescription failure) {
TestRecord.Builder current = mLatestChild.peek();
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
debugBuilder.setErrorMessage(failure.toString());
if (failure.getFailureStatus() != null) {
debugBuilder.setFailureStatus(failure.getFailureStatus());
}
DebugInfoContext.Builder debugContext = DebugInfoContext.newBuilder();
if (failure.getActionInProgress() != null) {
debugContext.setActionInProgress(failure.getActionInProgress().toString());
}
if (!Strings.isNullOrEmpty(failure.getDebugHelpMessage())) {
debugContext.setDebugHelpMessage(failure.getDebugHelpMessage());
}
debugBuilder.setDebugInfoContext(debugContext.build());
if (TestStatus.UNKNOWN.equals(current.getStatus())) {
current.setDebugInfo(debugBuilder.build());
} else {
// We are in a test case and we need the run parent.
TestRecord.Builder test = mLatestChild.pop();
TestRecord.Builder run = mLatestChild.peek();
run.setDebugInfo(debugBuilder.build());
// Re-add the test
mLatestChild.add(test);
}
}
@Override
public final void testRunEnded(long elapsedTimeMillis, HashMap<String, Metric> runMetrics) {
TestRecord.Builder runBuilder = mLatestChild.pop();
long startTime = timeStampToMillis(runBuilder.getStartTime());
runBuilder.setEndTime(createTimeStamp(startTime + elapsedTimeMillis));
runBuilder.putAllMetrics(runMetrics);
TestRecord.Builder parentBuilder = mLatestChild.peek();
// Finalize the run and track it in the child
TestRecord runRecord = runBuilder.build();
parentBuilder.addChildren(createChildReference(runRecord));
try {
processTestRunEnded(runRecord, mModuleInProgress);
} catch (RuntimeException e) {
CLog.e("Failed to process test run end:");
CLog.e(e);
}
}
// test case events
@Override
public final void testStarted(TestDescription test) {
testStarted(test, System.currentTimeMillis());
}
@Override
public final void testStarted(TestDescription test, long startTime) {
TestRecord.Builder testBuilder = TestRecord.newBuilder();
TestRecord.Builder parent = mLatestChild.peek();
testBuilder.setParentTestRecordId(parent.getTestRecordId());
testBuilder.setTestRecordId(test.toString());
testBuilder.setStartTime(createTimeStamp(startTime));
testBuilder.setStatus(TestStatus.PASS);
mLatestChild.add(testBuilder);
try {
processTestCaseStarted(testBuilder.build());
} catch (RuntimeException e) {
CLog.e("Failed to process invocation ended:");
CLog.e(e);
}
}
@Override
public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) {
testEnded(test, System.currentTimeMillis(), testMetrics);
}
@Override
public final void testEnded(
TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
TestRecord.Builder testBuilder = mLatestChild.pop();
testBuilder.setEndTime(createTimeStamp(endTime));
testBuilder.putAllMetrics(testMetrics);
TestRecord.Builder parentBuilder = mLatestChild.peek();
// Finalize the run and track it in the child
TestRecord testCaseRecord = testBuilder.build();
parentBuilder.addChildren(createChildReference(testCaseRecord));
try {
processTestCaseEnded(testCaseRecord);
} catch (RuntimeException e) {
CLog.e("Failed to process test case end:");
CLog.e(e);
}
}
@Override
public final void testFailed(TestDescription test, String trace) {
TestRecord.Builder testBuilder = mLatestChild.peek();
testBuilder.setStatus(TestStatus.FAIL);
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
// FIXME: extract the error message from the trace
debugBuilder.setErrorMessage(trace);
debugBuilder.setTrace(trace);
testBuilder.setDebugInfo(debugBuilder.build());
}
@Override
public final void testFailed(TestDescription test, FailureDescription failure) {
TestRecord.Builder testBuilder = mLatestChild.peek();
testBuilder.setStatus(TestStatus.FAIL);
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
// FIXME: extract the error message from the trace
debugBuilder.setErrorMessage(failure.toString());
debugBuilder.setTrace(failure.toString());
if (failure.getFailureStatus() != null) {
debugBuilder.setFailureStatus(failure.getFailureStatus());
}
DebugInfoContext.Builder debugContext = DebugInfoContext.newBuilder();
if (failure.getActionInProgress() != null) {
debugContext.setActionInProgress(failure.getActionInProgress().toString());
}
if (!Strings.isNullOrEmpty(failure.getDebugHelpMessage())) {
debugContext.setDebugHelpMessage(failure.getDebugHelpMessage());
}
debugBuilder.setDebugInfoContext(debugContext.build());
testBuilder.setDebugInfo(debugBuilder.build());
}
@Override
public final void testIgnored(TestDescription test) {
TestRecord.Builder testBuilder = mLatestChild.peek();
testBuilder.setStatus(TestStatus.IGNORED);
}
@Override
public final void testAssumptionFailure(TestDescription test, String trace) {
TestRecord.Builder testBuilder = mLatestChild.peek();
testBuilder.setStatus(TestStatus.ASSUMPTION_FAILURE);
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
// FIXME: extract the error message from the trace
debugBuilder.setErrorMessage(trace);
debugBuilder.setTrace(trace);
testBuilder.setDebugInfo(debugBuilder.build());
}
@Override
public final void testAssumptionFailure(TestDescription test, FailureDescription failure) {
TestRecord.Builder testBuilder = mLatestChild.peek();
testBuilder.setStatus(TestStatus.ASSUMPTION_FAILURE);
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
// FIXME: extract the error message from the trace
debugBuilder.setErrorMessage(failure.toString());
debugBuilder.setTrace(failure.toString());
if (failure.getFailureStatus() != null) {
debugBuilder.setFailureStatus(failure.getFailureStatus());
}
testBuilder.setDebugInfo(debugBuilder.build());
}
// log events
@Override
public final void logAssociation(String dataName, LogFile logFile) {
TestRecord.Builder current = mLatestChild.peek();
Map<String, Any> fullmap = new HashMap<>();
fullmap.putAll(current.getArtifactsMap());
Any any = Any.pack(createFileProto(logFile));
fullmap.put(dataName, any);
current.putAllArtifacts(fullmap);
}
private ChildReference createChildReference(TestRecord record) {
ChildReference.Builder child = ChildReference.newBuilder();
child.setTestRecordId(record.getTestRecordId());
child.setInlineTestRecord(record);
return child.build();
}
/** Create and populate Timestamp as recommended in the javadoc of the Timestamp proto. */
private Timestamp createTimeStamp(long currentTimeMs) {
return Timestamp.newBuilder()
.setSeconds(currentTimeMs / 1000)
.setNanos((int) ((currentTimeMs % 1000) * 1000000))
.build();
}
private long timeStampToMillis(Timestamp stamp) {
return stamp.getSeconds() * 1000L + (stamp.getNanos() / 1000000L);
}
private LogFileInfo createFileProto(LogFile logFile) {
LogFileInfo.Builder logFileBuilder = LogFileInfo.newBuilder();
logFileBuilder
.setPath(logFile.getPath())
.setIsText(logFile.isText())
.setLogType(logFile.getType().toString())
.setIsCompressed(logFile.isCompressed())
.setSize(logFile.getSize());
// Url can be null so avoid NPE by checking it before setting the proto
if (logFile.getUrl() != null) {
logFileBuilder.setUrl(logFile.getUrl());
}
return logFileBuilder.build();
}
private DebugInfo handleInvocationFailure() {
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
Throwable baseException = null;
if (mInvocationFailureDescription != null) {
baseException = mInvocationFailureDescription.getCause();
}
if (baseException == null) {
baseException = mInvocationFailure;
}
// TODO: Handle FailureDescription without getCause.
if (baseException == null) {
// No invocation failure
return null;
}
if (baseException.getMessage() != null) {
debugBuilder.setErrorMessage(baseException.getMessage());
}
debugBuilder.setTrace(StreamUtil.getStackTrace(baseException));
if (mInvocationFailureDescription != null
&& mInvocationFailureDescription.getFailureStatus() != null) {
debugBuilder.setFailureStatus(mInvocationFailureDescription.getFailureStatus());
}
DebugInfoContext.Builder debugContext = DebugInfoContext.newBuilder();
try {
debugContext.setErrorType(SerializationUtil.serializeToString(baseException));
} catch (IOException e) {
CLog.e("Failed to serialize the invocation failure:");
CLog.e(e);
}
debugBuilder.setDebugInfoContext(debugContext);
return debugBuilder.build();
}
}