1010
1111package 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
1915import org .junit .platform .commons .util .Preconditions ;
2016import 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}
0 commit comments