Create a reporter to populate the test record proto
Create a base abstract class that populates the test record
protobuf, a representation of Tradefed results.
The reporter should be extended to handle the final proto
and what should be done with it.
Test: unit tests
Bug: 79990695
Change-Id: I04081e538100b028094cc288ffe714e269a0b7dd
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
new file mode 100644
index 0000000..2499051
--- /dev/null
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -0,0 +1,365 @@
+/*
+ * 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.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.ILogSaver;
+import com.android.tradefed.result.ILogSaverListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
+
+import com.google.protobuf.Any;
+import com.google.protobuf.Timestamp;
+import com.tradefed.result.proto.LogFileProto.LogFileInfo;
+import com.tradefed.result.proto.TestRecordProto.ChildReference;
+import com.tradefed.result.proto.TestRecordProto.DebugInfo;
+import com.tradefed.result.proto.TestRecordProto.TestRecord;
+import com.tradefed.result.proto.TestRecordProto.TestStatus;
+
+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")
+abstract class ProtoResultReporter implements ITestInvocationListener, ILogSaverListener {
+
+ private Stack<TestRecord.Builder> mLatestChild;
+ private TestRecord.Builder mInvocationRecordBuilder;
+ private long mInvocationStartTime;
+
+ /**
+ * Handling of the partial invocation test record proto after {@link
+ * #invocationStarted(IInvocationContext)} occurred.
+ *
+ * @param invocationStartRecord The partial proto populated after the invocationStart.
+ */
+ public void processStartInvocation(TestRecord invocationStartRecord) {}
+
+ /**
+ * 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.
+ */
+ public void processTestRunEnded(TestRecord runRecord) {}
+
+ /**
+ * 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);
+
+ // 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);
+ } catch (RuntimeException e) {
+ CLog.e("Failed to process invocation started:");
+ CLog.e(e);
+ }
+ }
+
+ @Override
+ public final void invocationEnded(long elapsedTime) {
+ // Populate end time of invocation
+ Timestamp endTime = createTimeStamp(mInvocationStartTime + elapsedTime);
+ mInvocationRecordBuilder.setEndTime(endTime);
+
+ // 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) {
+ TestRecord.Builder moduleBuilder = TestRecord.newBuilder();
+ moduleBuilder.setParentTestRecordId(mInvocationRecordBuilder.getTestRecordId());
+ moduleBuilder.setTestRecordId(
+ moduleContext.getAttributes().get(ModuleDefinition.MODULE_ID).get(0));
+ moduleBuilder.setStartTime(createTimeStamp(System.currentTimeMillis()));
+ mLatestChild.add(moduleBuilder);
+ 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();
+ 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) {
+ TestRecord.Builder runBuilder = TestRecord.newBuilder();
+ TestRecord.Builder parent = mLatestChild.peek();
+ runBuilder.setParentTestRecordId(parent.getTestRecordId());
+ runBuilder.setTestRecordId(runName);
+ runBuilder.setNumExpectedChildren(testCount);
+ runBuilder.setStartTime(createTimeStamp(System.currentTimeMillis()));
+
+ 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 testRunEnded(long elapsedTimeMillis, HashMap<String, Metric> runMetrics) {
+ TestRecord.Builder runBuilder = mLatestChild.pop();
+ runBuilder.setEndTime(createTimeStamp(System.currentTimeMillis()));
+ 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);
+ } catch (RuntimeException e) {
+ CLog.e("Failed to process test run end:");
+ CLog.e(e);
+ }
+ }
+
+ // test case events
+
+ @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 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 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());
+ }
+
+ // log events
+
+ @Override
+ public final void testLogSaved(
+ String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile) {
+ // Ignored
+ }
+
+ @Override
+ public final void setLogSaver(ILogSaver logSaver) {
+ // Ignored
+ }
+
+ @Override
+ public final void logAssociation(String dataName, LogFile logFile) {
+ TestRecord.Builder current = mLatestChild.peek();
+ Map<String, Any> fullmap = new HashMap<>();
+ fullmap.putAll(current.getArtifacts());
+ Any any = Any.pack(createFileProto(logFile));
+ fullmap.put(dataName, any);
+ current.putAllArtifacts(fullmap);
+ }
+
+ private ChildReference createChildReference(TestRecord record) {
+ return ChildReference.newBuilder().setInlineTestRecord(record).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 LogFileInfo createFileProto(LogFile logFile) {
+ LogFileInfo.Builder logFileBuilder = LogFileInfo.newBuilder();
+ logFileBuilder
+ .setPath(logFile.getPath())
+ .setUrl(logFile.getUrl())
+ .setIsText(logFile.isText())
+ .setIsCompressed(logFile.isCompressed())
+ .setSize(logFile.getSize());
+ return logFileBuilder.build();
+ }
+}