Skip to content

Use a message based TeamCityPlugin #3007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cucumber-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<html-formatter.version>21.10.1</html-formatter.version>
<junit-xml-formatter.version>0.7.1</junit-xml-formatter.version>
<messages.version>27.2.0</messages.version>
<query.version>13.2.0</query.version>
<query.version>13.3.0</query.version>
<tag-expressions.version>6.1.2</tag-expressions.version>
<testng-xml-formatter.version>0.3.1</testng-xml-formatter.version>
</properties>
Expand Down
725 changes: 445 additions & 280 deletions cucumber-core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java

Large diffs are not rendered by default.

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

import java.util.Optional;
import java.util.function.Consumer;

public class StubHookDefinition implements HookDefinition {
Expand All @@ -8,31 +9,46 @@ public class StubHookDefinition implements HookDefinition {
private final String location;
private final RuntimeException exception;
private final Consumer<TestCaseState> action;
private final SourceReference sourceReference;
private final HookType hookType;

public StubHookDefinition(String location, RuntimeException exception, Consumer<TestCaseState> action) {
public StubHookDefinition(
String location, RuntimeException exception, Consumer<TestCaseState> action,
SourceReference sourceReference, HookType hookType
) {
this.location = location;
this.exception = exception;
this.action = action;
this.sourceReference = sourceReference;
this.hookType = hookType;
}

public StubHookDefinition(String location, Consumer<TestCaseState> action) {
this(location, null, action);
this(location, null, action, null, null);
}

public StubHookDefinition() {
this(STUBBED_LOCATION_WITH_DETAILS, null, null);
this(STUBBED_LOCATION_WITH_DETAILS, null, null, null, null);
}

public StubHookDefinition(Consumer<TestCaseState> action) {
this(STUBBED_LOCATION_WITH_DETAILS, null, action);
this(STUBBED_LOCATION_WITH_DETAILS, null, action, null, null);
}

public StubHookDefinition(RuntimeException exception) {
this(STUBBED_LOCATION_WITH_DETAILS, exception, null);
this(STUBBED_LOCATION_WITH_DETAILS, exception, null, null, null);
}

public StubHookDefinition(String location) {
this(location, null, null);
this(location, null, null, null, null);
}

public StubHookDefinition(SourceReference sourceReference, HookType hookType) {
this(null, null, null, sourceReference, hookType);
}

public StubHookDefinition(SourceReference sourceReference, HookType hookType, RuntimeException exception) {
this(null, exception, null, sourceReference, hookType);
}

@Override
Expand Down Expand Up @@ -65,4 +81,13 @@ public String getLocation() {
return location;
}

@Override
public Optional<SourceReference> getSourceReference() {
return Optional.ofNullable(sourceReference);
}

@Override
public Optional<HookType> getHookType() {
return Optional.ofNullable(hookType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
Expand All @@ -24,6 +24,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -152,7 +153,7 @@ void lazy_init() {

private static void checkUuidProperties(List<UUID> uuids) {
// all UUIDs are non-null
assertTrue(uuids.stream().filter(Objects::isNull).findFirst().isEmpty());
assertFalse(uuids.stream().anyMatch(Objects::isNull));

// UUID version is always 8
List<Integer> versions = uuids.stream().map(UUID::version).distinct().collect(Collectors.toList());
Expand Down Expand Up @@ -331,7 +332,7 @@ protected Class<?> findClass(String name) {

private byte[] loadClassBytesFromDisk(String className) {
try {
return Files.readAllBytes(Path.of(Objects.requireNonNull(NonCachingClassLoader.class
return Files.readAllBytes(Paths.get(Objects.requireNonNull(NonCachingClassLoader.class
.getResource(className.replaceFirst(".+\\.", "") + ".class")).toURI()));
} catch (IOException e) {
throw new RuntimeException("Unable to read file from disk");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ private StubException(String className, String message, String stacktrace) {
}

public StubException() {
this("the stack trace");
this("stub exception");
}

public StubException(String message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
import java.io.PrintStream;
import java.util.UUID;

import static io.cucumber.core.backend.HookDefinition.HookType.BEFORE;
import static io.cucumber.core.plugin.Bytes.bytes;
import static io.cucumber.core.plugin.TeamCityPluginTestStepDefinition.getAnnotationSourceReference;
import static io.cucumber.core.plugin.TeamCityPluginTestStepDefinition.getStackSourceReference;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.Clock.fixed;
import static java.time.Instant.EPOCH;
import static java.time.ZoneId.of;
Expand Down Expand Up @@ -72,7 +76,7 @@ void should_handle_scenario_outline() {
"##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location
+ "path/test.feature:5' name = 'examples name']\n" +
"##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location
+ "path/test.feature:7' name = 'Example #1.1']\n" +
+ "path/test.feature:7' name = '#1.1: name 1']\n" +
"##teamcity[customProgressStatus type = 'testStarted' timestamp = '1970-01-01T12:00:00.000+0000']\n" +
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location
+ "path/test.feature:3' captureStandardOutput = 'true' name = 'first step']\n" +
Expand All @@ -83,9 +87,10 @@ void should_handle_scenario_outline() {
"##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' name = 'second step']\n"
+
"##teamcity[customProgressStatus type = 'testFinished' timestamp = '1970-01-01T12:00:00.000+0000']\n" +
"##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Example #1.1']\n" +
"##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = '#1.1: name 1']\n"
+
"##teamcity[testSuiteStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location
+ "path/test.feature:8' name = 'Example #1.2']\n" +
+ "path/test.feature:8' name = '#1.2: name 2']\n" +
"##teamcity[customProgressStatus type = 'testStarted' timestamp = '1970-01-01T12:00:00.000+0000']\n" +
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '" + location
+ "path/test.feature:3' captureStandardOutput = 'true' name = 'first step']\n" +
Expand All @@ -96,7 +101,8 @@ void should_handle_scenario_outline() {
"##teamcity[testFinished timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' name = 'third step']\n"
+
"##teamcity[customProgressStatus type = 'testFinished' timestamp = '1970-01-01T12:00:00.000+0000']\n" +
"##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'Example #1.2']\n" +
"##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = '#1.2: name 2']\n"
+
"##teamcity[customProgressStatus testsCategory = '' count = '0' timestamp = '1970-01-01T12:00:00.000+0000']\n"
+
"##teamcity[testSuiteFinished timestamp = '1970-01-01T12:00:00.000+0000' name = 'examples name']\n" +
Expand All @@ -121,7 +127,7 @@ void should_handle_nameless_attach_events() {
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
singletonList(new StubHookDefinition(
(TestCaseState state) -> state.attach("A message", "text/plain", null))),
(TestCaseState state) -> state.attach("A message".getBytes(UTF_8), "text/plain", null))),
singletonList(new StubStepDefinition("first step")),
emptyList()))
.build()
Expand Down Expand Up @@ -168,7 +174,8 @@ void should_handle_attach_events() {
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
singletonList(new StubHookDefinition(
(TestCaseState state) -> state.attach("A message", "text/plain", "message.txt"))),
(TestCaseState state) -> state.attach("A message".getBytes(UTF_8), "text/plain",
"message.txt"))),
singletonList(new StubStepDefinition("first step")),
emptyList()))
.build()
Expand Down Expand Up @@ -258,17 +265,17 @@ void should_print_error_message_for_before_hooks() {
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
singletonList(
new StubHookDefinition(new StubException("Step failed")
new StubHookDefinition(getAnnotationSourceReference(), BEFORE, new StubException()
.withStacktrace("the stack trace"))),
singletonList(new StubStepDefinition("first step")),
emptyList()))
.build()
.run();

assertThat(out, bytes(containsString("" +
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = '{stubbed location with details}' captureStandardOutput = 'true' name = 'Before']\n"
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://io.cucumber.core.plugin.TeamCityPluginTestStepDefinition/beforeHook' captureStandardOutput = 'true' name = 'Before(beforeHook)']\n"
+
"##teamcity[testFailed timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' message = 'Step failed' details = 'Step failed|n\tthe stack trace|n' name = 'Before']")));
"##teamcity[testFailed timestamp = '1970-01-01T12:00:00.000+0000' duration = '0' message = 'Step failed' details = 'stub exception|n\tthe stack trace|n' name = 'Before(beforeHook)']")));
}

@Test
Expand All @@ -284,14 +291,14 @@ void should_print_location_hint_for_java_hooks() {
.withAdditionalPlugins(new TeamCityPlugin(new PrintStream(out)))
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
singletonList(new StubHookDefinition("com.example.HookDefinition.beforeHook()")),
singletonList(new StubHookDefinition(getAnnotationSourceReference(), BEFORE)),
singletonList(new StubStepDefinition("first step")),
emptyList()))
.build()
.run();

assertThat(out, bytes(containsString("" +
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://com.example.HookDefinition/beforeHook' captureStandardOutput = 'true' name = 'Before(beforeHook)']\n")));
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://io.cucumber.core.plugin.TeamCityPluginTestStepDefinition/beforeHook' captureStandardOutput = 'true' name = 'Before(beforeHook)']\n")));
}

@Test
Expand All @@ -307,14 +314,14 @@ void should_print_location_hint_for_lambda_hooks() {
.withAdditionalPlugins(new TeamCityPlugin(new PrintStream(out)))
.withEventBus(new TimeServiceEventBus(fixed(EPOCH, of("UTC")), UUID::randomUUID))
.withBackendSupplier(new StubBackendSupplier(
singletonList(new StubHookDefinition("com.example.HookDefinition.<init>(HookDefinition.java:12)")),
singletonList(new StubHookDefinition(getStackSourceReference(), BEFORE)),
singletonList(new StubStepDefinition("first step")),
emptyList()))
.build()
.run();

assertThat(out, bytes(containsString("" +
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://com.example.HookDefinition/HookDefinition' captureStandardOutput = 'true' name = 'Before(HookDefinition)']\n")));
"##teamcity[testStarted timestamp = '1970-01-01T12:00:00.000+0000' locationHint = 'java:test://io.cucumber.core.plugin.TeamCityPluginTestStepDefinition/TeamCityPluginTestStepDefinition' captureStandardOutput = 'true' name = 'Before(TeamCityPluginTestStepDefinition)']\n")));
}

@Test
Expand Down Expand Up @@ -397,4 +404,5 @@ void should_print_comparison_failure_for_failed_assert_equal_with_prefix() {
assertThat(out,
bytes(containsString("expected = 'one value' actual = 'another value' name = 'first step']")));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.cucumber.core.plugin;

import io.cucumber.core.backend.SourceReference;

import java.lang.reflect.Method;

class TeamCityPluginTestStepDefinition {
SourceReference source;

TeamCityPluginTestStepDefinition() {
source = SourceReference.fromStackTraceElement(new Exception().getStackTrace()[0]);
}

public void beforeHook() {

}

static SourceReference getAnnotationSourceReference() {
try {
Method method = TeamCityPluginTestStepDefinition.class.getMethod("beforeHook");
return SourceReference.fromMethod(method);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
}

static SourceReference getStackSourceReference() {
return new TeamCityPluginTestStepDefinition().source;
}
}