refactoring [junit5, test]: Add reporting structure for JUnit 5 integration

GitOrigin-RevId: 2ccaf0ada562f3de56da6bc70330794d5b14fcb6
This commit is contained in:
Aleksey Dobrynin
2026-01-09 15:50:49 +00:00
committed by intellij-monorepo-bot
parent f0f4245754
commit f46575397c
17 changed files with 939 additions and 492 deletions
@@ -1,14 +1,25 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.rt.execution.junit;
import java.util.Iterator;
import java.util.Map;
public final class MapSerializerUtil {
public static final String TEST_STARTED = "testStarted";
public static final String TEST_FINISHED = "testFinished";
public static final String TEST_FAILED = "testFailed";
public static final String TEST_IGNORED = "testIgnored";
public static final String TEST_STD_OUT = "testStdOut";
public static final String TEST_SUITE_STARTED = "testSuiteStarted";
public static final String TEST_SUITE_FINISHED = "testSuiteFinished";
public static final String SUITE_TREE_STARTED = "suiteTreeStarted";
public static final String SUITE_TREE_ENDED = "suiteTreeEnded";
public static final String SUITE_TREE_NODE = "suiteTreeNode";
public static final String ROOT_NAME = "rootName";
/**
* String escaping info provider.
@@ -99,14 +110,16 @@ public final class MapSerializerUtil {
return result;
}
public static String asString(final String messageName, final Map attributes) {
String text = "##teamcity[" + messageName;
for (Iterator iterator = attributes.keySet().iterator(); iterator.hasNext(); ) {
final Object attrName = iterator.next();
text += " " + attrName + "='" + escapeStr((String)attributes.get(attrName), STD_ESCAPER) + "'";
public static String asString(final String messageName, final Map<String, String> attributes) {
StringBuilder text = new StringBuilder("##teamcity[" + messageName);
for (final Object attrName : attributes.keySet()) {
text.append(" ")
.append(attrName)
.append("='")
.append(escapeStr(attributes.get(attrName), STD_ESCAPER))
.append("'");
}
text += "]";
return text;
text.append("]");
return text.toString();
}
}
}
@@ -85,6 +85,7 @@ public class JUnit5IdeaTestRunner implements IdeaTestRunner<TestIdentifier> {
myForkedTestPlan = LauncherFactory.create().discover(discoveryRequest);
final Set<TestIdentifier> roots = myForkedTestPlan.getRoots();
if (roots.isEmpty()) return null;
@SuppressWarnings("SSBasedInspection")
List<TestIdentifier> nonEmptyRoots = roots.stream()
.filter(identifier -> !myForkedTestPlan.getChildren(identifier).isEmpty())
.collect(Collectors.toList());
@@ -115,6 +116,7 @@ public class JUnit5IdeaTestRunner implements IdeaTestRunner<TestIdentifier> {
final TestIdentifier testIdentifier = child;
final String className = JUnit5TestExecutionListener.getClassName(testIdentifier);
final String methodSignature = JUnit5TestExecutionListener.getMethodSignature(testIdentifier);
//noinspection ConstantValue
if (methodSignature != null) {
return className + "," + methodSignature;
}
@@ -155,4 +157,4 @@ public class JUnit5IdeaTestRunner implements IdeaTestRunner<TestIdentifier> {
}
}
}
}
}
@@ -1,405 +1,118 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5;
import com.intellij.junit4.ExpectedPatterns;
import com.intellij.rt.execution.junit.ComparisonFailureData;
import com.intellij.rt.execution.junit.MapSerializerUtil;
import com.intellij.junit5.report.ExecutionState;
import com.intellij.junit5.report.TeamCityTestReporter;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.engine.support.descriptor.ClassSource;
import org.junit.platform.engine.support.descriptor.CompositeTestSource;
import org.junit.platform.engine.support.descriptor.FileSource;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.opentest4j.MultipleFailuresError;
import java.io.File;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import static com.intellij.rt.execution.TestListenerProtocol.CLASS_CONFIGURATION;
import java.util.HashSet;
import java.util.Set;
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public class JUnit5TestExecutionListener implements TestExecutionListener {
private static final String NO_LOCATION_HINT = "";
private static final String NO_LOCATION_HINT_VALUE = "";
private final PrintStream myPrintStream;
private TestPlan myTestPlan;
private long myCurrentTestStart;
private int myFinishCount = 0;
private String myRootName;
private String myPresentableName;
private boolean mySuccessful = true;
private String myIdSuffix = "";
private boolean mySendTree;
private final ExecutionState myState;
public JUnit5TestExecutionListener() {
this(System.out);
}
public JUnit5TestExecutionListener(PrintStream printStream) {
myPrintStream = printStream;
myPrintStream.println("##teamcity[enteredTheMatrix]");
myState = new ExecutionState(printStream);
myState.print("##teamcity[enteredTheMatrix]");
}
public boolean wasSuccessful() {
return mySuccessful;
return myState.wasSuccessful();
}
public void initializeIdSuffix(boolean forked) {
if (forked && myIdSuffix.isEmpty()) {
myIdSuffix = String.valueOf(System.currentTimeMillis());
}
}
public void initializeIdSuffix(int i) {
myIdSuffix = i + "th";
myState.initializeIdSuffix(forked);
}
@Override
public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
StringBuilder builder = new StringBuilder();
builder.append("timestamp = ").append(entry.getTimestamp());
entry.getKeyValuePairs().forEach((key, value) -> builder.append(", ").append(key).append(" = ").append(value));
builder.append("\n");
myPrintStream.println("##teamcity[testStdOut" + idAndName(testIdentifier) + " out = '" + escapeName(builder.toString()) + "']");
public void initializeIdSuffix(int i) {
myState.initializeIdSuffix(i);
}
public void setRootName(String rootName) {
myState.setRootName(rootName);
}
public void setPresentableName(String presentableName) {
myState.setPresentableName(presentableName);
}
public void setSendTree() {
myState.setSendTree(true);
}
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
myTestPlan = testPlan;
if (mySendTree) {
for (TestIdentifier root : myTestPlan.getRoots()) {
myState.setPlan(testPlan);
if (myState.isSendTree()) {
for (TestIdentifier root : testPlan.getRoots()) {
assert root.isContainer();
for (TestIdentifier testIdentifier : myTestPlan.getChildren(root)) {
for (TestIdentifier testIdentifier : testPlan.getChildren(root)) {
String legacyReportingName = testIdentifier.getLegacyReportingName();
if (legacyReportingName != null && legacyReportingName.equals(myRootName)) setPresentableName(testIdentifier.getDisplayName());
if (legacyReportingName != null && legacyReportingName.equals(myState.getRootName())) {
myState.setPresentableName(testIdentifier.getDisplayName());
}
sendTreeUnderRoot(testIdentifier, new HashSet<>());
}
}
myPrintStream.println("##teamcity[treeEnded]");
}
if (myRootName != null) {
int lastPointIdx = myRootName.lastIndexOf('.');
String name = myRootName;
String comment = null;
if (lastPointIdx >= 0) {
name = myRootName.substring(lastPointIdx + 1);
comment = myRootName.substring(0, lastPointIdx);
}
String messageName = (myPresentableName == null || myPresentableName.isEmpty()) ? name : myPresentableName;
myPrintStream.println("##teamcity[rootName name = '" + escapeName(messageName) +
(comment != null ? ("' comment = '" + escapeName(comment)) : "") + "'" +
" location = 'java:suite://" + escapeName(myRootName) +
"']");
myState.print("##teamcity[treeEnded]");
}
myState.printRootNameIfNeeded();
}
private void sendTreeUnderRoot(TestIdentifier root, HashSet<TestIdentifier> visited) {
String idAndName = idAndName(root);
if (root.isContainer()) {
boolean skipContainer = shouldSkipContainer(root);
if (!skipContainer) myPrintStream.println("##teamcity[suiteTreeStarted" + idAndName + " " + getLocationHint(root) + "]");
for (TestIdentifier childIdentifier : myTestPlan.getChildren(root)) {
if (visited.add(childIdentifier)) {
sendTreeUnderRoot(childIdentifier, visited);
}
else {
System.err.println("Identifier '" + getId(childIdentifier) + "' is reused");
}
public void sendTreeUnderRoot(TestIdentifier root, Set<TestIdentifier> visited) {
TeamCityTestReporter reporter = TeamCityTestReporter.get(root, myState);
reporter.treeStarted().forEach(myState::print);
for (TestIdentifier child : myState.plan().getChildren(root)) {
if (visited.add(child)) {
sendTreeUnderRoot(child, visited);
}
else {
System.err.println("Identifier '" + child.getUniqueId() + "' is reused");
}
if (!skipContainer) myPrintStream.println("##teamcity[suiteTreeEnded" + idAndName + "]");
}
else if (root.isTest()) {
myPrintStream.println("##teamcity[suiteTreeNode " + idAndName + " " + getLocationHint(root) + "]");
}
reporter.treeFinished().forEach(myState::print);
}
@Override
public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
TeamCityTestReporter.get(testIdentifier, myState).output(entry)
.forEach(myState::print);
}
@Override
public void executionSkipped(TestIdentifier testIdentifier, String reason) {
executionStarted (testIdentifier);
executionFinished(testIdentifier, TestExecutionResult.Status.ABORTED, null, reason);
executionStarted(testIdentifier);
TeamCityTestReporter.get(testIdentifier, myState)
.skip(reason)
.forEach(myState::print);
}
@Override
public void executionStarted(TestIdentifier testIdentifier) {
if (testIdentifier.isTest()) {
testStarted(testIdentifier);
myCurrentTestStart = System.nanoTime();
}
else if (!shouldSkipContainer(testIdentifier)) {
myFinishCount = 0;
myPrintStream.println("##teamcity[testSuiteStarted" + idAndName(testIdentifier) + getLocationHint(testIdentifier) + "]");
}
TeamCityTestReporter.get(testIdentifier, myState).start()
.forEach(myState::print);
}
@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
TestExecutionResult.Status status = testExecutionResult.getStatus();
Throwable throwableOptional = testExecutionResult.getThrowable().orElse(null);
executionFinished(testIdentifier, status, throwableOptional, null);
mySuccessful &= TestExecutionResult.Status.SUCCESSFUL == testExecutionResult.getStatus();
}
private void executionFinished(TestIdentifier testIdentifier,
TestExecutionResult.Status status,
Throwable throwableOptional,
String reason) {
if (testIdentifier.isTest()) {
long duration = getDuration();
if (status == TestExecutionResult.Status.FAILED) {
testFailure(testIdentifier, MapSerializerUtil.TEST_FAILED, throwableOptional, duration, reason, true);
}
else if (status == TestExecutionResult.Status.ABORTED) {
testFailure(testIdentifier, MapSerializerUtil.TEST_IGNORED, throwableOptional, duration, reason, true);
}
testFinished(testIdentifier, duration);
myFinishCount++;
}
else if (!shouldSkipContainer(testIdentifier) || status == TestExecutionResult.Status.FAILED){
String messageName = null;
if (status == TestExecutionResult.Status.FAILED) {
messageName = MapSerializerUtil.TEST_FAILED;
}
else if (status == TestExecutionResult.Status.ABORTED) {
messageName = MapSerializerUtil.TEST_IGNORED;
}
if (messageName != null) {
Set<TestIdentifier> descendants = myTestPlan != null ? myTestPlan.getDescendants(testIdentifier) : Collections.emptySet();
if (status == TestExecutionResult.Status.FAILED) {
String parentId = getParentId(testIdentifier);
String nameAndId = " name='" + CLASS_CONFIGURATION +
"' nodeId='" + escapeName(getId(testIdentifier)) +
"' parentNodeId='" + escapeName(parentId) + "' ";
myPrintStream.println("##teamcity[testStarted " + nameAndId + " ]");
testFailure(CLASS_CONFIGURATION, getId(testIdentifier), parentId, messageName, throwableOptional, 0, reason, true);
myPrintStream.println("##teamcity[testFinished" + nameAndId + "]");
}
else if (descendants.isEmpty()) {
testFailure(testIdentifier, MapSerializerUtil.TEST_IGNORED, throwableOptional, 0, reason, true);
}
if (!descendants.isEmpty() && myFinishCount == 0) {
for (TestIdentifier childIdentifier : descendants) {
testStarted(childIdentifier);
Throwable throwable = status == TestExecutionResult.Status.ABORTED ? throwableOptional : null;
testFailure(childIdentifier, MapSerializerUtil.TEST_IGNORED, throwable, 0, reason, status == TestExecutionResult.Status.ABORTED);
testFinished(childIdentifier, 0);
}
myFinishCount = 0;
}
}
myPrintStream.println("##teamcity[testSuiteFinished" + idAndName(testIdentifier) + "]");
}
}
private boolean shouldSkipContainer(TestIdentifier testIdentifier) {
UniqueId id = UniqueId.parse(testIdentifier.getUniqueId());
List<UniqueId.Segment> segments = id.getSegments();
if (segments.isEmpty()) return false;
UniqueId.Segment lastSegment = segments.get(segments.size() - 1);
return lastSegment.getType().equals("engine") ||
myRootName != null && myRootName.equals(lastSegment.getValue());
}
protected long getDuration() {
return (System.nanoTime() - myCurrentTestStart) / 1_000_000;
}
private void testStarted(TestIdentifier testIdentifier) {
myPrintStream.println("##teamcity[testStarted" + idAndName(testIdentifier) + " " + getLocationHint(testIdentifier) + "]");
}
private void testFinished(TestIdentifier testIdentifier, long duration) {
myPrintStream.println("##teamcity[testFinished" + idAndName(testIdentifier) + (duration > 0 ? " duration='" + duration + "'" : "") + "]");
}
private void testFailure(TestIdentifier testIdentifier,
String messageName,
Throwable ex,
long duration,
String reason,
boolean includeThrowable) {
testFailure(testIdentifier.getDisplayName(), getId(testIdentifier), getParentId(testIdentifier), messageName, ex, duration, reason, includeThrowable);
}
private void testFailure(String methodName,
String id,
String parentId,
String messageName,
Throwable ex,
long duration,
String reason,
boolean includeThrowable) {
Map<String, String> attrs = new LinkedHashMap<>();
attrs.put("name", methodName);
attrs.put("id", id);
attrs.put("nodeId", id);
attrs.put("parentNodeId", parentId);
if (duration > 0) {
attrs.put("duration", Long.toString(duration));
}
if (reason != null) {
attrs.put("message", reason);
}
try {
if (ex != null) {
ComparisonFailureData failureData = null;
if (ex instanceof MultipleFailuresError && ((MultipleFailuresError)ex).hasFailures()) {
for (Throwable assertionError : ((MultipleFailuresError)ex).getFailures()) {
testFailure(methodName, id, parentId, messageName, assertionError, duration, reason, true);
}
}
else {
try {
failureData = ExpectedPatterns.createExceptionNotification(ex);
}
catch (Throwable ignore) {}
}
if (includeThrowable || failureData == null) {
ComparisonFailureData.registerSMAttributes(failureData, getTrace(ex), ex.getMessage(), attrs, ex, "Comparison Failure: ", "expected: <");
}
else {
ComparisonFailureData.registerSMAttributes(failureData, "", ex.getMessage(), attrs, ex, "Comparison Failure: ", "expected: <");
}
}
}
finally {
myPrintStream.println(MapSerializerUtil.asString(messageName, attrs));
}
}
protected String getTrace(Throwable ex) {
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
ex.printStackTrace(writer);
return stringWriter.toString();
}
public void setRootName(String rootName) {
myRootName = rootName;
}
public void setPresentableName(String presentableName) {
myPresentableName = presentableName;
}
public void setSendTree() {
mySendTree = true;
}
private String getId(TestIdentifier identifier) {
return identifier.getUniqueId() + myIdSuffix;
}
private String idAndName(TestIdentifier testIdentifier) {
return " id='" + escapeName(getId(testIdentifier)) +
"' name='" + escapeName(testIdentifier.getDisplayName()) +
"' nodeId='" + escapeName(getId(testIdentifier)) +
"' parentNodeId='" + escapeName(getParentId(testIdentifier)) + "'";
}
private String getParentId(TestIdentifier testIdentifier) {
Optional<TestIdentifier> parent = myTestPlan.getParent(testIdentifier);
return parent
.map(identifier -> shouldSkipContainer(identifier) ? getParentId(identifier) : identifier.getUniqueId() + myIdSuffix)
.orElse("0");
}
private String getLocationHint(TestIdentifier root) {
return getLocationHint(root, myTestPlan.getParent(root).orElse(null));
}
public static String getLocationHint(TestIdentifier root, TestIdentifier rootParent) {
return root.getSource()
.map(testSource -> getLocationHintValue(testSource, rootParent != null ? rootParent.getSource().orElse(null) : null))
.filter(maybeLocationHintValue -> !NO_LOCATION_HINT_VALUE.equals(maybeLocationHintValue))
.map(locationHintValue -> "locationHint='" + locationHintValue + "'" + getMetaInfo(root))
.orElse(NO_LOCATION_HINT);
}
private static String getMetaInfo(TestIdentifier root) {
return root.getSource()
.map(testSource -> {
if (testSource instanceof MethodSource) {
//noinspection SpellCheckingInspection
return " metainfo='" + ((MethodSource)testSource).getMethodParameterTypes() + "'";
}
if (testSource instanceof ClassSource) {
//noinspection SpellCheckingInspection
return ((ClassSource)testSource).getPosition()
.map(position ->
" metainfo='"
// Convert JUnit's 1-based values to 0-based
+ (position.getLine() - 1) + ":"
+ (position.getColumn().orElse(1) - 1) + "'")
.orElse(NO_LOCATION_HINT);
}
return NO_LOCATION_HINT;
})
.orElse(NO_LOCATION_HINT);
}
public static String getLocationHintValue(TestSource testSource, TestSource parentSource) {
if (testSource instanceof CompositeTestSource) {
CompositeTestSource compositeTestSource = ((CompositeTestSource)testSource);
for (TestSource sourceFromComposite : compositeTestSource.getSources()) {
String locationHintValue = getLocationHintValue(sourceFromComposite, parentSource);
if (!NO_LOCATION_HINT_VALUE.equals(locationHintValue)) {
return locationHintValue;
}
}
return NO_LOCATION_HINT_VALUE;
}
if (testSource instanceof FileSource) {
FileSource fileSource = (FileSource)testSource;
File file = fileSource.getFile();
String line = fileSource.getPosition()
.map(position -> ":" + position.getLine())
.orElse("");
return "file://" + file.getAbsolutePath() + line;
}
if (testSource instanceof MethodSource) {
MethodSource methodSource = (MethodSource)testSource;
return javaLocation(methodSource.getClassName(), methodSource.getMethodName(), true);
}
if (testSource instanceof ClassSource) {
String className = ((ClassSource)testSource).getClassName();
return javaLocation(className, null, false);
}
if (parentSource != null) {
return getLocationHintValue(parentSource,null);
}
return NO_LOCATION_HINT_VALUE;
}
private static String javaLocation(String className, String maybeMethodName, boolean isTest) {
String type = isTest ? "test" : "suite";
String methodName = maybeMethodName == null ? "" : "/" + maybeMethodName;
String location = escapeName(className + methodName);
return "java:" + type + "://" + location;
}
private static String escapeName(String str) {
return MapSerializerUtil.escapeStr(str, MapSerializerUtil.STD_ESCAPER);
TeamCityTestReporter.get(testIdentifier, myState).finish(testExecutionResult)
.forEach(myState::print);
myState.updateSuccessful(testExecutionResult.getStatus());
}
public static String getClassName(TestIdentifier description) {
@@ -0,0 +1,113 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import com.intellij.rt.execution.junit.ComparisonFailureData;
import com.intellij.rt.execution.junit.MapSerializerUtil;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.launcher.TestIdentifier;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
public abstract class AbstractTestReporter implements TeamCityTestReporter {
protected final TestIdentifier identifier;
protected final ExecutionState state;
protected AbstractTestReporter(TestIdentifier identifier, ExecutionState state) {
this.identifier = identifier;
this.state = state;
}
@Override
public List<String> output(ReportEntry entry) {
Map<String, String> attrs = attributes(ReportedField.ID, ReportedField.NAME, ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID);
StringBuilder builder = new StringBuilder();
builder.append("timestamp = ").append(entry.getTimestamp());
entry.getKeyValuePairs().forEach((key, value) -> builder.append(", ").append(key).append(" = ").append(value));
builder.append("\n");
attrs.put("out", builder.toString());
return Collections.singletonList(MapSerializerUtil.asString(MapSerializerUtil.TEST_STD_OUT, attrs));
}
protected String id() {
return identifier.getUniqueId() + state.suffix();
}
protected Optional<SuiteReporter> getParent() {
//noinspection SimplifyOptionalCallChains
return state.plan().getParent(identifier)
.map(i -> new SuiteReporter(i, state))
.map(r -> r.isSkipped() ? r.getParent() : Optional.of(r))
.orElse(Optional.empty());
}
protected Map<String, String> attributes(ReportedField... fields) {
Map<String, String> attrs = new LinkedHashMap<>(fields.length * 2);
LocationInfo location = locationInfo();
for (ReportedField field : fields) {
String value = null;
switch (field) {
case ID:
case NODE_ID:
value = id();
break;
case NAME:
value = name();
break;
case PARENT_NODE_ID:
value = getParent().map(AbstractTestReporter::id).orElse("0");
break;
case HINT:
value = location.locationHint();
break;
case METAINFO:
value = location.metainfo();
break;
}
if (value != null) {
attrs.put(field.getId(), value);
}
}
return attrs;
}
protected String name() {
return identifier.getDisplayName();
}
private LocationInfo locationInfo() {
if (state.plan() == null) return LocationInfo.EMPTY;
TestIdentifier parent = state.plan().getParent(identifier).orElse(null);
return LocationInfo.compute(identifier, parent);
}
protected static String getTrace(Throwable ex) {
if (ComparisonFailureData.isAssertionError(ex.getClass())) return "";
StringWriter writer = new StringWriter();
ex.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
protected enum ReportedField {
ID("id"),
NAME("name"),
NODE_ID("nodeId"),
PARENT_NODE_ID("parentNodeId"),
HINT("locationHint"),
METAINFO("metainfo");
private final String myId;
ReportedField(String id) {
myId = id;
}
public String getId() {
return myId;
}
}
}
@@ -0,0 +1,64 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import com.intellij.rt.execution.junit.MapSerializerUtil;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.launcher.TestIdentifier;
import java.util.ArrayList;
import java.util.List;
// e.g., parametrized test
public class CompositeTestReporter extends AbstractTestReporter {
private final TestReporter asTest;
private final SuiteReporter asSuite;
public CompositeTestReporter(TestIdentifier identifier, ExecutionState state) {
super(identifier, state);
asTest = new TestReporter(identifier, state);
asSuite = new SuiteReporter(identifier, state);
}
@Override
public List<String> start() {
List<String> out = new ArrayList<>();
out.addAll(asSuite.start());
out.addAll(asTest.start());
return out;
}
@Override
public List<String> output(ReportEntry entry) {
return asTest.output(entry);
}
@Override
public List<String> finish(TestExecutionResult result) {
List<String> out = new ArrayList<>();
out.addAll(asTest.finish(result));
out.addAll(asSuite.finish(result));
return out;
}
@Override
public List<String> treeStarted() {
List<String> out = new ArrayList<>(asSuite.treeStarted());
String testNode = MapSerializerUtil.asString(MapSerializerUtil.SUITE_TREE_NODE,
attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID,
ReportedField.HINT, ReportedField.METAINFO));
out.add(testNode);
return out;
}
@Override
public List<String> treeFinished() {
return asSuite.treeFinished();
}
@Override
public List<String> skip(String reason) {
return asTest.skip(reason);
}
}
@@ -0,0 +1,127 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import com.intellij.rt.execution.junit.MapSerializerUtil;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.TestPlan;
import java.io.PrintStream;
import java.util.LinkedHashMap;
import java.util.Map;
final public class ExecutionState {
private final PrintStream myOut;
private TestPlan myPlan;
private long myCurrentTestStartNanos;
private int myFinishCount;
private String myRootName;
private String myPresentableName;
private boolean mySuccessful = true;
private String myIdSuffix = "";
private boolean mySendTree;
public ExecutionState(PrintStream out) {
myOut = out;
}
public void setPlan(TestPlan plan) {
myPlan = plan;
}
public TestPlan plan() {
return myPlan;
}
public void print(String line) {
myOut.println(line);
}
public boolean wasSuccessful() {
return mySuccessful;
}
public void updateSuccessful(TestExecutionResult.Status status) {
mySuccessful &= (status == TestExecutionResult.Status.SUCCESSFUL);
}
public void initializeIdSuffix(boolean forked) {
if (forked && myIdSuffix.isEmpty()) {
myIdSuffix = String.valueOf(System.currentTimeMillis());
}
}
public void initializeIdSuffix(int i) {
myIdSuffix = i + "th";
}
public String suffix() {
return myIdSuffix;
}
public void setRootName(String rootName) {
myRootName = rootName;
}
public String getRootName() {
return myRootName;
}
public void setPresentableName(String presentableName) {
myPresentableName = presentableName;
}
public String getPresentableName() {
return myPresentableName;
}
public void setSendTree(boolean sendTree) {
mySendTree = sendTree;
}
public boolean isSendTree() {
return mySendTree;
}
public void onLeafTestStarted() {
myCurrentTestStartNanos = System.nanoTime();
}
public long onLeafTestFinishedAndGetDurationMs() {
return (System.nanoTime() - myCurrentTestStartNanos) / 1_000_000L;
}
public void resetFinishCount() {
myFinishCount = 0;
}
public void incrementFinishCount() {
myFinishCount++;
}
public int finishCount() {
return myFinishCount;
}
public void printRootNameIfNeeded() {
if (myRootName == null) return;
int lastPointIdx = myRootName.lastIndexOf('.');
String name = myRootName;
String comment = null;
if (lastPointIdx >= 0) {
name = myRootName.substring(lastPointIdx + 1);
comment = myRootName.substring(0, lastPointIdx);
}
String messageName = (myPresentableName == null || myPresentableName.isEmpty()) ? name : myPresentableName;
Map<String, String> attrs = new LinkedHashMap<>();
attrs.put("name", messageName);
if (comment != null) attrs.put("comment", comment);
attrs.put("location", "java:suite://" + myRootName);
print(MapSerializerUtil.asString(MapSerializerUtil.ROOT_NAME, attrs));
}
}
@@ -0,0 +1,104 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import com.intellij.rt.execution.junit.MapSerializerUtil;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.support.descriptor.*;
import org.junit.platform.launcher.TestIdentifier;
import java.io.File;
import java.util.Optional;
final public class LocationInfo {
static final LocationInfo EMPTY = new LocationInfo(null, null, null);
private final String locationHint;
private final String metainfo;
public LocationInfo(TestSource opt, TestSource parent, TestIdentifier node) {
this.locationHint = getLocationHintValue(opt, parent);
if (this.locationHint.isEmpty()) {
this.metainfo = null;
} else {
this.metainfo = getMetaInfoValue(node);
}
}
public String locationHint() {
return locationHint;
}
public String metainfo() {
return metainfo;
}
public static LocationInfo compute(TestIdentifier node, TestIdentifier parent) {
TestSource parentSource = parent != null ? parent.getSource().orElse(null) : null;
Optional<TestSource> sourceOpt = node.getSource();
//noinspection OptionalIsPresent
if (!sourceOpt.isPresent()) return EMPTY;
return new LocationInfo(sourceOpt.get(), parentSource, node);
}
private static String getMetaInfoValue(TestIdentifier root) {
if (root == null || !root.getSource().isPresent()) return null;
TestSource source = root.getSource().get();
if (source instanceof MethodSource) {
return ((MethodSource)source).getMethodParameterTypes();
}
else if (source instanceof ClassSource) {
Optional<FilePosition> position = ((ClassSource)source).getPosition();
if (!position.isPresent()) return null;
FilePosition pos = position.get();
return (pos.getLine() - 1) + ":" + (pos.getColumn().orElse(1) - 1);
}
else {
return null;
}
}
private static String getLocationHintValue(TestSource testSource, TestSource parentSource) {
if (testSource instanceof CompositeTestSource) {
CompositeTestSource composite = (CompositeTestSource)testSource;
for (TestSource s : composite.getSources()) {
String v = getLocationHintValue(s, parentSource);
if (!v.isEmpty()) return v;
}
return "";
}
if (testSource instanceof FileSource) {
FileSource fileSource = (FileSource)testSource;
File file = fileSource.getFile();
String line = fileSource.getPosition()
.map(position -> ":" + position.getLine())
.orElse("");
return "file://" + file.getAbsolutePath() + line;
}
if (testSource instanceof MethodSource) {
MethodSource methodSource = (MethodSource)testSource;
return javaLocation(methodSource.getClassName(), methodSource.getMethodName(), true);
}
if (testSource instanceof ClassSource) {
String className = ((ClassSource)testSource).getClassName();
return javaLocation(className, null, false);
}
if (parentSource != null) {
return getLocationHintValue(parentSource, null);
}
return "";
}
private static String javaLocation(String className, String maybeMethodName, boolean isTest) {
String type = isTest ? "test" : "suite";
String methodName = maybeMethodName == null ? "" : "/" + maybeMethodName;
return "java:" + type + "://" + MapSerializerUtil.escapeStr(className + methodName, MapSerializerUtil.STD_ESCAPER);
}
}
@@ -0,0 +1,139 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.launcher.TestIdentifier;
import java.util.*;
import static com.intellij.rt.execution.TestListenerProtocol.CLASS_CONFIGURATION;
import static com.intellij.rt.execution.junit.MapSerializerUtil.*;
public class SuiteReporter extends AbstractTestReporter {
public SuiteReporter(TestIdentifier identifier, ExecutionState state) {
super(identifier, state);
}
// only for suites
public boolean isSkipped() {
UniqueId id = UniqueId.parse(identifier.getUniqueId());
List<UniqueId.Segment> segments = id.getSegments();
if (segments.isEmpty()) return false;
UniqueId.Segment lastSegment = segments.get(segments.size() - 1);
if ("engine".equals(lastSegment.getType())) return true;
String root = state.getRootName();
return root != null && root.equals(lastSegment.getValue());
}
@Override
public List<String> start() {
if (isSkipped()) return Collections.emptyList();
state.resetFinishCount();
String start = asString(TEST_SUITE_STARTED, attributes(ReportedField.ID, ReportedField.NAME, ReportedField.NODE_ID,
ReportedField.PARENT_NODE_ID, ReportedField.HINT, ReportedField.METAINFO));
return Collections.singletonList(start);
}
@Override
public List<String> finish(TestExecutionResult result) {
if (isSkipped() && result.getStatus() != TestExecutionResult.Status.FAILED) {
return Collections.emptyList();
}
List<String> out = new ArrayList<>();
TestExecutionResult.Status status = result.getStatus();
Throwable throwable = result.getThrowable().orElse(null);
Set<TestIdentifier> descendants = state.plan() != null ? state.plan().getDescendants(identifier) : Collections.emptySet();
if (status == TestExecutionResult.Status.FAILED) {
// Report class-level failure as CLASS_CONFIGURATION test
TestReporter reporter = new TestReporter(identifier, state, CLASS_CONFIGURATION);
out.add(asString(TEST_STARTED, reporter.attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID,
ReportedField.HINT, ReportedField.METAINFO)));
out.addAll(reporter.reportFailure(throwable, 0, true, TEST_FAILED, null));
out.add(asString(TEST_FINISHED, reporter.attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID)));
}
else if (status == TestExecutionResult.Status.ABORTED) {
if (descendants.isEmpty()) {
// Report as ignored
Map<String, String> attrs = attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID);
if (throwable != null) {
attrs.put("message", throwable.getMessage());
}
out.add(asString(TEST_IGNORED, attrs));
}
}
if (!descendants.isEmpty() && state.finishCount() == 0) {
String reason = (throwable != null) ? throwable.getMessage() : null;
for (TestIdentifier child : descendants) {
AbstractTestReporter reporter = TeamCityTestReporter.get(child, state);
if (reporter instanceof TestReporter) {
Throwable childEx = (status == TestExecutionResult.Status.ABORTED) ? throwable : null;
out.addAll(((TestReporter)reporter).ignore(childEx, reason));
}
}
state.resetFinishCount();
}
if (!isSkipped()) {
out.add(asString(TEST_SUITE_FINISHED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID)));
}
return out;
}
@Override
public List<String> treeStarted() {
if (isSkipped()) return Collections.emptyList();
String started = asString(SUITE_TREE_STARTED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID,
ReportedField.HINT, ReportedField.METAINFO
));
return Collections.singletonList(started);
}
@Override
public List<String> treeFinished() {
if (isSkipped()) return Collections.emptyList();
String finished = asString(SUITE_TREE_ENDED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID));
return Collections.singletonList(finished);
}
@Override
public List<String> skip(String reason) {
List<String> out = new ArrayList<>();
Set<TestIdentifier> descendants = state.plan() != null ? state.plan().getDescendants(identifier) : Collections.emptySet();
if (descendants.isEmpty()) {
Map<String, String> attrs = attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID);
if (reason != null && !reason.isEmpty()) attrs.put("message", reason);
out.add(asString(TEST_IGNORED, attrs));
} else {
for (TestIdentifier child : descendants) {
AbstractTestReporter reporter = TeamCityTestReporter.get(child, state);
out.addAll(reporter.start());
out.addAll(reporter.skip(reason));
}
state.resetFinishCount();
}
if (!isSkipped()) {
out.add(asString(TEST_SUITE_FINISHED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID)));
}
return out;
}
}
@@ -0,0 +1,34 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.launcher.TestIdentifier;
import java.util.List;
public interface TeamCityTestReporter {
List<String> start();
List<String> output(ReportEntry entry);
List<String> finish(TestExecutionResult result);
List<String> skip(String reason);
List<String> treeStarted();
List<String> treeFinished();
static AbstractTestReporter get(TestIdentifier identifier, ExecutionState state) {
if (identifier.isTest() && identifier.isContainer()) {
return new CompositeTestReporter(identifier, state);
}
else if (identifier.isTest()) {
return new TestReporter(identifier, state);
}
else {
return new SuiteReporter(identifier, state);
}
}
}
@@ -0,0 +1,153 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5.report;
import com.intellij.junit4.ExpectedPatterns;
import com.intellij.rt.execution.junit.ComparisonFailureData;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.TestIdentifier;
import org.opentest4j.MultipleFailuresError;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static com.intellij.rt.execution.junit.MapSerializerUtil.*;
public class TestReporter extends AbstractTestReporter {
private final String name;
public TestReporter(TestIdentifier identifier, ExecutionState state, String name) {
super(identifier, state);
this.name = name;
}
public TestReporter(TestIdentifier identifier, ExecutionState state) {
this(identifier, state, identifier.getDisplayName());
}
@Override
protected String name() {
return name;
}
@Override
public List<String> start() {
state.onLeafTestStarted();
String start = asString(TEST_STARTED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID,
ReportedField.HINT, ReportedField.METAINFO));
return Collections.singletonList(start);
}
@Override
public List<String> finish(TestExecutionResult result) {
Throwable ex = result.getThrowable().orElse(null);
long duration = state.onLeafTestFinishedAndGetDurationMs();
List<String> out = new ArrayList<>();
switch (result.getStatus()) {
case ABORTED:
out.addAll(reportFailure(ex, duration, true, TEST_IGNORED));
break;
case FAILED:
out.addAll(reportFailure(ex, duration, true, TEST_FAILED));
break;
case SUCCESSFUL: // do nothing
break;
}
Map<String, String> attributes = attributes(ReportedField.ID, ReportedField.NAME, ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID);
if (duration > 0) {
attributes.put("duration", Long.toString(duration));
}
out.add(asString(TEST_FINISHED, attributes));
state.incrementFinishCount();
return out;
}
@Override
public List<String> treeStarted() {
String started = asString(SUITE_TREE_NODE, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID,
ReportedField.HINT, ReportedField.METAINFO
));
return Collections.singletonList(started);
}
@Override
public List<String> treeFinished() {
return Collections.emptyList();
}
@Override
public List<String> skip(String reason) {
List<String> out = new ArrayList<>(reportFailure(null, 0, true, TEST_IGNORED, reason));
out.add(asString(TEST_FINISHED, attributes(ReportedField.ID, ReportedField.NAME, ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID)));
state.incrementFinishCount();
return out;
}
@SuppressWarnings("SameParameterValue")
private List<String> reportFailure(Throwable ex,
long duration,
boolean includeThrowable,
String messageType) {
return reportFailure(ex, duration, includeThrowable, messageType, null);
}
protected List<String> ignore(Throwable ex, String reason) {
List<String> out = new ArrayList<>();
out.add(asString(TEST_STARTED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID,
ReportedField.HINT, ReportedField.METAINFO)));
out.addAll(reportFailure(ex, 0, true, TEST_IGNORED, reason));
out.add(asString(TEST_FINISHED, attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID)));
return out;
}
List<String> reportFailure(Throwable ex,
long duration,
boolean includeThrowable,
String messageType, // TEST_FAILED or TEST_IGNORED
String reason) {
Map<String, String> attributes = attributes(ReportedField.ID, ReportedField.NAME,
ReportedField.NODE_ID, ReportedField.PARENT_NODE_ID);
if (duration > 0) {
attributes.put("duration", Long.toString(duration));
}
if (reason != null) {
attributes.put("message", reason);
}
List<String> out = new ArrayList<>();
if (ex != null) {
ComparisonFailureData failureData = null;
if (ex instanceof MultipleFailuresError && ((MultipleFailuresError)ex).hasFailures()) {
for (Throwable t : ((MultipleFailuresError)ex).getFailures()) {
out.addAll(reportFailure(t, duration, true, messageType, reason));
}
}
else {
try {
failureData = ExpectedPatterns.createExceptionNotification(ex);
}
catch (Throwable ignore) {
}
}
ComparisonFailureData.registerSMAttributes(failureData, includeThrowable ? getTrace(ex) : "",
ex.getMessage(), attributes, ex,
"Comparison Failure: ", "expected: <");
}
out.add(asString(messageType, attributes));
return out;
}
}
@@ -47,14 +47,14 @@ public class JUnit5EventsTest {
.finishWithFailure(multipleFailuresError);
Assertions.assertEquals("""
##teamcity[enteredTheMatrix]
##teamcity[rootName name = 'testClass' location = 'java:suite://testClass']
##teamcity[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' locationHint='java:test://com.intellij.junit5.testData.MyTestClass/test1' metainfo='']
##teamcity[testStdOut id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' out = 'timestamp = ${timestamp}, key1 = value1, stdout = out1|n']
##teamcity[testFailed name='test1()' id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' message='message1|nComparison Failure: ' expected='expected1' actual='actual1' details='TRACE']
##teamcity[testFailed name='test1()' id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' message='message2|nComparison Failure: ' expected='expected2' actual='actual2' details='TRACE']
##teamcity[testFailed name='test1()' id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' message='2 errors (2 failures)|n\torg.opentest4j.AssertionFailedError: message1|n\torg.opentest4j.AssertionFailedError: message2' details='TRACE']
##teamcity[testFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0']
##TC[enteredTheMatrix]
##TC[rootName name='testClass' location='java:suite://testClass']
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' locationHint='java:test://com.intellij.junit5.testData.MyTestClass/test1' metainfo='']
##TC[testStdOut id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' out='timestamp = ${timestamp}, key1 = value1, stdout = out1|n']
##TC[testFailed id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' message='message1|nComparison Failure: ' expected='expected1' actual='actual1' details='TRACE']
##TC[testFailed id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' message='message2|nComparison Failure: ' expected='expected2' actual='actual2' details='TRACE']
##TC[testFailed id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' message='2 errors (2 failures)|n\torg.opentest4j.AssertionFailedError: message1|n\torg.opentest4j.AssertionFailedError: message2' details='TRACE']
##TC[testFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='test1()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0']
""".replace("${timestamp}", reportEntry.getTimestamp().toString()), builder.getFormattedOutput());
}
@@ -82,11 +82,11 @@ public class JUnit5EventsTest {
Assertions.assertEquals(
"""
##teamcity[enteredTheMatrix]
##teamcity[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test1|]' name='test1 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test1|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass' metainfo='110:221']
##teamcity[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test2|]' name='test2 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test2|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='file:///directory/test2.java:12']
##teamcity[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test3|]' name='test3 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test3|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:test://MyTestClass/test4Method' metainfo='java.lang.String,java.util.List']
##teamcity[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test4|]' name='test4 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test4|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:suite://com.intellij.junit5.JUnit5EventsTest']
##TC[enteredTheMatrix]
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test1|]' name='test1 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test1|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass' metainfo='110:221']
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test2|]' name='test2 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test2|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='file:///directory/test2.java:12']
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test3|]' name='test3 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test3|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:test://MyTestClass/test4Method' metainfo='java.lang.String,java.util.List']
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:test4|]' name='test4 display name' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:test4|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:suite://com.intellij.junit5.JUnit5EventsTest']
""", builder.getFormattedOutput());
}
@@ -104,15 +104,14 @@ public class JUnit5EventsTest {
testContext.startTestOnly().finishWithFailure(new IllegalStateException());
Assertions.assertEquals("""
##teamcity[enteredTheMatrix]
##teamcity[suiteTreeStarted id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##teamcity[suiteTreeEnded id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0']
##teamcity[treeEnded]
##teamcity[rootName name = 'testMethod' location = 'java:suite://testMethod']
##teamcity[testStarted name='Class Configuration' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' ]
##teamcity[testFailed name='Class Configuration' id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' error='true' message='' details='TRACE']
##teamcity[testFinished name='Class Configuration' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' ]
##teamcity[testSuiteFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
##TC[enteredTheMatrix]
##TC[suiteTreeStarted id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##TC[suiteTreeEnded id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0']
##TC[treeEnded]
##TC[rootName name='testMethod' location='java:suite://testMethod']
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='Class Configuration' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:test://com.intellij.junit5.testData.MyTestClass/brokenStream' metainfo='']
##TC[testFailed id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='Class Configuration' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' error='true' message='' details='TRACE']
##TC[testFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='Class Configuration' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
""", builder.getFormattedOutput());
}
@@ -131,14 +130,14 @@ public class JUnit5EventsTest {
builder.getExecutionListener().executionFinished(testIdentifier, TestExecutionResult.successful());
Assertions.assertEquals("""
##teamcity[enteredTheMatrix]
##teamcity[suiteTreeStarted id='|[engine:engine|]/|[suite:suiteClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[suite:suiteClass|]' parentNodeId='0' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##teamcity[suiteTreeStarted id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##teamcity[suiteTreeEnded id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]']
##teamcity[suiteTreeEnded id='|[engine:engine|]/|[suite:suiteClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[suite:suiteClass|]' parentNodeId='0']
##teamcity[treeEnded]
##teamcity[testSuiteStarted id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]'locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##teamcity[testSuiteFinished id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]']
##TC[enteredTheMatrix]
##TC[suiteTreeStarted id='|[engine:engine|]/|[suite:suiteClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[suite:suiteClass|]' parentNodeId='0' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##TC[suiteTreeStarted id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##TC[suiteTreeEnded id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]']
##TC[suiteTreeEnded id='|[engine:engine|]/|[suite:suiteClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[suite:suiteClass|]' parentNodeId='0']
##TC[treeEnded]
##TC[testSuiteStarted id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##TC[testSuiteFinished id='|[engine:secondEngine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:secondEngine|]/|[class:testClass|]' parentNodeId='|[engine:engine|]/|[suite:suiteClass|]']
""", builder.getFormattedOutput());
}
@@ -153,15 +152,15 @@ public class JUnit5EventsTest {
testContext.startTestOnly().finishAborted();
Assertions.assertEquals("""
##teamcity[enteredTheMatrix]
##teamcity[suiteTreeStarted id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##teamcity[suiteTreeStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:test://com.intellij.junit5.testData.MyTestClass/brokenStream' metainfo='']
##teamcity[suiteTreeEnded id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
##teamcity[suiteTreeEnded id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0']
##teamcity[treeEnded]
##teamcity[testSuiteStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]'locationHint='java:test://com.intellij.junit5.testData.MyTestClass/brokenStream' metainfo='']
##teamcity[testIgnored name='brokenStream()' id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
##teamcity[testSuiteFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
##TC[enteredTheMatrix]
##TC[suiteTreeStarted id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0' locationHint='java:suite://com.intellij.junit5.testData.MyTestClass']
##TC[suiteTreeStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:test://com.intellij.junit5.testData.MyTestClass/brokenStream' metainfo='']
##TC[suiteTreeEnded id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
##TC[suiteTreeEnded id='|[engine:engine|]/|[class:testClass|]' name='MyTestClass' nodeId='|[engine:engine|]/|[class:testClass|]' parentNodeId='0']
##TC[treeEnded]
##TC[testSuiteStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]' locationHint='java:test://com.intellij.junit5.testData.MyTestClass/brokenStream' metainfo='']
##TC[testIgnored id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
##TC[testSuiteFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='brokenStream()' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='|[engine:engine|]/|[class:testClass|]']
""", builder.getFormattedOutput());
}
@@ -189,10 +188,10 @@ public class JUnit5EventsTest {
testContext.startExecution().finish();
Assertions.assertEquals("""
##teamcity[enteredTheMatrix]
##teamcity[rootName name = 'testClass' location = 'java:suite://testClass']
##teamcity[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='|[test|'s method|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' locationHint='java:test://com.intellij.junit5.testData.AnnotationsTestClass/test1' metainfo='']
##teamcity[testFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='|[test|'s method|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0']
##TC[enteredTheMatrix]
##TC[rootName name='testClass' location='java:suite://testClass']
##TC[testStarted id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='|[test|'s method|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0' locationHint='java:test://com.intellij.junit5.testData.AnnotationsTestClass/test1' metainfo='']
##TC[testFinished id='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' name='|[test|'s method|]' nodeId='|[engine:engine|]/|[class:testClass|]/|[method:testMethod|]' parentNodeId='0']
""", builder.getFormattedOutput());
}
}
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.junit5;
import com.intellij.junit5.report.LocationInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -53,8 +54,7 @@ class JUnit5NavigationTest {
myTestSource = anySupportedSource();
String locationHint = locationHint();
Assertions.assertTrue(locationHint.startsWith("locationHint='"), locationHint);
Assertions.assertTrue(locationHint.endsWith("'"), locationHint);
Assertions.assertEquals("java:suite://java.lang.String", locationHint);
}
@Test
@@ -115,7 +115,8 @@ class JUnit5NavigationTest {
private String locationHint() {
TestIdentifier testIdentifier = TestIdentifier.from(new ConfigurableTestDescriptor(myTestSource));
return JUnit5TestExecutionListener.getLocationHint(testIdentifier, null);
LocationInfo info = new LocationInfo(testIdentifier.getSource().orElse(null), null, null);
return info.locationHint();
}
private String locationHintValue() {
@@ -124,7 +125,8 @@ class JUnit5NavigationTest {
private static String locationHintValue(final ConfigurableTestDescriptor descriptor) {
TestIdentifier testIdentifier = TestIdentifier.from(descriptor);
return JUnit5TestExecutionListener.getLocationHintValue(testIdentifier.getSource().orElseThrow(IllegalStateException::new), null);
LocationInfo info = new LocationInfo(testIdentifier.getSource().orElseThrow(IllegalStateException::new), null, null);
return info.locationHint();
}
private static ClassSource anySupportedSource() {
@@ -87,17 +87,7 @@ public class JUnit5TestRunnerBuilder {
public void write(int b) {
myStringBuffer.append(new String(new byte[]{(byte)b}, StandardCharsets.UTF_8));
}
}, false, StandardCharsets.UTF_8)) {
@Override
protected long getDuration() {
return 0;
}
@Override
protected String getTrace(Throwable ex) {
return "TRACE";
}
};
}, false, StandardCharsets.UTF_8));
}
public TestDescriptorContext withTestMethod(Class<?> testClass, String methodName) throws NoSuchMethodException {
@@ -220,7 +210,12 @@ public class JUnit5TestRunnerBuilder {
}
public String getFormattedOutput() {
return StringUtil.convertLineSeparators(myStringBuffer.toString()).replaceAll("\\|r", "");
String out = myStringBuffer.toString();
return StringUtil.convertLineSeparators(out)
.replaceAll("\\|r", "")
.replaceAll("##teamcity\\[", "##TC[")
.replaceAll(" duration='[0-9]+'", "")
.replaceAll("details='.*?'\\]", "details='TRACE']");
}
public JUnit5TestExecutionListener getExecutionListener() {
@@ -11,14 +11,15 @@ import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.PlatformTestUtil;
import jetbrains.buildServer.messages.serviceMessages.*;
import jetbrains.buildServer.messages.serviceMessages.BaseTestMessage;
import jetbrains.buildServer.messages.serviceMessages.ServiceMessage;
import jetbrains.buildServer.messages.serviceMessages.TestStarted;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager;
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static com.intellij.junit6.ServiceMessageUtil.replaceAttributes;
@@ -44,39 +45,24 @@ public class JUnit6EventsTest extends AbstractTestFrameworkCompilingIntegrationT
ProcessOutput output = doStartTestsProcess(createRunMethodConfiguration("com.intellij.junit6.testData.MyTestClass", "test1"));
assertEmpty(output.err);
List<ServiceMessage> messages = output.messages;
Map<String, TestStarted> tests = getStartedTests(messages);
// Ensure our test method started with a proper location hint
TestStarted started = tests.values().stream()
.filter(t -> "java:test://com.intellij.junit6.testData.MyTestClass/test1".equals(t.getAttributes().get("locationHint")))
.findFirst().orElse(null);
assertNotNull(started);
assertEquals("java:test://com.intellij.junit6.testData.MyTestClass/test1", started.getAttributes().get("locationHint"));
String nodeId = started.getAttributes().get("nodeId");
// There should be multiple failures reported for the same test
List<ServiceMessage> failed = messages.stream()
.filter(m -> nodeId.equals(m.getAttributes().get("nodeId")))
.filter(m -> m instanceof TestFailed).toList();
assertSize(3, failed);
Set<String> errors = failed.stream().filter(m -> m.getAttributes().get("expected") != null)
.map(m -> "expected='" + m.getAttributes().get("expected") + "', actual='" + m.getAttributes().get("actual") + "'")
.collect(Collectors.toSet());
assertEquals(Set.of(
"expected='expected1', actual='actual1'",
"expected='expected2', actual='actual2'"
), errors);
// StdOut message should include our published entries
Set<String> outs = messages.stream()
.filter(m -> nodeId.equals(m.getAttributes().get("nodeId")))
.filter(m -> m instanceof TestStdOut)
.map(m -> m.getAttributes().get("out"))
String tests = output.messages.stream().filter(m -> m instanceof BaseTestMessage)
.map(m -> replaceAttributes(m, Map.of(
"timestamp", "##timestamp##",
"duration", "##duration##",
"details", "##details##"
)))
.map(m -> m.replaceAll("##teamcity\\[", "##TC["))
.map(s -> s.replaceAll("timestamp = [0-9\\-:.T]+", "timestamp = ##timestamp##"))
.collect(Collectors.toSet());
assertEquals(Set.of("timestamp = ##timestamp##, key1 = value1, stdout = out1\n"), outs);
.collect(Collectors.joining("\n"));
assertEquals("""
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/test1' metainfo='org.junit.jupiter.api.TestReporter']
##TC[testStdOut id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' out='timestamp = ##timestamp##, key1 = value1, stdout = out1|n']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='message1 ==> expected: <expected1> but was: <actual1>|nComparison Failure: ' expected='expected1' actual='actual1' details='##details##']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='message2 ==> expected: <expected2> but was: <actual2>|nComparison Failure: ' expected='expected2' actual='actual2' details='##details##']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='2 errors (2 failures)|n org.opentest4j.AssertionFailedError: message1 ==> expected: <expected1> but was: <actual1>|n org.opentest4j.AssertionFailedError: message2 ==> expected: <expected2> but was: <actual2>' details='##details##']
##TC[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##']""",
tests);
}
public void testContainerFailure() throws Exception {
@@ -84,46 +70,49 @@ public class JUnit6EventsTest extends AbstractTestFrameworkCompilingIntegrationT
assertEmpty(output.err);
// Expect a configuration failure
List<String> test = output.messages.stream().filter(BaseTestMessage.class::isInstance)
String test = output.messages.stream().filter(BaseTestMessage.class::isInstance)
.map(BaseTestMessage.class::cast)
.filter(m -> m.getTestName().equals("Class Configuration"))
.map(m -> replaceAttributes(m, Map.of("details", "##details##")))
.toList();
assertEquals(List.of(
"##teamcity[testStarted name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']",
"##teamcity[testFailed name='Class Configuration' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' error='true' message='java.lang.IllegalStateException: broken' details='##details##']",
"##teamcity[testFinished name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']"
), test);
.map(m -> m.replaceAll("##teamcity\\[", "##TC["))
.collect(Collectors.joining("\n"));
assertEquals("""
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/brokenStream' metainfo='']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' error='true' message='java.lang.IllegalStateException: broken' details='##details##']
##TC[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']""",
test);
}
public void testContainerDisabled() throws Exception {
ProcessOutput output = doStartTestsProcess(createRunClassConfiguration("com.intellij.junit6.testData.MyTestClass"));
assertEmpty(output.err);
List<String> tests = output.messages.stream().filter(m -> m instanceof BaseTestMessage)
String tests = output.messages.stream().filter(m -> m instanceof BaseTestMessage)
.map(m -> replaceAttributes(m, Map.of(
"timestamp", "##timestamp##",
"duration", "##duration##",
"message", "##message##",
"details", "##details##"
)))
.map(s -> s.replaceAll("timestamp = [0-9\\-:.T]+", "timestamp = ##timestamp##")).toList();
.map(m -> m.replaceAll("##teamcity\\[", "##TC["))
.map(s -> s.replaceAll("timestamp = [0-9\\-:.T]+", "timestamp = ##timestamp##"))
.collect(Collectors.joining("\n"));
assertEquals(List.of(
"##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/disabledTest' metainfo='']",
"##teamcity[testIgnored name='disabledTest()' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0' message='##message##']",
"##teamcity[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0']",
"##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/test1' metainfo='org.junit.jupiter.api.TestReporter']",
"##teamcity[testStdOut id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' out='timestamp = ##timestamp##, key1 = value1, stdout = out1|n']",
"##teamcity[testFailed name='test1(TestReporter)' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' expected='expected1' actual='actual1' details='##details##']",
"##teamcity[testFailed name='test1(TestReporter)' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' expected='expected2' actual='actual2' details='##details##']",
"##teamcity[testFailed name='test1(TestReporter)' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' details='##details##']",
"##teamcity[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##']",
"##teamcity[testIgnored name='brokenStreamDisabled()' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStreamDisabled()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStreamDisabled()|]' parentNodeId='0' message='##message##']",
"##teamcity[testStarted name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']",
"##teamcity[testFailed name='Class Configuration' id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' error='true' message='##message##' details='##details##']",
"##teamcity[testFinished name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']"
), tests);
assertEquals("""
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/disabledTest' metainfo='']
##TC[testIgnored id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0' message='##message##']
##TC[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' name='disabledTest()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:disabledTest()|]' parentNodeId='0']
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/test1' metainfo='org.junit.jupiter.api.TestReporter']
##TC[testStdOut id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' out='timestamp = ##timestamp##, key1 = value1, stdout = out1|n']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' expected='expected1' actual='actual1' details='##details##']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' expected='expected2' actual='actual2' details='##details##']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##' message='##message##' details='##details##']
##TC[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' name='test1(TestReporter)' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[method:test1(org.junit.jupiter.api.TestReporter)|]' parentNodeId='0' duration='##duration##']
##TC[testIgnored id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStreamDisabled()|]' name='brokenStreamDisabled()' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStreamDisabled()|]' parentNodeId='0' message='##message##']
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' locationHint='java:test://com.intellij.junit6.testData.MyTestClass/brokenStream' metainfo='']
##TC[testFailed id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0' error='true' message='##message##' details='##details##']
##TC[testFinished id='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' name='Class Configuration' nodeId='|[engine:junit-jupiter|]/|[class:com.intellij.junit6.testData.MyTestClass|]/|[test-factory:brokenStream()|]' parentNodeId='0']""",
tests);
}
public void testEscaping() throws Exception {
@@ -15,8 +15,8 @@ import org.jetbrains.idea.maven.aether.ArtifactRepositoryManager;
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor;
import org.junit.jupiter.api.DisplayName;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.intellij.junit6.ServiceMessageUtil.replaceAttributes;
@@ -33,26 +33,28 @@ public class JUnit6NavigationTest extends AbstractTestFrameworkCompilingIntegrat
ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(getTestContentRoot())
.addSourceFolder(getTestContentRoot() + "/test", true));
final ArtifactRepositoryManager repoManager = getRepoManager();
addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION), repoManager);
addMavenLibs(myModule, new JpsMavenRepositoryLibraryDescriptor("org.junit.jupiter", "junit-jupiter-api", JUnit6Constants.VERSION),
repoManager);
}
public void testNavigation() throws Exception {
ProcessOutput output = doStartTestsProcess(createRunClassConfiguration("org.example.impl.NavTest"));
assertEmpty(output.err);
List<String> messages = output.messages.stream().filter(m -> m.getAttributes().containsKey("locationHint"))
String messages = output.messages.stream().filter(m -> m.getAttributes().containsKey("locationHint"))
.map(m -> m.getAttributes().get("locationHint").startsWith("file://")
? replaceAttributes(m, Map.of("locationHint", "file://##path##"))
: m.asString())
.toList();
.map(m -> m.replaceAll("##teamcity\\[", "##TC["))
.collect(Collectors.joining("\n"));
assertEquals(List.of(
"##teamcity[suiteTreeStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' name='fileSourceDynamicTests()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/fileSourceDynamicTests' metainfo='']",
"##teamcity[suiteTreeNode id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' name='methodNavigation()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/methodNavigation' metainfo='']",
"##teamcity[testSuiteStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' name='fileSourceDynamicTests()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/fileSourceDynamicTests' metainfo='']",
"##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]/|[dynamic-test:#1|]' name='fileSource' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]/|[dynamic-test:#1|]' parentNodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' locationHint='file://##path##']",
"##teamcity[testStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' name='methodNavigation()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/methodNavigation' metainfo='']"
), messages);
assertEquals("""
##TC[suiteTreeStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' name='fileSourceDynamicTests()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/fileSourceDynamicTests' metainfo='']
##TC[suiteTreeNode id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' name='methodNavigation()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/methodNavigation' metainfo='']
##TC[testSuiteStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' name='fileSourceDynamicTests()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/fileSourceDynamicTests' metainfo='']
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]/|[dynamic-test:#1|]' name='fileSource' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]/|[dynamic-test:#1|]' parentNodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[test-factory:fileSourceDynamicTests()|]' locationHint='file://##path##']
##TC[testStarted id='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' name='methodNavigation()' nodeId='|[engine:junit-jupiter|]/|[class:org.example.impl.NavTest|]/|[method:methodNavigation()|]' parentNodeId='0' locationHint='java:test://org.example.impl.NavTest/methodNavigation' metainfo='']""",
messages);
}
@NotNull
@@ -91,7 +91,7 @@ public class JUnit6SuiteApiIntegrationTest extends AbstractTestFrameworkCompilin
private static Map<String, TestStarted> getStartedTests(List<ServiceMessage> messages) {
return messages.stream().filter(TestStarted.class::isInstance).map(TestStarted.class::cast)
.collect(Collectors.toMap(t -> t.getAttributes().get("id"), t -> t));
.collect(Collectors.toMap(t -> t.getAttributes().get("id"), t -> t, (existing, replacement) -> existing));
}
private static <T extends BaseTestMessage> Set<String> getTestIds(Map<String, TestStarted> tests, Set<String> locationHints) {
@@ -1,10 +1,8 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.example.api;
import org.example.impl.FirstTest;
import org.example.impl.SecondTest;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
import org.example.impl.*;
import org.junit.platform.suite.api.*;
@Suite
@SelectClasses({FirstTest.class, SecondTest.class})