/*
* Copyright 2015-present Facebook, Inc.
*
* 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.facebook.buck.apple;
import com.facebook.buck.log.Logger;
import com.facebook.buck.test.TestResultSummary;
import com.facebook.buck.test.TestStatusMessage;
import com.facebook.buck.test.result.type.ResultType;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonStreamParser;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.annotation.Nullable;
/** Utility class to parse the output from {@code xctool -reporter json-stream}. */
class XctoolOutputParsing {
private static final Logger LOG = Logger.get(XctoolOutputParsing.class);
// Utility class; do not instantiate.
private XctoolOutputParsing() {}
public static class TestException {
@Nullable public String filePathInProject = null;
public int lineNumber = -1;
@Nullable public String reason = null;
}
public static class BeginOcunitEvent {
public double timestamp = -1;
@Nullable public String targetName = null;
}
public static class EndOcunitEvent {
public double timestamp = -1;
@Nullable public String message = null;
public boolean succeeded = false;
@Nullable public String targetName = null;
}
public static class StatusEvent {
public double timestamp = -1;
@Nullable public String message = null;
@Nullable public String level = null;
}
public static class BeginTestSuiteEvent {
public double timestamp = -1;
@Nullable public String suite = null;
}
public static class EndTestSuiteEvent {
public double timestamp = -1;
public double totalDuration = -1;
public double testDuration = -1;
@Nullable public String suite = null;
public int testCaseCount = -1;
public int totalFailureCount = -1;
public int unexpectedExceptionCount = -1;
public boolean succeeded = false;
}
public static class BeginTestEvent {
public double timestamp = -1;
@Nullable public String test = null;
@Nullable public String className = null;
@Nullable public String methodName = null;
}
public static class EndTestEvent {
public double totalDuration = -1;
public double timestamp = -1;
@Nullable public String test = null;
@Nullable public String className = null;
@Nullable public String methodName = null;
@Nullable public String output = null;
public boolean succeeded = false;
public List<TestException> exceptions = new ArrayList<>();
}
/** Callbacks invoked with events emitted by {@code xctool -reporter json-stream}. */
public interface XctoolEventCallback {
void handleBeginOcunitEvent(BeginOcunitEvent event);
void handleEndOcunitEvent(EndOcunitEvent event);
void handleBeginStatusEvent(StatusEvent event);
void handleEndStatusEvent(StatusEvent event);
void handleBeginTestSuiteEvent(BeginTestSuiteEvent event);
void handleEndTestSuiteEvent(EndTestSuiteEvent event);
void handleBeginTestEvent(BeginTestEvent event);
void handleEndTestEvent(EndTestEvent event);
}
/**
* Decodes a stream of JSON objects as produced by {@code xctool -reporter json-stream} and
* invokes the callbacks in {@code eventCallback} with each event in the stream.
*/
public static void streamOutputFromReader(Reader reader, XctoolEventCallback eventCallback) {
Gson gson = new Gson();
JsonStreamParser streamParser = new JsonStreamParser(reader);
try {
while (streamParser.hasNext()) {
dispatchEventCallback(gson, streamParser.next(), eventCallback);
}
} catch (JsonParseException e) {
LOG.warn(e, "Couldn't parse xctool JSON stream");
}
}
private static void dispatchEventCallback(
Gson gson, JsonElement element, XctoolEventCallback eventCallback) throws JsonParseException {
LOG.debug("Parsing xctool event: %s", element);
if (!element.isJsonObject()) {
LOG.warn("Couldn't parse JSON object from xctool event: %s", element);
return;
}
JsonObject object = element.getAsJsonObject();
if (!object.has("event")) {
LOG.warn("Couldn't parse JSON event from xctool event: %s", element);
return;
}
JsonElement event = object.get("event");
if (event == null || !event.isJsonPrimitive()) {
LOG.warn("Couldn't parse event field from xctool event: %s", element);
return;
}
switch (event.getAsString()) {
case "begin-ocunit":
eventCallback.handleBeginOcunitEvent(gson.fromJson(element, BeginOcunitEvent.class));
break;
case "end-ocunit":
eventCallback.handleEndOcunitEvent(gson.fromJson(element, EndOcunitEvent.class));
break;
case "begin-status":
eventCallback.handleBeginStatusEvent(gson.fromJson(element, StatusEvent.class));
break;
case "end-status":
eventCallback.handleEndStatusEvent(gson.fromJson(element, StatusEvent.class));
break;
case "begin-test-suite":
eventCallback.handleBeginTestSuiteEvent(gson.fromJson(element, BeginTestSuiteEvent.class));
break;
case "end-test-suite":
eventCallback.handleEndTestSuiteEvent(gson.fromJson(element, EndTestSuiteEvent.class));
break;
case "begin-test":
eventCallback.handleBeginTestEvent(gson.fromJson(element, BeginTestEvent.class));
break;
case "end-test":
eventCallback.handleEndTestEvent(gson.fromJson(element, EndTestEvent.class));
break;
}
}
public static Optional<TestStatusMessage> testStatusMessageForStatusEvent(
StatusEvent statusEvent) {
if (statusEvent.message == null || statusEvent.level == null) {
LOG.warn("Ignoring invalid status (message or level is null): %s", statusEvent);
return Optional.empty();
}
Level level;
switch (statusEvent.level) {
case "Verbose":
level = Level.FINER;
break;
case "Debug":
level = Level.FINE;
break;
case "Info":
level = Level.INFO;
break;
case "Warning":
level = Level.WARNING;
break;
case "Error":
level = Level.SEVERE;
break;
default:
LOG.warn("Ignoring invalid status (unknown level %s)", statusEvent.level);
return Optional.empty();
}
long timeMillis = (long) (statusEvent.timestamp * TimeUnit.SECONDS.toMillis(1));
return Optional.of(TestStatusMessage.of(statusEvent.message, level, timeMillis));
}
public static TestResultSummary testResultSummaryForEndTestEvent(EndTestEvent endTestEvent) {
long timeMillis = (long) (endTestEvent.totalDuration * TimeUnit.SECONDS.toMillis(1));
TestResultSummary testResultSummary =
new TestResultSummary(
Preconditions.checkNotNull(endTestEvent.className),
Preconditions.checkNotNull(endTestEvent.test),
endTestEvent.succeeded ? ResultType.SUCCESS : ResultType.FAILURE,
timeMillis,
formatTestMessage(endTestEvent),
null, // stackTrace,
formatTestStdout(endTestEvent),
null // stdErr
);
LOG.debug("Test result summary: %s", testResultSummary);
return testResultSummary;
}
@Nullable
private static String formatTestMessage(EndTestEvent endTestEvent) {
if (endTestEvent.exceptions != null && !endTestEvent.exceptions.isEmpty()) {
StringBuilder exceptionsMessage = new StringBuilder();
for (TestException testException : endTestEvent.exceptions) {
if (exceptionsMessage.length() > 0) {
exceptionsMessage.append('\n');
}
exceptionsMessage
.append(testException.filePathInProject)
.append(':')
.append(testException.lineNumber)
.append(": ")
.append(testException.reason);
}
return exceptionsMessage.toString();
}
return null;
}
@Nullable
private static String formatTestStdout(EndTestEvent endTestEvent) {
if (endTestEvent.output != null && !endTestEvent.output.isEmpty()) {
return endTestEvent.output;
} else {
return null;
}
}
public static Optional<TestResultSummary> internalErrorForEndOcunitEvent(
EndOcunitEvent endOcunitEvent) {
if (endOcunitEvent.succeeded || endOcunitEvent.message == null) {
// We only care about failures with a message. (Failures without a message always
// happen with any random test failure.)
return Optional.empty();
}
TestResultSummary testResultSummary =
new TestResultSummary(
"Internal error from test runner",
"main",
ResultType.FAILURE,
0L,
endOcunitEvent.message,
null, // stackTrace,
null, // stdOut
null // stdErr
);
LOG.debug("OCUnit/XCTest internal failure: %s", testResultSummary);
return Optional.of(testResultSummary);
}
}