Skip to content

Commit

Permalink
[Core] Support diff toolwindow in IntelliJ IDEA (#2608)
Browse files Browse the repository at this point in the history
For the toolwindow are required the "expected" and "actual" attributes
in the test-failed-output message.

Fixes: #2607

Co-authored-by: Andrey Vokin <[email protected]>
Co-authored-by: M.P. Korstanje <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2022
1 parent 3ea5401 commit 99c22e1
Showing 5 changed files with 153 additions and 9 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -10,8 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- [Core] Support comparison of expected and actual values in IntelliJ IDEA ([#2607](https://github.com/cucumber/cucumber-jvm/issues/2607)
- [Datatable] Support parsing Booleans in Datatables ([#2614](https://github.com/cucumber/cucumber-jvm/pull/2614) G. Jourdan-Weil)

## [7.7.0] - 2022-09-08
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@

import static io.cucumber.core.exception.ExceptionUtils.printStackTrace;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;

/**
@@ -77,6 +78,9 @@ public class TeamCityPlugin implements EventListener {
+ "[testFinished timestamp = '%s' duration = '%s' name = '%s']";
private static final String TEMPLATE_TEST_FAILED = TEAMCITY_PREFIX
+ "[testFailed timestamp = '%s' duration = '%s' message = '%s' details = '%s' name = '%s']";

private static final String TEMPLATE_TEST_COMPARISON_FAILED = TEAMCITY_PREFIX
+ "[testFailed timestamp = '%s' duration = '%s' message = '%s' details = '%s' expected = '%s' actual = '%s' name = '%s']";
private static final String TEMPLATE_TEST_IGNORED = TEAMCITY_PREFIX
+ "[testIgnored timestamp = '%s' duration = '%s' message = '%s' name = '%s']";

@@ -101,6 +105,24 @@ public class TeamCityPlugin implements EventListener {
private static final Pattern ANNOTATION_GLUE_CODE_LOCATION_PATTERN = Pattern.compile("^(.*)\\.(.*)\\([^:]*\\)");
private static final Pattern LAMBDA_GLUE_CODE_LOCATION_PATTERN = Pattern.compile("^(.*)\\.(.*)\\(.*:.*\\)");

private static final Pattern[] COMPARE_PATTERNS = new Pattern[] {
// Hamcrest 2 MatcherAssert.assertThat
Pattern.compile("expected: (.*)(?:\r\n|\r|\n) {5}but: was (.*)$",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE),
// AssertJ 3 ShouldBeEqual.smartErrorMessage
Pattern.compile("expected: (.*)(?:\r\n|\r|\n) but was: (.*)$",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE),
// JUnit 5 AssertionFailureBuilder
Pattern.compile("expected: <(.*)> but was: <(.*)>$",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE),
// JUnit 4 Assert.assertEquals
Pattern.compile("expected:\\s?<(.*)> but was:\\s?<(.*)>$",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE),
// TestNG 7 Assert.assertEquals
Pattern.compile("expected \\[(.*)] but found \\[(.*)]\n$",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE),
};

private final PrintStream out;
private final List<SnippetsSuggestedEvent> suggestions = new ArrayList<>();
private final Map<URI, Collection<Node>> parsedTestSources = new HashMap<>();
@@ -281,7 +303,18 @@ private void printTestStepFinished(TestStepFinished event) {
case AMBIGUOUS:
case FAILED: {
String details = printStackTrace(error);
print(TEMPLATE_TEST_FAILED, timeStamp, duration, "Step failed", details, name);
String message = error.getMessage();
if (message == null) {
print(TEMPLATE_TEST_FAILED, timeStamp, duration, "Step failed", details, name);
break;
}
ComparisonFailure comparisonFailure = ComparisonFailure.parse(message.trim());
if (comparisonFailure == null) {
print(TEMPLATE_TEST_FAILED, timeStamp, duration, "Step failed", details, name);
break;
}
print(TEMPLATE_TEST_COMPARISON_FAILED, timeStamp, duration, "Step failed", details,
comparisonFailure.getExpected(), comparisonFailure.getActual(), name);
break;
}
default:
@@ -420,4 +453,43 @@ private String escape(String source) {
.replace("]", "|]");
}

private static class ComparisonFailure {

static ComparisonFailure parse(String message) {
for (Pattern pattern : COMPARE_PATTERNS) {
ComparisonFailure result = parse(message, pattern);
if (result != null) {
return result;
}
}
return null;
}

static ComparisonFailure parse(String message, Pattern pattern) {
final Matcher matcher = pattern.matcher(message);
if (!matcher.find()) {
return null;
}
String expected = matcher.group(1);
String actual = matcher.group(2);
return new ComparisonFailure(expected, actual);
}

private final String expected;

private final String actual;

ComparisonFailure(String expected, String actual) {
this.expected = requireNonNull(expected);
this.actual = requireNonNull(actual);
}

public String getExpected() {
return expected;
}

public String getActual() {
return actual;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.cucumber.core.backend;

public class StubLocation implements Located {

private final String location;

public StubLocation(String location) {
this.location = location;
}

@Override
public boolean isDefinedAt(StackTraceElement stackTraceElement) {
return false;
}

@Override
public String getLocation() {
return location;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.cucumber.core.backend;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.stream.Collectors;
@@ -12,8 +13,8 @@ public class StubStepDefinition implements StepDefinition {
private static final String STUBBED_LOCATION_WITH_DETAILS = "{stubbed location with details}";
private final List<ParameterInfo> parameterInfos;
private final String expression;
private final RuntimeException exception;
private final String location;
private final Throwable exception;
private final Located location;

public StubStepDefinition(String pattern, String location, Type... types) {
this(pattern, location, null, types);
@@ -23,14 +24,14 @@ public StubStepDefinition(String pattern, Type... types) {
this(pattern, STUBBED_LOCATION_WITH_DETAILS, null, types);
}

public StubStepDefinition(String pattern, RuntimeException exception, Type... types) {
public StubStepDefinition(String pattern, Throwable exception, Type... types) {
this(pattern, STUBBED_LOCATION_WITH_DETAILS, exception, types);
}

public StubStepDefinition(String pattern, String location, RuntimeException exception, Type... types) {
public StubStepDefinition(String pattern, String location, Throwable exception, Type... types) {
this.parameterInfos = Stream.of(types).map(StubParameterInfo::new).collect(Collectors.toList());
this.expression = pattern;
this.location = location;
this.location = new StubLocation(location);
this.exception = exception;
}

@@ -41,13 +42,16 @@ public boolean isDefinedAt(StackTraceElement stackTraceElement) {

@Override
public String getLocation() {
return location;
return location.getLocation();
}

@Override
public void execute(Object[] args) {
if (exception != null) {
throw exception;
if (exception instanceof CucumberBackendException) {
throw (CucumberBackendException) exception;
}
throw new CucumberInvocationTargetException(location, new InvocationTargetException(exception));
}

assertEquals(parameterInfos.size(), args.length);
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure;
import static org.junit.jupiter.api.Assertions.assertThrows;

@DisabledOnOs(OS.WINDOWS)
@@ -342,4 +343,50 @@ void should_print_system_failure_for_failed_hooks() {
"##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Before All/After All']"));
}

@Test
void should_print_comparison_failure_for_failed_assert_equal() {
Feature feature = TestFeatureParser.parse("path/test.feature", "" +
"Feature: feature name\n" +
" Scenario: scenario name\n" +
" Given first step\n");

ByteArrayOutputStream out = new ByteArrayOutputStream();
Runtime.builder()
.withFeatureSupplier(new StubFeatureSupplier(feature))
.withAdditionalPlugins(new TeamCityPlugin(new PrintStream(out)))
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
emptyList(),
singletonList(
new StubStepDefinition("first step", assertionFailure().expected(1).actual(2).build())),
emptyList()))
.build()
.run();

assertThat(out, bytesContainsString("expected = '1' actual = '2' name = 'first step']"));
}

@Test
void should_print_comparison_failure_for_failed_assert_equal_with_prefix() {
Feature feature = TestFeatureParser.parse("path/test.feature", "" +
"Feature: feature name\n" +
" Scenario: scenario name\n" +
" Given first step\n");

ByteArrayOutputStream out = new ByteArrayOutputStream();
Runtime.builder()
.withFeatureSupplier(new StubFeatureSupplier(feature))
.withAdditionalPlugins(new TeamCityPlugin(new PrintStream(out)))
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
emptyList(),
singletonList(
new StubStepDefinition("first step",
assertionFailure().message("oops").expected("one value").actual("another value").build())),
emptyList()))
.build()
.run();

assertThat(out, bytesContainsString("expected = 'one value' actual = 'another value' name = 'first step']"));
}
}

0 comments on commit 99c22e1

Please sign in to comment.