Skip to content

Commit 69674cc

Browse files
authored
Segregate native & user listeners before ordering (#2864)
* Segregate native & user listeners before ordering
1 parent c087eaa commit 69674cc

File tree

17 files changed

+224
-33
lines changed

17 files changed

+224
-33
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Current
2+
Fixed: GITHUB-2771: After upgrading to TestNG 7.5.0, setting ITestResult.status to FAILURE doesn't fail the test anymore (Julien Herr & Krishnan Mahadevan)
23
Fixed: GITHUB-2796: Option for onAfterClass to run after @AfterClass
34
Fixed: GITHUB-2857: XmlTest index is not set for test suites invoked with YAML
45

testng-collections/src/main/java/org/testng/collections/Lists.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,4 @@ public static <T> List<T> merge(List<T> l1, BiPredicate<T, T> condition, List<T>
7676
});
7777
return result;
7878
}
79-
80-
public static <K> List<K> newReversedArrayList(Collection<K> c) {
81-
List<K> list = newArrayList(c);
82-
Collections.reverse(list);
83-
return list;
84-
}
8579
}

testng-core-api/src/main/java/org/testng/internal/RuntimeBehavior.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.testng.internal;
22

3+
import java.util.Arrays;
4+
import java.util.List;
5+
import java.util.Optional;
36
import java.util.TimeZone;
47

