Skip to content

Commit cec4762

Browse files
author
peng
committed
Cyclic graph error now reports 2 paths that leads to the cycle
1 parent 068c5bb commit cec4762

File tree

2 files changed

+69
-22
lines changed

2 files changed

+69
-22
lines changed

junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryResultValidator.java

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010

1111
package org.junit.platform.launcher.core;
1212

13-
import java.util.ArrayDeque;
14-
import java.util.HashSet;
15-
import java.util.Optional;
16-
import java.util.Queue;
17-
import java.util.Set;
13+
import java.util.*;
1814

1915
import org.junit.platform.commons.util.Preconditions;
2016
import org.junit.platform.engine.TestDescriptor;
@@ -38,39 +34,75 @@ void validate(TestEngine testEngine, TestDescriptor root) {
3834
() -> String.format(
3935
"The discover() method for TestEngine with ID '%s' must return a non-null root TestDescriptor.",
4036
testEngine.getId()));
41-
Optional<UniqueId> cyclicUIDs = getFirstCyclicUID(root);
42-
Preconditions.condition(!cyclicUIDs.isPresent(),
37+
Optional<String> cyclicGraphInfo = getCyclicGraphInfo(root);
38+
Preconditions.condition(!cyclicGraphInfo.isPresent(),
4339
() -> String.format("The discover() method for TestEngine with ID '%s' returned a cyclic graph: "
44-
+ "Cycle found at unique ID '%s'",
45-
testEngine.getId(), cyclicUIDs));
40+
+ cyclicGraphInfo
41+
)
42+
);
4643
}
4744

4845
/**
4946
* @return {@code true} if the tree does <em>not</em> contain a cycle; else {@code false}.
5047
*/
5148
boolean isAcyclic(TestDescriptor root) {
52-
return !getFirstCyclicUID(root).isPresent();
49+
return !getCyclicGraphInfo(root).isPresent();
5350
}
5451

55-
Optional<UniqueId> getFirstCyclicUID(TestDescriptor root) {
56-
Set<UniqueId> visited = new HashSet<>();
57-
Optional<UniqueId> cyclic = Optional.empty();
58-
visited.add(root.getUniqueId());
52+
Optional<String> getCyclicGraphInfo(TestDescriptor root) {
53+
HashMap<UniqueId, Optional<UniqueId>> visited = new HashMap<>();
54+
55+
visited.put(root.getUniqueId(), Optional.empty());
5956
Queue<TestDescriptor> queue = new ArrayDeque<>();
6057
queue.add(root);
6158
while (!queue.isEmpty()) {
62-
for (TestDescriptor child : queue.remove().getChildren()) {
59+
TestDescriptor parent = queue.remove();
60+
for (TestDescriptor child : parent.getChildren()) {
6361
UniqueId uid = child.getUniqueId();
64-
if (!visited.add(uid)) {
65-
cyclic = Optional.of(uid);
66-
return cyclic; // id already known: cycle detected!
62+
if (visited.containsKey(uid)) {// id already known: cycle detected!
63+
64+
StringBuilder path1 = new StringBuilder();
65+
path1.append(uid);
66+
addPath(visited, uid, path1);
67+
68+
StringBuilder path2 = new StringBuilder();
69+
path2.append(uid);
70+
UniqueId parentUID = parent.getUniqueId();
71+
path2.append(" <- ").append(parentUID);
72+
addPath(visited, parentUID, path2);
73+
74+
String msg = String.format("Test %s exists in at least two paths:", uid) +
75+
"\n\t" + path1.toString() +
76+
"\n\t" + path2.toString();
77+
return Optional.of(msg);
78+
}
79+
else {
80+
visited.put(uid, Optional.of(parent.getUniqueId()));
6781
}
6882
if (child.isContainer()) {
6983
queue.add(child);
7084
}
7185
}
7286
}
73-
return cyclic;
87+
return Optional.empty();
88+
}
89+
90+
private static void addPath(
91+
HashMap<UniqueId, Optional<UniqueId>> visited,
92+
UniqueId from,
93+
StringBuilder path
94+
) {
95+
UniqueId current = from;
96+
97+
while (visited.containsKey(current)) {
98+
Optional<UniqueId> backTraced = visited.get(current);
99+
if (backTraced.isPresent()) {
100+
path.append(" <- ").append(backTraced.get());
101+
current = backTraced.get();
102+
} else {
103+
break;
104+
}
105+
}
74106
}
75107

76108
}

platform-tests/src/test/java/org/junit/platform/launcher/core/EngineDiscoveryResultValidatorTests.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ void detectCycleWithDoubleRoot() {
3333

3434
root.addChild(root);
3535
assertFalse(validator.isAcyclic(root));
36-
assertEquals(validator.getFirstCyclicUID(root).get(), UniqueId.forEngine("root"));
36+
assertEquals(
37+
validator.getCyclicGraphInfo(root).get(),
38+
"Test [engine:root] exists in at least two paths:\n" +
39+
"\t[engine:root]\n" +
40+
"\t[engine:root] <- [engine:root]"
41+
);
3742
}
3843

3944
@Test
@@ -48,7 +53,12 @@ void detectCycleWithDoubleGroup() {
4853

4954
group2.addChild(group1);
5055
assertFalse(validator.isAcyclic(root));
51-
assertEquals(validator.getFirstCyclicUID(root).get(), UniqueId.forEngine("root").append("group", "1"));
56+
assertEquals(
57+
validator.getCyclicGraphInfo(root).get(),
58+
"Test [engine:root]/[group:1] exists in at least two paths:\n" +
59+
"\t[engine:root]/[group:1] <- [engine:root]\n" +
60+
"\t[engine:root]/[group:1] <- [engine:root]/[group:2] <- [engine:root]"
61+
);
5262
}
5363

5464
@Test
@@ -67,7 +77,12 @@ void detectCycleWithDoubleTest() {
6777

6878
group2.addChild(test1);
6979
assertFalse(validator.isAcyclic(root));
70-
assertEquals(validator.getFirstCyclicUID(root).get(), UniqueId.forEngine("root").append("test", "1"));
80+
assertEquals(
81+
validator.getCyclicGraphInfo(root).get(),
82+
"Test [engine:root]/[test:1] exists in at least two paths:\n" +
83+
"\t[engine:root]/[test:1] <- [engine:root]/[group:1] <- [engine:root]\n" +
84+
"\t[engine:root]/[test:1] <- [engine:root]/[group:2] <- [engine:root]"
85+
);
7186
}
7287

7388
}

0 commit comments

Comments
 (0)