Skip to content

Commit 05d629d

Browse files
committed
Streamline dependencies for configurations
Closes #3000
1 parent 2e5d8bf commit 05d629d

File tree

6 files changed

+125
-7
lines changed

6 files changed

+125
-7
lines changed

CHANGES.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Current (7.10.0)
22

3+
Fixed: GITHUB-3000: Method predecessors lookup and/or method sorting is broken in certain inheritance and naming setups (Krishnan Mahadevan)
34
Fixed: GITHUB-3095: Super class annotated with ITestNGListenerFactory makes derived test class throw TestNGException on execution (Krishnan Mahadevan)
45
Fixed: GITHUB-3081: Discrepancy with combination of (Shared Thread pool and Method Interceptor) (Krishnan Mahadevan)
56
Fixed: GITHUB-2381: Controlling the inclusion of the listener at runtime (Krishnan Mahadevan)

testng-core/src/main/java/org/testng/internal/MethodHelper.java

+45-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.List;
88
import java.util.Map;
99
import java.util.Objects;
10+
import java.util.Optional;
1011
import java.util.Set;
1112
import java.util.concurrent.ConcurrentHashMap;
1213
import java.util.concurrent.atomic.AtomicReference;
@@ -325,6 +326,7 @@ private static Graph<ITestNGMethod> topologicalSort(
325326
Map<Object, List<ITestNGMethod>> testInstances = sortMethodsByInstance(methods);
326327

327328
XmlTest xmlTest = null;
329+
328330
for (ITestNGMethod m : methods) {
329331
if (xmlTest == null) {
330332
xmlTest = m.getXmlTest();
@@ -358,13 +360,12 @@ private static Graph<ITestNGMethod> topologicalSort(
358360
!(m.isBeforeGroupsConfiguration() || m.isAfterGroupsConfiguration());
359361
boolean isGroupAgnosticConfigMethod = !m.isTest() && anyConfigExceptGroupConfigs;
360362
if (isGroupAgnosticConfigMethod) {
361-
String[] groupsDependedUpon = m.getGroupsDependedUpon();
362-
if (groupsDependedUpon.length > 0) {
363-
for (String group : groupsDependedUpon) {
364-
ITestNGMethod[] methodsThatBelongToGroup =
365-
MethodGroupsHelper.findMethodsThatBelongToGroup(m, methods, group);
366-
predecessors.addAll(Arrays.asList(methodsThatBelongToGroup));
367-
}
363+
String[] groupsDependedUpon =
364+
Optional.ofNullable(m.getGroupsDependedUpon()).orElse(new String[0]);
365+
for (String group : groupsDependedUpon) {
366+
ITestNGMethod[] methodsThatBelongToGroup =
367+
MethodGroupsHelper.findMethodsThatBelongToGroup(m, methods, group);
368+
predecessors.addAll(Arrays.asList(methodsThatBelongToGroup));
368369
}
369370
}
370371

@@ -380,6 +381,31 @@ private static Graph<ITestNGMethod> topologicalSort(
380381
return result;
381382
}
382383

384+
private static Comparator<ITestNGMethod> bubbleUpIndependentMethodsFirst() {
385+
return (a, b) -> {
386+
boolean aIsIndependent = isIndependent(a);
387+
boolean bIsIndependent = isIndependent(b);
388+
if (aIsIndependent && bIsIndependent) {
389+
// Both a and b are independent methods. So treat them as equal.
390+
return 0;
391+
}
392+
if (aIsIndependent) {
393+
// First method is independent. So a should come before b.
394+
return -1;
395+
}
396+
if (bIsIndependent) {
397+
// Second method is independent. So a should come after b.
398+
return 1;
399+
}
400+
// Both a and b are dependent methods. So treat them as equal
401+
return 0;
402+
};
403+
}
404+
405+
private static boolean isIndependent(ITestNGMethod tm) {
406+
return tm.getMethodsDependedUpon().length == 0 && tm.getGroupsDependedUpon().length == 0;
407+
}
408+
383409
/**
384410
* This method is used to create a map of test instances and their associated method(s) . Used to
385411
* decrease the scope to only a methods instance when trying to find method dependencies.
@@ -442,6 +468,18 @@ private static List<ITestNGMethod> sortMethods(
442468
|| m.isBeforeSuiteConfiguration()
443469
|| m.isBeforeTestConfiguration();
444470
MethodInheritance.fixMethodInheritance(allMethodsArray, before);
471+
472+
// In-case the user has ended up using "dependsOn" on configurations, then sometimes
473+
// TestNG ends up finding the configuration methods in such a way that, it can cause
474+
// un-expected failures. This usually happens due to the fully qualified method names
475+
// acting up. So let's re-order the methods such that, the independent configurations always
476+
// bubble up to the top and the ones that have dependencies get pushed to the bottom.
477+
// That way, when we do a topologicalSort sort, the logic would work fine.
478+
479+
allMethodsArray =
480+
Arrays.stream(allMethodsArray)
481+
.sorted(bubbleUpIndependentMethodsFirst())
482+
.toArray(ITestNGMethod[]::new);
445483
}
446484

447485
topologicalSort(allMethodsArray, sl, pl, comparator);

testng-core/src/test/java/test/configuration/BeforeClassTest.java

+31
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
import java.util.ArrayList;
66
import java.util.List;
7+
import java.util.stream.Collectors;
8+
import org.testng.IReporter;
9+
import org.testng.ISuite;
10+
import org.testng.ITestResult;
711
import org.testng.TestNG;
812
import org.testng.annotations.Test;
913
import org.testng.xml.XmlSuite;
1014
import org.testng.xml.XmlSuite.ParallelMode;
1115
import test.SimpleBaseTest;
1216
import test.configuration.issue1035.InvocationTracker;
1317
import test.configuration.issue1035.MyFactory;
18+
import test.configuration.issue3000.TestClassSample;
1419

1520
public class BeforeClassTest extends SimpleBaseTest {
1621

@@ -48,4 +53,30 @@ public void ensureBeforeClassGetsCalledConcurrentlyWhenWorkingWithFactories() {
4853
previousThreadId = current.getThreadId();
4954
}
5055
}
56+
57+
@Test(description = "GITHUB-3000")
58+
public void ensureIndependentConfigurationsAlwaysRunFirstWhenUsingDependencies() {
59+
TestNG testng = create(TestClassSample.class);
60+
testng.setVerbose(2);
61+
62+
List<ITestResult> failures = new ArrayList<>();
63+
testng.addListener(
64+
new IReporter() {
65+
@Override
66+
public void generateReport(
67+
List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
68+
List<ITestResult> filtered =
69+
suites.stream()
70+
.flatMap(it -> it.getResults().values().stream())
71+
.flatMap(
72+
it ->
73+
it.getTestContext().getFailedConfigurations().getAllResults().stream())
74+
.collect(Collectors.toList());
75+
failures.addAll(filtered);
76+
}
77+
});
78+
testng.run();
79+
assertThat(testng.getStatus()).isZero();
80+
assertThat(failures).isEmpty();
81+
}
5182
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package test.configuration.issue3000;
2+
3+
import org.testng.annotations.BeforeClass;
4+
5+
abstract class MyBaseTestSample implements MyInterface {
6+
protected Object dependency;
7+
8+
public void setDependency(Object ignored) {}
9+
10+
@BeforeClass
11+
public void setupDependency() {
12+
dependency = new Object();
13+
}
14+
15+
// Had to add the "__" to this method (This is not how it looks like in the sample provided
16+
// in the GitHub issue). A combination of the fully qualified method names is what causes
17+
// this method to be first found in the configuration methods by TestNG and so it causes
18+
// the issue.
19+
@BeforeClass(dependsOnMethods = "setupDependency")
20+
public void __setupAdditionalDependency_() {}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package test.configuration.issue3000;
2+
3+
@SuppressWarnings("unused")
4+
interface MyInterface {
5+
void setDependency(Object dependency);
6+
7+
default Object getDependency() {
8+
return null;
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package test.configuration.issue3000;
2+
3+
import static org.testng.Assert.assertNotNull;
4+
5+
import org.testng.annotations.BeforeClass;
6+
import org.testng.annotations.Test;
7+
8+
public class TestClassSample extends MyBaseTestSample {
9+
10+
@BeforeClass
11+
public void beforeClass() {
12+
assertNotNull(dependency);
13+
}
14+
15+
@Test
16+
public void test() {}
17+
}

0 commit comments

Comments
 (0)