58
/** This class houses handling all JVM arguments by TestNG */
@@ -16,13 +19,25 @@ public final class RuntimeBehavior {
1619
public static final String TESTNG_DEFAULT_VERBOSE = "testng.default.verbose";
1720
public static final String IGNORE_CALLBACK_INVOCATION_SKIPS = "testng.ignore.callback.skip";
1821
public static final String SYMMETRIC_LISTENER_EXECUTION = "testng.listener.execution.symmetric";
22+
public static final String PREFERENTIAL_LISTENERS = "testng.preferential.listeners.package";
1923

2024
private RuntimeBehavior() {}
2125

2226
public static boolean ignoreCallbackInvocationSkips() {
2327
return Boolean.getBoolean(IGNORE_CALLBACK_INVOCATION_SKIPS);
2428
}
2529

30+
/**
31+
* @return - A comma separated list of packages that represent special listeners which users will
32+
* expect to be executed after executing the regular listeners. Here special listeners can be
33+
* anything that a user feels should be executed ALWAYS at the end.
34+
*/
35+
public static List<String> getPreferentialListeners() {
36+
String packages =
37+
Optional.ofNullable(System.getProperty(PREFERENTIAL_LISTENERS)).orElse("com.intellij.rt.*");
38+
return Arrays.asList(packages.split(","));
39+
}
40+
2641
public static boolean strictParallelism() {
2742
return Boolean.getBoolean(STRICTLY_HONOUR_PARALLEL_MODE);
2843
}

testng-core/src/main/java/org/testng/SuiteRunner.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class SuiteRunner implements ISuite, IInvokedMethodListener {
6969
private final SuiteRunState suiteState = new SuiteRunState();
7070
private final IAttributes attributes = new Attributes();
7171
private final Set<IExecutionVisualiser> visualisers = Sets.newHashSet();
72+
private ITestListener exitCodeListener;
7273

7374
public SuiteRunner(
7475
IConfiguration configuration,
@@ -94,7 +95,7 @@ public SuiteRunner(
9495
useDefaultListeners,
9596
new ArrayList<>() /* method interceptor */,
9697
null /* invoked method listeners */,
97-
null /* test listeners */,
98+
new TestListenersContainer() /* test listeners */,
9899
null /* class listeners */,
99100
new DataProviderHolder(),
100101
comparator);
@@ -108,7 +109,7 @@ protected SuiteRunner(
108109
boolean useDefaultListeners,
109110
List<IMethodInterceptor> methodInterceptors,
110111
Collection<IInvokedMethodListener> invokedMethodListeners,
111-
Collection<ITestListener> testListeners,
112+
TestListenersContainer container,
112113
Collection<IClassListener> classListeners,
113114
DataProviderHolder holder,
114115
Comparator<ITestNGMethod> comparator) {
@@ -120,7 +121,7 @@ protected SuiteRunner(
120121
useDefaultListeners,
121122
methodInterceptors,
122123
invokedMethodListeners,
123-
testListeners,
124+
container,
124125
classListeners,
125126
holder,
126127
comparator);
@@ -134,7 +135,7 @@ private void init(
134135
boolean useDefaultListeners,
135136
List<IMethodInterceptor> methodInterceptors,
136137
Collection<IInvokedMethodListener> invokedMethodListener,
137-
Collection<ITestListener> testListeners,
138+
TestListenersContainer container,
138139
Collection<IClassListener> classListeners,
139140
DataProviderHolder attribs,
140141
Comparator<ITestNGMethod> comparator) {
@@ -146,6 +147,7 @@ private void init(
146147
this.xmlSuite = suite;
147148
this.useDefaultListeners = useDefaultListeners;
148149
this.tmpRunnerFactory = runnerFactory;
150+
this.exitCodeListener = container.exitCodeListener;
149151
List<IMethodInterceptor> localMethodInterceptors =
150152
Optional.ofNullable(methodInterceptors).orElse(Lists.newArrayList());
151153
setOutputDir(outputDir);
@@ -206,9 +208,7 @@ public <T> T newInstance(Constructor<T> constructor, Object... parameters) {
206208
}
207209

208210
skipFailedInvocationCounts = suite.skipFailedInvocationCounts();
209-
if (null != testListeners) {
210-
this.testListeners.addAll(testListeners);
211-
}
211+
this.testListeners.addAll(container.listeners);
212212
for (IClassListener classListener :
213213
Optional.ofNullable(classListeners).orElse(Collections.emptyList())) {
214214
this.classListeners.put(classListener.getClass(), classListener);
@@ -257,13 +257,19 @@ public void setReportResults(boolean reportResults) {
257257
useDefaultListeners = reportResults;
258258
}
259259

260+
ITestListener getExitCodeListener() {
261+
return exitCodeListener;
262+
}
263+
260264
private void invokeListeners(boolean start) {
261265
if (start) {
262-
for (ISuiteListener sl : Lists.newArrayList(listeners.values())) {
266+
for (ISuiteListener sl :
267+
ListenerOrderDeterminer.order(Lists.newArrayList(listeners.values()))) {
263268
sl.onStart(this);
264269
}
265270
} else {
266-
List<ISuiteListener> suiteListenersReversed = Lists.newReversedArrayList(listeners.values());
271+
List<ISuiteListener> suiteListenersReversed =
272+
ListenerOrderDeterminer.reversedOrder(listeners.values());
267273
for (ISuiteListener sl : suiteListenersReversed) {
268274
sl.onFinish(this);
269275
}
@@ -813,4 +819,19 @@ public List<ITestNGMethod> getAllMethods() {
813819
.flatMap(tr -> Arrays.stream(tr.getAllTestMethods()))
814820
.collect(Collectors.toList());
815821
}
822+
823+
static class TestListenersContainer {
824+
private final List<ITestListener> listeners = Lists.newArrayList();
825+
private final ITestListener exitCodeListener;
826+
827+
TestListenersContainer() {
828+
this(Collections.emptyList(), null);
829+
}
830+
831+
TestListenersContainer(List<ITestListener> listeners, ITestListener exitCodeListener) {
832+
this.listeners.addAll(listeners);
833+
this.exitCodeListener =
834+
Objects.requireNonNullElseGet(exitCodeListener, () -> new ITestListener() {});
835+
}
836+
}
816837
}

testng-core/src/main/java/org/testng/TestNG.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Set;
2121
import java.util.concurrent.LinkedBlockingQueue;
2222
import java.util.concurrent.TimeUnit;
23+
import org.testng.SuiteRunner.TestListenersContainer;
2324
import org.testng.annotations.ITestAnnotation;
2425
import org.testng.collections.Lists;
2526
import org.testng.collections.Maps;
@@ -29,6 +30,7 @@
2930
import org.testng.internal.DynamicGraph;
3031
import org.testng.internal.ExitCode;
3132
import org.testng.internal.IConfiguration;
33+
import org.testng.internal.ListenerOrderDeterminer;
3234
import org.testng.internal.OverrideProcessor;
3335
import org.testng.internal.ReporterConfig;
3436
import org.testng.internal.RuntimeBehavior;
@@ -924,7 +926,6 @@ private void initializeDefaultListeners() {
924926
if (m_failIfAllTestsSkipped) {
925927
this.exitCodeListener.failIfAllTestsSkipped();
926928
}
927-
addListener(this.exitCodeListener);
928929
if (m_useDefaultListeners) {
929930
addReporter(SuiteHTMLReporter.class);
930931
addReporter(Main.class);
@@ -1106,17 +1107,22 @@ private void runSuiteAlterationListeners() {
11061107
}
11071108

11081109
private void runExecutionListeners(boolean start) {
1109-
List<IExecutionListener> executionListeners = m_configuration.getExecutionListeners();
1110+
List<IExecutionListener> executionListeners =
1111+
ListenerOrderDeterminer.order(m_configuration.getExecutionListeners());
11101112
if (start) {
11111113
for (IExecutionListener l : executionListeners) {
11121114
l.onExecutionStart();
11131115
}
1116+
// Invoke our exit code listener after all the user's listeners have run.
1117+
exitCodeListener.onExecutionStart();
11141118
} else {
11151119
List<IExecutionListener> executionListenersReversed =
1116-
Lists.newReversedArrayList(executionListeners);
1120+
ListenerOrderDeterminer.reversedOrder(executionListeners);
11171121
for (IExecutionListener l : executionListenersReversed) {
11181122
l.onExecutionFinish();
11191123
}
1124+
// Invoke our exit code listener after all the user's listeners have run.
1125+
exitCodeListener.onExecutionFinish();
11201126
}
11211127
}
11221128

@@ -1128,7 +1134,11 @@ private static void usage() {
11281134
}
11291135

11301136
private void generateReports(List<ISuite> suiteRunners) {
1131-
for (IReporter reporter : m_reporters.values()) {
1137+
List<IReporter> reporters = new ArrayList<>(m_reporters.values());
1138+
// Add our Exit code listener as the last of the reporter so that we can still accommodate
1139+
// whatever changes were done by a user's reporting listener
1140+
reporters.add(exitCodeListener);
1141+
for (IReporter reporter : reporters) {
11321142
try {
11331143
long start = System.currentTimeMillis();
11341144
reporter.generateReport(m_suites, suiteRunners, m_outputDir);
@@ -1334,6 +1344,8 @@ private SuiteRunner createSuiteRunner(XmlSuite xmlSuite) {
13341344
DataProviderHolder holder = new DataProviderHolder();
13351345
holder.addListeners(m_dataProviderListeners.values());
13361346
holder.addInterceptors(m_dataProviderInterceptors.values());
1347+
TestListenersContainer container =
1348+
new TestListenersContainer(getTestListeners(), this.exitCodeListener);
13371349
SuiteRunner result =
13381350
new SuiteRunner(
13391351
getConfiguration(),
@@ -1343,7 +1355,7 @@ private SuiteRunner createSuiteRunner(XmlSuite xmlSuite) {
13431355
m_useDefaultListeners,
13441356
m_methodInterceptors,
13451357
m_invokedMethodListeners.values(),
1346-
m_testListeners.values(),
1358+
container,
13471359
m_classListeners.values(),
13481360
holder,
13491361
Systematiser.getComparator());

testng-core/src/main/java/org/testng/TestRunner.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.Date;
1111
import java.util.List;
1212
import java.util.Map;
13+
import java.util.Objects;
1314
import java.util.Set;
1415
import java.util.concurrent.BlockingQueue;
1516
import java.util.concurrent.LinkedBlockingQueue;
@@ -36,6 +37,7 @@
3637
import org.testng.internal.IContainer;
3738
import org.testng.internal.ITestClassConfigInfo;
3839
import org.testng.internal.ITestResultNotifier;
40+
import org.testng.internal.ListenerOrderDeterminer;
3941
import org.testng.internal.MethodGroupsHelper;
4042
import org.testng.internal.MethodHelper;
4143
import org.testng.internal.ResultMap;
@@ -118,6 +120,8 @@ public class TestRunner
118120
// The XML method selector (groups/methods included/excluded in XML)
119121
private final XmlMethodSelector m_xmlMethodSelector = new XmlMethodSelector();
120122

123+
private ITestListener exitCodeListener;
124+
121125
//
122126
// These next fields contain all the configuration methods found on this class.
123127
// At initialization time, they just contain all the various @Configuration methods
@@ -252,6 +256,13 @@ private void init(
252256
m_injectorFactory = m_configuration.getInjectorFactory();
253257
m_objectFactory = suite.getObjectFactory();
254258
setVerbose(test.getVerbose());
259+
if (suiteRunner == null) {
260+
if (suite instanceof SuiteRunner) {
261+
setExitCodeListener(((SuiteRunner) suite).getExitCodeListener());
262+
}
263+
} else {
264+
setExitCodeListener(suiteRunner.getExitCodeListener());
265+
}
255266

256267
boolean preserveOrder = test.getPreserveOrder();
257268
IMethodInterceptor builtinInterceptor =
@@ -963,15 +974,18 @@ private void logStart() {
963974
*/
964975
private void fireEvent(boolean isStart) {
965976
if (isStart) {
966-
for (ITestListener itl : m_testListeners) {
977+
for (ITestListener itl : ListenerOrderDeterminer.order(m_testListeners)) {
967978
itl.onStart(this);
968979
}
980+
this.exitCodeListener.onStart(this);
969981

970982
} else {
971-
List<ITestListener> testListenersReversed = Lists.newReversedArrayList(m_testListeners);
983+
List<ITestListener> testListenersReversed =
984+
ListenerOrderDeterminer.reversedOrder(m_testListeners);
972985
for (ITestListener itl : testListenersReversed) {
973986
itl.onFinish(this);
974987
}
988+
this.exitCodeListener.onFinish(this);
975989
}
976990
if (!isStart) {
977991
MethodHelper.clear(methods(this.getPassedConfigurations()));
@@ -1238,6 +1252,15 @@ void addConfigurationListener(IConfigurationListener icl) {
12381252
}
12391253
}
12401254

1255+
private void setExitCodeListener(ITestListener exitCodeListener) {
1256+
this.exitCodeListener = exitCodeListener;
1257+
}
1258+
1259+
@Override
1260+
public ITestListener getExitCodeListener() {
1261+
return Objects.requireNonNull(exitCodeListener, "ExitCodeListener cannot be null.");
1262+
}
1263+
12411264
private void dumpInvokedMethods() {
12421265
MethodHelper.dumpInvokedMethodInfoToConsole(getAllTestMethods(), getVerbose());
12431266
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.testng.internal;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
import java.util.List;
7+
import java.util.Objects;
8+
import java.util.function.Predicate;
9+
import java.util.stream.Collectors;
10+
import org.testng.collections.Lists;
11+
import org.testng.internal.collections.Pair;
12+
13+
/**
14+
* A Utility that helps us differentiate between a user's listener and preferential Listener.
15+
*
16+
* <p>When dealing with TestNG listeners we would need to ensure that the user created listeners are
17+
* invoked first followed by Preferential listeners. This is required so that we always honour any
18+
* state changes that a user's listener may have done to the internal state of objects that can
19+
* affect the outcome of the execution (for e.g.,{@link org.testng.ITestResult})
20+
*
21+
* <p>The ordering must be done such that, when dealing with "beforeXXX|afterXXX" we group all the
22+
* IDE listeners at the end. That way, we can always ensure that the preferential listeners (for
23+
* e.g., IDE listeners) always honour the changes that were done to the TestNG internal states and
24+
* give a consistent experience for users.
25+
*/
26+
public final class ListenerOrderDeterminer {
27+
28+
private ListenerOrderDeterminer() {
29+
// Defeat instantiation
30+
}
31+
32+
private static final List<String> PREFERENTIAL_PACKAGES =
33+
RuntimeBehavior.getPreferentialListeners().stream()
34+
.map(each -> each.replaceAll("\\Q.*\\E", ""))
35+
.collect(Collectors.toList());
36+
37+
private static final Predicate<Class<?>> SHOULD_ADD_AT_END =
38+
clazz ->
39+
PREFERENTIAL_PACKAGES.stream()
40+
.anyMatch(each -> clazz.getPackage().getName().contains(each));
41+
42+
/**
43+
* @param original - The original collection of listeners
44+
* @return - A re-ordered collection wherein preferential listeners are added at the end
45+
*/
46+
public static <T> List<T> order(Collection<T> original) {
47+
Pair<List<T>, List<T>> ordered = arrange(original);
48+
List<T> ideListeners = ordered.first();
49+
List<T> regularListeners = ordered.second();
50+
return Lists.merge(regularListeners, ideListeners);
51+
}
52+
53+
/**
54+
* @param original - The original collection of listeners
55+
* @return - A reversed ordered list wherein the user listeners are found in reverse order
56+
* followed by preferential listeners also in reverse order.
57+
*/
58+
public static <T> List<T> reversedOrder(Collection<T> original) {
59+
Pair<List<T>, List<T>> ordered = arrange(original);
60+
List<T> preferentialListeners = ordered.first();
61+
List<T> regularListeners = ordered.second();
62+
Collections.reverse(regularListeners);
63+
Collections.reverse(preferentialListeners);
64+
return Lists.merge(regularListeners, preferentialListeners);
65+
}
66+
67+
private static <T> Pair<List<T>, List<T>> arrange(Collection<T> original) {
68+
List<T> preferentialListeners = new ArrayList<>();
69+
List<T> regularListeners = new ArrayList<>();
70+
original.stream()
71+
.filter(Objects::nonNull)
72+
.forEach(
73+
each -> {
74+
if (SHOULD_ADD_AT_END.test(each.getClass())) {
75+
preferentialListeners.add(each);
76+
} else {
77+
regularListeners.add(each);
78+
}
79+
});
80+
return new Pair<>(preferentialListeners, regularListeners);
81+
}
82+
}

0 commit comments

Comments
 (0)