/*
* 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.result.type.ResultType;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** Utility class to parse the output from {@code xctest}. */
class XctestOutputParsing {
private static final Logger LOG = Logger.get(XctestOutputParsing.class);
// Utility class; do not instantiate.
private XctestOutputParsing() {}
private static final Pattern SUITE_STARTED_PATTERN =
Pattern.compile("Test Suite '(.*)' started at (.*)");
private static final Pattern SUITE_FINISHED_PATTERN =
Pattern.compile("Test Suite '(.*)' (finished|passed|failed) at (.*).");
private static final Pattern SUITE_FINISHED_DETAILS_PATTERN =
Pattern.compile(
"\t (Executed|Passed) (.*) tests?, with (.*) failures? \\((.*) unexpected\\) "
+ "in (.*) \\((.*)\\) seconds");
private static final Pattern CASE_STARTED_PATTERN =
Pattern.compile("Test Case '(.*)' started\\.?");
private static final Pattern CASE_FINISHED_PATTERN =
Pattern.compile("Test Case '(.*)' (passed|failed) \\((.*) seconds\\)\\.");
private static final Pattern ERROR_PATTERN = Pattern.compile("(.*):(.*): error: (.*) : (.*)");
private static final Pattern CASE_PATTERN = Pattern.compile("-\\[(\\S+) (\\S+)\\]");
public static class TestError {
@Nullable public String filePathInProject = null;
public int lineNumber = -1;
@Nullable public String reason = null;
}
public static class BeginXctestEvent {}
public static class EndXctestEvent {}
public static class BeginTestSuiteEvent {
@Nullable public String suite = null;
}
public static class EndTestSuiteEvent {
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 static class BeginTestCaseEvent {
@Nullable public String test = null;
@Nullable public String className = null;
@Nullable public String methodName = null;
}
public static class EndTestCaseEvent {
public double totalDuration = -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;
@Nullable public List<TestError> exceptions = null;
}
/** Callbacks invoked with events emitted by {@code xctest}. */
public interface XctestEventCallback {
void handleBeginXctestEvent(BeginXctestEvent event);
void handleEndXctestEvent(EndXctestEvent event);
void handleBeginTestSuiteEvent(BeginTestSuiteEvent event);
void handleEndTestSuiteEvent(EndTestSuiteEvent event);
void handleBeginTestCaseEvent(BeginTestCaseEvent event);
void handleEndTestCaseEvent(EndTestCaseEvent event);
}
public static TestResultSummary testResultSummaryForEndTestCaseEvent(EndTestCaseEvent event) {
long timeMillis = (long) (event.totalDuration * TimeUnit.SECONDS.toMillis(1));
TestResultSummary testResultSummary =
new TestResultSummary(
Optional.ofNullable(event.className).orElse(Preconditions.checkNotNull(event.test)),
Optional.ofNullable(event.methodName).orElse(Preconditions.checkNotNull(event.test)),
event.succeeded ? ResultType.SUCCESS : ResultType.FAILURE,
timeMillis,
formatTestMessage(event),
null, // stackTrace,
formatTestOutput(event),
null // stderr
);
LOG.debug("Test result summary: %s", testResultSummary);
return testResultSummary;
}
@Nullable
private static String formatTestMessage(EndTestCaseEvent endTestCaseEvent) {
if (endTestCaseEvent.exceptions != null && !endTestCaseEvent.exceptions.isEmpty()) {
StringBuilder exceptionsMessage = new StringBuilder();
for (TestError testException : endTestCaseEvent.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 formatTestOutput(EndTestCaseEvent event) {
if (event.output != null && !event.output.isEmpty()) {
return event.output;
} else {
return null;
}
}
/**
* Decodes a stream of output lines as produced by {@code xctest} and invokes the callbacks in
* {@code eventCallback} with each event in the stream.
*/
public static void streamOutput(Reader output, XctestEventCallback eventCallback) {
try {
LOG.debug("Began parsing xctest output");
BeginXctestEvent beginEvent = new BeginXctestEvent();
eventCallback.handleBeginXctestEvent(beginEvent);
try (BufferedReader outputBufferedReader = new BufferedReader(output)) {
LOG.debug("Created buffered readers");
String line;
while ((line = outputBufferedReader.readLine()) != null) {
handleLine(line, outputBufferedReader, eventCallback);
}
}
EndXctestEvent endEvent = new EndXctestEvent();
eventCallback.handleEndXctestEvent(endEvent);
LOG.debug("Finished parsing xctest output");
} catch (IOException e) {
LOG.warn(e, "Couldn't parse xctest output");
}
}
private static void handleLine(
String line, BufferedReader outputBufferedReader, XctestEventCallback eventCallback) {
LOG.debug("Parsing xctest line: %s", line);
Matcher suiteStartedMatcher = XctestOutputParsing.SUITE_STARTED_PATTERN.matcher(line);
if (suiteStartedMatcher.find()) {
handleSuiteStarted(suiteStartedMatcher, eventCallback);
return;
}
Matcher suiteFinishedMatcher = XctestOutputParsing.SUITE_FINISHED_PATTERN.matcher(line);
if (suiteFinishedMatcher.find()) {
handleSuiteFinished(suiteFinishedMatcher, outputBufferedReader, eventCallback);
return;
}
Matcher caseStartedMatcher = XctestOutputParsing.CASE_STARTED_PATTERN.matcher(line);
if (caseStartedMatcher.find()) {
handleCaseStarted(caseStartedMatcher, outputBufferedReader, eventCallback);
return;
}
LOG.debug("Line was not matched");
}
public static void handleSuiteStarted(Matcher matcher, XctestEventCallback eventCallback) {
BeginTestSuiteEvent event = new BeginTestSuiteEvent();
event.suite = matcher.group(1);
eventCallback.handleBeginTestSuiteEvent(event);
LOG.debug("Started suite: %s", event.suite);
}
public static void handleSuiteFinished(
Matcher matcher, BufferedReader outputBufferedReader, XctestEventCallback eventCallback) {
EndTestSuiteEvent event = new EndTestSuiteEvent();
event.suite = matcher.group(1);
String line;
try {
line = outputBufferedReader.readLine();
if (line == null) {
LOG.error("Missing suite finished details line");
return;
}
} catch (IOException e) {
LOG.error("Error reading finished details line");
return;
}
Matcher detailsMatcher = XctestOutputParsing.SUITE_FINISHED_DETAILS_PATTERN.matcher(line);
if (!detailsMatcher.find()) {
LOG.error("Invalid suite finished details line: %s", line);
return;
}
try {
event.testCaseCount = Integer.parseInt(detailsMatcher.group(2));
} catch (NumberFormatException e) {
LOG.error("Invalid test case count: %s", detailsMatcher.group(2));
}
try {
event.totalFailureCount = Integer.parseInt(detailsMatcher.group(3));
} catch (NumberFormatException e) {
LOG.error("Invalid total failures: %s", detailsMatcher.group(3));
}
try {
event.unexpectedExceptionCount = Integer.parseInt(detailsMatcher.group(4));
} catch (NumberFormatException e) {
LOG.error("Invalid unexpected failures: %s", detailsMatcher.group(4));
}
try {
event.testDuration = Double.parseDouble(detailsMatcher.group(5));
} catch (NumberFormatException e) {
LOG.error("Invalid test duration: %s", detailsMatcher.group(5));
}
try {
event.totalDuration = Double.parseDouble(detailsMatcher.group(6));
} catch (NumberFormatException e) {
LOG.error("Invalid total duration: %s", detailsMatcher.group(6));
}
eventCallback.handleEndTestSuiteEvent(event);
LOG.debug("Finished suite: %s", event.suite);
}
public static void handleCaseStarted(
Matcher matcher, BufferedReader outputBufferedReader, XctestEventCallback eventCallback) {
BeginTestCaseEvent event = new BeginTestCaseEvent();
event.test = matcher.group(1);
Matcher nameMatcher = XctestOutputParsing.CASE_PATTERN.matcher(event.test);
if (nameMatcher.find()) {
event.className = nameMatcher.group(1);
event.methodName = nameMatcher.group(2);
}
eventCallback.handleBeginTestCaseEvent(event);
LOG.debug("Started test case: %s", event.test);
StringBuilder output = new StringBuilder();
ImmutableList.Builder<TestError> exceptions = new ImmutableList.Builder<TestError>();
try {
String line;
while ((line = outputBufferedReader.readLine()) != null) {
LOG.debug("Parsing xctest case line: %s", line);
Matcher caseFinishedMatcher = XctestOutputParsing.CASE_FINISHED_PATTERN.matcher(line);
if (caseFinishedMatcher.find()) {
handleCaseFinished(
caseFinishedMatcher, exceptions.build(), output.toString(), eventCallback);
return;
}
Matcher errorMatcher = XctestOutputParsing.ERROR_PATTERN.matcher(line);
if (errorMatcher.find()) {
handleError(errorMatcher, exceptions);
continue;
}
output.append(line);
output.append("\n");
}
} catch (IOException e) {
LOG.error("Error reading line");
}
LOG.error("Test case did not end!");
// Synthesize a failure to note the test crashed or stopped.
EndTestCaseEvent endEvent = new EndTestCaseEvent();
endEvent.test = event.test;
endEvent.className = event.className;
endEvent.methodName = event.methodName;
endEvent.totalDuration = 0;
endEvent.output = output.toString();
endEvent.succeeded = false; // failure
endEvent.exceptions = exceptions.build();
eventCallback.handleEndTestCaseEvent(endEvent);
}
public static void handleCaseFinished(
Matcher matcher,
ImmutableList<TestError> exceptions,
String output,
XctestEventCallback eventCallback) {
EndTestCaseEvent event = new EndTestCaseEvent();
event.test = matcher.group(1);
Matcher nameMatcher = XctestOutputParsing.CASE_PATTERN.matcher(event.test);
if (nameMatcher.find()) {
event.className = nameMatcher.group(1);
event.methodName = nameMatcher.group(2);
}
try {
event.totalDuration = Double.parseDouble(matcher.group(3));
} catch (NumberFormatException e) {
LOG.warn("Invalid duration: %s", matcher.group(3));
}
event.output = output;
event.succeeded = matcher.group(2).equals("passed");
event.exceptions = exceptions;
eventCallback.handleEndTestCaseEvent(event);
LOG.debug(
"Finished %s: %s, duration %f",
event.test, event.succeeded ? "passed" : "failed", event.totalDuration);
}
public static void handleError(Matcher matcher, ImmutableList.Builder<TestError> exceptions) {
TestError error = new TestError();
error.filePathInProject = matcher.group(1);
try {
error.lineNumber = Integer.parseInt(matcher.group(2));
} catch (NumberFormatException e) {
LOG.warn("Invalid line number: %s", matcher.group(2));
}
error.reason = matcher.group(4);
exceptions.add(error);
LOG.debug("Test error: %s:%d: %s", error.filePathInProject, error.lineNumber, error.reason);
}
}