/* * Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan * * 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.goide.runconfig.testing.frameworks.gocheck; import com.goide.runconfig.testing.GoTestEventsConverterBase; import com.goide.runconfig.testing.GoTestLocator; import com.intellij.execution.testframework.TestConsoleProperties; import com.intellij.execution.testframework.sm.ServiceMessageBuilder; import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.text.ParseException; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.intellij.openapi.util.Pair.pair; public class GocheckEventsConverter extends OutputToGeneralTestEventsConverter implements GoTestEventsConverterBase { private static final String FRAMEWORK_NAME = "gocheck"; /* * Scope hierarchy looks like this: * * GLOBAL * SUITE * SUITE_SETUP * TEST * TEST_SETUP * TEST_TEARDOWN * SUITE_TEARDOWN */ private enum Scope { GLOBAL, SUITE, SUITE_SETUP, SUITE_TEARDOWN, TEST, TEST_SETUP, TEST_TEARDOWN } private static final Pattern SUITE_START = Pattern.compile("=== RUN (.+)\\s*$"); private static final Pattern SUITE_END = Pattern.compile("((PASS)|(FAIL))\\s*$"); private static final Pattern TEST_START = Pattern.compile("(.*)START: [^:]+:\\d+: ([^\\s]+)\\s*$"); private static final Pattern TEST_PASSED = Pattern.compile("(.*)PASS: [^:]+:\\d+: ([^\\s]+)\\t[^\\s]+\\s*$"); private static final Pattern TEST_FAILED = Pattern.compile("(.*)FAIL: [^:]+:\\d+: ([^\\s]+)\\s*$"); private static final Pattern TEST_PANICKED = Pattern.compile("(.*)PANIC: [^:]+:\\d+: ([^\\s]+)\\s*$"); private static final Pattern TEST_MISSED = Pattern.compile("(.*)MISS: [^:]+:\\d+: ([^\\s]+)\\s*$"); private static final Pattern TEST_SKIPPED = Pattern.compile("(.*)SKIP: [^:]+:\\d+: ([^\\s]+)( \\(.*\\))?\\s*$"); private static final Pattern ERROR_LOCATION = Pattern.compile("(.*:\\d+):\\s*$"); private static final Pattern ERROR_ACTUAL = Pattern.compile("\\.\\.\\. ((obtained)|(value)) (.*?)( \\+)?\\s*$"); private static final Pattern ERROR_EXPECTED = Pattern.compile("\\.\\.\\. ((expected)|(regex)) (.*?)( \\+)?\\s*$"); private static final Pattern ERROR_CONTINUATION = Pattern.compile("\\.\\.\\. {5}(.*?)( +\\+)?\\s*$"); private static final Pattern PANIC_VALUE = Pattern.compile("(.*)\\.\\.\\. (Panic: .* \\(.*\\)\\s*)$"); private Scope myScope = Scope.GLOBAL; private String mySuiteName; private String myTestName; private long myCurrentTestStart; private TestResult myFixtureFailure; private List<String> myStdOut; private enum Status { PASSED, FAILED, PANICKED, MISSED, SKIPPED } private static final class TestResult { private final Status myStatus; private final Map<String, String> myAttributes = ContainerUtil.newHashMap(); TestResult(@NotNull Status status) { this(status, null); } TestResult(@NotNull Status status, @Nullable Map<String, String> attributes) { myStatus = status; if (attributes != null) myAttributes.putAll(attributes); } @NotNull public Status getStatus() { return myStatus; } public void addAttributesTo(@NotNull ServiceMessageBuilder serviceMessageBuilder) { for (Map.Entry<String, String> entry : myAttributes.entrySet()) { serviceMessageBuilder.addAttribute(entry.getKey(), entry.getValue()); } } } public GocheckEventsConverter(@NotNull TestConsoleProperties consoleProperties) { super(FRAMEWORK_NAME, consoleProperties); } @Override public boolean processServiceMessages(@NotNull String text, Key outputType, ServiceMessageVisitor visitor) throws ParseException { Matcher matcher; switch (myScope) { case GLOBAL: if (SUITE_START.matcher(text).matches()) { myScope = Scope.SUITE; return true; } break; case SUITE: if ((matcher = TEST_START.matcher(text)).matches()) { myStdOut = ContainerUtil.newArrayList(); myTestName = matcher.group(2); processTestSectionStart(myTestName, outputType, visitor); if (myTestName.endsWith(".SetUpSuite")) { myScope = Scope.SUITE_SETUP; return true; } if (myTestName.endsWith(".TearDownSuite")) { myScope = Scope.SUITE_TEARDOWN; return true; } myScope = Scope.TEST; return processTestStarted(myTestName, outputType, visitor); } if (SUITE_END.matcher(text).matches()) { myScope = Scope.GLOBAL; if (mySuiteName != null) { String suiteFinishedMsg = ServiceMessageBuilder.testSuiteFinished(mySuiteName).toString(); super.processServiceMessages(suiteFinishedMsg, outputType, visitor); processStdOut("SuiteTearDown", outputType, visitor); } return true; } break; case SUITE_SETUP: TestResult suiteSetUpResult = detectTestResult(text, true); if (suiteSetUpResult != null) { myScope = Scope.SUITE; if (suiteSetUpResult.getStatus() != Status.PASSED) { myFixtureFailure = suiteSetUpResult; } return true; } break; case SUITE_TEARDOWN: if (detectTestResult(text, false) != null) { myScope = Scope.SUITE; return true; } break; case TEST: if ((matcher = TEST_START.matcher(text)).matches()) { String stdOutLeftover = matcher.group(1); if (!StringUtil.isEmptyOrSpaces(stdOutLeftover)) { myStdOut.add(stdOutLeftover); } String testName = matcher.group(2); if (testName.endsWith(".SetUpTest")) { myScope = Scope.TEST_SETUP; return true; } if (testName.endsWith(".TearDownTest")) { myScope = Scope.TEST_TEARDOWN; return true; } } TestResult testResult = detectTestResult(text, true); if (testResult != null) { myScope = Scope.SUITE; if (StringUtil.notNullize(testResult.myAttributes.get("details")).contains("Fixture has panicked") || (testResult.getStatus() == Status.MISSED || testResult.getStatus() == Status.SKIPPED) && myFixtureFailure != null) { testResult = myFixtureFailure; } myFixtureFailure = null; processTestResult(testResult, outputType, visitor); return true; } break; case TEST_SETUP: TestResult testSetUpResult = detectTestResult(text, true); if (testSetUpResult != null) { myScope = Scope.TEST; if (testSetUpResult.getStatus() != Status.PASSED) { myFixtureFailure = testSetUpResult; } return true; } break; case TEST_TEARDOWN: boolean isSetUpFailed = myFixtureFailure != null; TestResult testTearDownResult = detectTestResult(text, !isSetUpFailed); if (testTearDownResult != null) { myScope = Scope.TEST; if (!isSetUpFailed && testTearDownResult.getStatus() != Status.PASSED) { myFixtureFailure = testTearDownResult; } return true; } break; } if (myStdOut != null) { myStdOut.add(text); return true; } return super.processServiceMessages(text, outputType, visitor); } @Nullable private TestResult detectTestResult(String text, boolean parseDetails) { Matcher matcher; if ((matcher = TEST_PASSED.matcher(text)).matches()) { myStdOut.add(StringUtil.notNullize(matcher.group(1)).trim()); return new TestResult(Status.PASSED); } if ((matcher = TEST_MISSED.matcher(text)).matches()) { myStdOut.add(StringUtil.notNullize(matcher.group(1)).trim()); return new TestResult(Status.MISSED); } if ((matcher = TEST_SKIPPED.matcher(text)).matches()) { myStdOut.add(StringUtil.notNullize(matcher.group(1)).trim()); return new TestResult(Status.SKIPPED); } if ((matcher = TEST_FAILED.matcher(text)).matches()) { myStdOut.add(StringUtil.notNullize(matcher.group(1)).trim()); if (parseDetails) { return new TestResult(Status.FAILED, parseFailureAttributes()); } return new TestResult(Status.FAILED); } if ((matcher = TEST_PANICKED.matcher(text)).matches()) { myStdOut.add(StringUtil.notNullize(matcher.group(1)).trim()); if (parseDetails) { return new TestResult(Status.PANICKED, parsePanickedAttributes()); } return new TestResult(Status.FAILED); } return null; } private boolean processTestStarted(@NotNull String testName, Key outputType, ServiceMessageVisitor visitor) throws ParseException { String testStartedMsg = ServiceMessageBuilder.testStarted(testName) .addAttribute("locationHint", testUrl(testName)).toString(); return super.processServiceMessages(testStartedMsg, outputType, visitor); } private void processTestResult(@NotNull TestResult testResult, Key outputType, ServiceMessageVisitor visitor) throws ParseException { processStdOut(myTestName, outputType, visitor); switch (testResult.getStatus()) { case PASSED: break; case MISSED: case SKIPPED: String testIgnoredStr = ServiceMessageBuilder.testIgnored(myTestName).toString(); super.processServiceMessages(testIgnoredStr, outputType, visitor); break; case FAILED: ServiceMessageBuilder testError = ServiceMessageBuilder.testFailed(myTestName); testResult.addAttributesTo(testError); super.processServiceMessages(testError.toString(), outputType, visitor); break; case PANICKED: ServiceMessageBuilder testPanicked = ServiceMessageBuilder.testFailed(myTestName); testResult.addAttributesTo(testPanicked); super.processServiceMessages(testPanicked.toString(), outputType, visitor); break; default: throw new RuntimeException("Unexpected test result: " + testResult); } long duration = System.currentTimeMillis() - myCurrentTestStart; String testFinishedMsg = ServiceMessageBuilder.testFinished(myTestName).addAttribute("duration", Long.toString(duration)).toString(); super.processServiceMessages(testFinishedMsg, outputType, visitor); } private void processStdOut(@NotNull String testName, Key outputType, ServiceMessageVisitor visitor) throws ParseException { if (myStdOut == null) { return; } String allStdOut = StringUtil.join(myStdOut, ""); if (!StringUtil.isEmptyOrSpaces(allStdOut)) { String testStdOutMsg = ServiceMessageBuilder.testStdOut(testName).addAttribute("out", allStdOut).toString(); super.processServiceMessages(testStdOutMsg, outputType, visitor); } myStdOut = null; } private void processTestSectionStart(@NotNull String testName, Key outputType, ServiceMessageVisitor visitor) throws ParseException { String suiteName = testName.substring(0, testName.indexOf(".")); myTestName = testName; myCurrentTestStart = System.currentTimeMillis(); if (!suiteName.equals(mySuiteName)) { if (mySuiteName != null) { String suiteFinishedMsg = ServiceMessageBuilder.testSuiteFinished(mySuiteName).toString(); super.processServiceMessages(suiteFinishedMsg, outputType, visitor); } mySuiteName = suiteName; String suiteStartedMsg = ServiceMessageBuilder.testSuiteStarted(suiteName) .addAttribute("locationHint", suiteUrl(suiteName)).toString(); super.processServiceMessages(suiteStartedMsg, outputType, visitor); } } /** * Parses assertion error report into a set of SystemMessage attributes. * <p/> * An assertion error report usually looks like this: * <pre> * all_fail_test.go:36: * c.Assert("Foo", Equals, "Bar") * ... obtained string = "Foo" * ... expected string = "Bar" * </pre> * or this: * <pre> * all_fail_test.go:21: * c.Assert("Foo", IsNil) * ... value string = "Foo" * </pre> * or this: * <pre> * all_fail_test.go:54: * c.Assert(`multi * * line * string`, * Equals, * `Another * multi * line * string`) * ... obtained string = "" + * ... "multi\n" + * ... "\n" + * ... "\t line\n" + * ... "\t string" * ... expected string = "" + * ... "Another\n" + * ... "multi\n" + * ... "\tline\n" + * ... "\t\tstring" * </pre> * There are other variation. Check out the respective unit test. * * @return a map of system message attributes. */ @Nullable private Map<String, String> parseFailureAttributes() { if (myStdOut == null || myStdOut.isEmpty()) return null; int lineNumber = myStdOut.size() - 1; StringBuilder expectedMessage = new StringBuilder(); StringBuilder actualMessage = new StringBuilder(); StringBuilder errorMessage = new StringBuilder(); String details = ""; // Skip forward to the error description. while (lineNumber >= 0 && !StringUtil.startsWith(myStdOut.get(lineNumber), "...")) { lineNumber--; } lineNumber = collectErrorMessage(myStdOut, lineNumber, ERROR_CONTINUATION, ERROR_EXPECTED, expectedMessage); lineNumber = collectErrorMessage(myStdOut, lineNumber, ERROR_CONTINUATION, ERROR_ACTUAL, actualMessage); // Collect all lines of the error message and details while (lineNumber >= 0) { String line = myStdOut.get(lineNumber); Matcher matcher = ERROR_LOCATION.matcher(line); if (matcher.matches()) { details = matcher.group(1); break; } else { errorMessage.insert(0, line); lineNumber--; } } // Remove the assertion error info from the test StdOut. myStdOut = safeSublist(myStdOut, lineNumber); return ContainerUtil.newHashMap(pair("expected", expectedMessage.toString().trim()), pair("actual", actualMessage.toString().trim()), pair("type", "comparisonFailure"), pair("message", errorMessage.toString().trim()), pair("details", details)); } private static int collectErrorMessage(List<String> lines, int currentLine, Pattern continuationPattern, Pattern messagePattern, StringBuilder result) { while (currentLine >= 0) { String line = lines.get(currentLine); Matcher continuationMatcher = continuationPattern.matcher(line); if (continuationMatcher.matches()) { result.insert(0, '\n').insert(0, continuationMatcher.group(1)); currentLine--; continue; } Matcher messageMatcher = messagePattern.matcher(line); if (messageMatcher.matches()) { result.insert(0, '\n').insert(0, messageMatcher.group(4)); currentLine--; } break; } return currentLine; } /** * Parses panic report into a set of SystemMessage attributes. * <p/> * A panic report usually looks like this: * <pre> * ... Panic: bar (PC=0x3B0A5) * * /usr/local/go/src/runtime/panic.go:387 * in gopanic * some_panic_test.go:31 * in SomePanicSuite.TestD * /usr/local/go/src/reflect/value.go:296 * in Value.Call * /usr/local/go/src/runtime/asm_amd64.s:2232 * in goexit * </pre> * * @return a map of system message attributes. */ @Nullable private Map<String, String> parsePanickedAttributes() { if (myStdOut == null || myStdOut.isEmpty()) return null; int lineNumber = myStdOut.size() - 1; // Ignore trailing empty lines. while (lineNumber >= 0 && StringUtil.isEmptyOrSpaces(myStdOut.get(lineNumber))) { lineNumber--; } StringBuilder detailsMessage = new StringBuilder(); // All lines up until an empty one comprise the stack trace. while (lineNumber >= 0 && !StringUtil.isEmptyOrSpaces(myStdOut.get(lineNumber))) { detailsMessage.insert(0, myStdOut.get(lineNumber)); lineNumber--; } lineNumber--; // skip empty line // Then follows the panic description. String errorMessage = ""; Matcher matcher; if (lineNumber >= 0 && (matcher = PANIC_VALUE.matcher(myStdOut.get(lineNumber))).matches()) { String stdoutLeftover = matcher.group(1); if (!StringUtil.isEmptyOrSpaces(stdoutLeftover)) { myStdOut.set(lineNumber, stdoutLeftover); lineNumber++; } errorMessage = matcher.group(2); } // Remove the panic info from the test StdOut. myStdOut = safeSublist(myStdOut, lineNumber); return ContainerUtil.newHashMap(pair("details", detailsMessage.toString()), pair("message", errorMessage)); } @NotNull private static List<String> safeSublist(@NotNull List<String> list, int until) { if (0 < until && until <= list.size() - 1) { return list.subList(0, until); } return ContainerUtil.newArrayList(); } @NotNull private static String suiteUrl(@NotNull String suiteName) { return GoTestLocator.SUITE_PROTOCOL + "://" + suiteName; } @NotNull private static String testUrl(@NotNull String testName) { return GoTestLocator.PROTOCOL + "://" + testName; } }