Skip to content

Commit 26387a5

Browse files
committed
Streamline running Parallel Dataproviders+retries
Closes #2934
1 parent a3c87ef commit 26387a5

File tree

9 files changed

+224
-45
lines changed

9 files changed

+224
-45
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-2934: Parallel Dataproviders & retries causes test result count to be skewed (Krishnan Mahadevan)
23
Fixed: GITHUB-2925: Issue in ITestcontext.getAllTestMethods() with annotation @BeforeSuite (Krishnan Mahadevan)
34
Fixed: GITHUB-2928: The constructor of TestRunner encountered NBC changes in 7.8.0 (Krishnan Mahadevan)
45
Fixed: GITHUB-581: Parameters of nested test suites are overridden(Krishnan Mahadevan)

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Collections;
77
import java.util.List;
88
import java.util.Map;
9+
import java.util.Objects;
910
import java.util.Optional;
1011
import java.util.Set;
1112
import java.util.concurrent.Callable;
@@ -817,7 +818,8 @@ private IRetryAnalyzer getRetryAnalyzerConsideringMethodParameters(ITestResult t
817818
this.m_retryAnalyzer = computeRetryAnalyzerInstanceToUse(tr);
818819
return this.m_retryAnalyzer;
819820
}
820-
final String keyAsString = getSimpleName() + "#" + getParameterInvocationCount();
821+
822+
final String keyAsString = getSimpleName() + "#" + hashParameters(tr);
821823
return m_testMethodToRetryAnalyzer.computeIfAbsent(
822824
keyAsString,
823825
key -> {
@@ -827,6 +829,11 @@ private IRetryAnalyzer getRetryAnalyzerConsideringMethodParameters(ITestResult t
827829
});
828830
}
829831

832+
private int hashParameters(ITestResult itr) {
833+
Object[] parameters = itr.getParameters();
834+
return Objects.hash(parameters);
835+
}
836+
830837
private static boolean isNotParameterisedTest(ITestResult tr) {
831838
return Optional.ofNullable(tr.getParameters()).orElse(new Object[0]).length == 0;
832839
}

testng-core/src/main/java/org/testng/internal/invokers/ITestInvoker.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.testng.internal.invokers;
22

33
import java.util.List;
4+
import java.util.concurrent.atomic.AtomicBoolean;
5+
import java.util.concurrent.atomic.AtomicInteger;
46
import org.testng.IInvokedMethod;
57
import org.testng.ITestContext;
68
import org.testng.ITestNGMethod;
@@ -14,9 +16,9 @@ public interface ITestInvoker {
1416

1517
class FailureContext {
1618

17-
int count = 0;
19+
AtomicInteger count = new AtomicInteger(0);
1820
List<Object> instances = Lists.newArrayList();
19-
boolean representsRetriedMethod = false;
21+
AtomicBoolean representsRetriedMethod = new AtomicBoolean(false);
2022
}
2123

2224
List<ITestResult> invokeTestMethods(

testng-core/src/main/java/org/testng/internal/invokers/MethodRunner.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@ public List<ITestResult> runInSequence(
5959
result.addAll(tmpResults);
6060
} else {
6161
List<ITestResult> retryResults = Lists.newArrayList();
62-
failure = testInvoker.retryFailed(tmArguments, retryResults, failure.count, context);
62+
failure =
63+
testInvoker.retryFailed(tmArguments, retryResults, failure.count.get(), context);
6364
result.addAll(retryResults);
6465
}
6566

6667
// If we have a failure, skip all the
6768
// other invocationCounts
68-
if (failure.count > 0
69+
if (failure.count.get() > 0
6970
&& (skipFailedInvocationCounts
7071
|| tmArguments.getTestMethod().skipFailedInvocations())) {
7172
while (invocationCount.getAndDecrement() > 0) {
@@ -122,7 +123,7 @@ public List<ITestResult> runInParallel(
122123
context,
123124
skipFailedInvocationCounts,
124125
invocationCount.get(),
125-
failure.count,
126+
failure.count.get(),
126127
testInvoker.getNotifier());
127128
workers.add(w);
128129
// testng387: increment the param index in the bag.

testng-core/src/main/java/org/testng/internal/invokers/TestInvoker.java

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ public FailureContext retryFailed(
234234
int failureCount,
235235
ITestContext testContext) {
236236
FailureContext failure = new FailureContext();
237-
failure.count = failureCount;
238-
failure.representsRetriedMethod = true;
237+
failure.count.set(failureCount);
238+
failure.representsRetriedMethod.set(true);
239239
do {
240240
failure.instances = Lists.newArrayList();
241241
Object[] parameterValues = arguments.getParameterValues();
@@ -533,7 +533,7 @@ private void handleInvocationResult(
533533
} else {
534534
testResult.setStatus(holder.status);
535535
if (holder.status == ITestResult.FAILURE && !holder.handled) {
536-
int count = failure.count++;
536+
int count = failure.count.getAndIncrement();
537537
if (testMethod.isDataDriven()) {
538538
count = 0;
539539
}
@@ -580,7 +580,7 @@ private ITestResult invokeMethod(
580580
long startTime = System.currentTimeMillis();
581581
InvokedMethod invokedMethod = new InvokedMethod(startTime, testResult);
582582

583-
if (!failureContext.representsRetriedMethod
583+
if (!failureContext.representsRetriedMethod.get()
584584
&& invoker.hasConfigurationFailureFor(
585585
arguments.getTestMethod(),
586586
arguments.getTestMethod().getGroups(),
@@ -827,46 +827,50 @@ public ITestResult registerSkippedTestResult(
827827
return result;
828828
}
829829

830+
private static final Object LOCK = new Object();
831+
830832
private StatusHolder considerExceptions(
831833
ITestNGMethod tm,
832834
ITestResult testResult,
833835
ExpectedExceptionsHolder exceptionsHolder,
834836
FailureContext failure) {
835-
StatusHolder holder = new StatusHolder();
836-
int status = testResult.getStatus();
837-
holder.handled = false;
838-
839-
Throwable ite = testResult.getThrowable();
840-
if (status == ITestResult.FAILURE && ite != null) {
841-
842-
// Invocation caused an exception, see if the method was annotated with @ExpectedException
843-
if (exceptionsHolder != null) {
844-
if (exceptionsHolder.isExpectedException(ite)) {
845-
testResult.setStatus(ITestResult.SUCCESS);
846-
status = ITestResult.SUCCESS;
847-
} else {
848-
if (isSkipExceptionAndSkip(ite)) {
849-
status = ITestResult.SKIP;
837+
synchronized (LOCK) {
838+
StatusHolder holder = new StatusHolder();
839+
int status = testResult.getStatus();
840+
holder.handled = false;
841+
842+
Throwable ite = testResult.getThrowable();
843+
if (status == ITestResult.FAILURE && ite != null) {
844+
845+
// Invocation caused an exception, see if the method was annotated with @ExpectedException
846+
if (exceptionsHolder != null) {
847+
if (exceptionsHolder.isExpectedException(ite)) {
848+
testResult.setStatus(ITestResult.SUCCESS);
849+
status = ITestResult.SUCCESS;
850850
} else {
851-
testResult.setThrowable(exceptionsHolder.wrongException(ite));
852-
status = ITestResult.FAILURE;
851+
if (isSkipExceptionAndSkip(ite)) {
852+
status = ITestResult.SKIP;
853+
} else {
854+
testResult.setThrowable(exceptionsHolder.wrongException(ite));
855+
status = ITestResult.FAILURE;
856+
}
853857
}
858+
} else {
859+
handleException(ite, tm, testResult, failure.count.getAndIncrement());
860+
holder.handled = true;
861+
status = testResult.getStatus();
862+
}
863+
} else if (status != ITestResult.SKIP && exceptionsHolder != null) {
864+
TestException exception = exceptionsHolder.noException(tm);
865+
if (exception != null) {
866+
testResult.setThrowable(exception);
867+
status = ITestResult.FAILURE;
854868
}
855-
} else {
856-
handleException(ite, tm, testResult, failure.count++);
857-
holder.handled = true;
858-
status = testResult.getStatus();
859-
}
860-
} else if (status != ITestResult.SKIP && exceptionsHolder != null) {
861-
TestException exception = exceptionsHolder.noException(tm);
862-
if (exception != null) {
863-
testResult.setThrowable(exception);
864-
status = ITestResult.FAILURE;
865869
}
870+
holder.originalStatus = testResult.getStatus();
871+
holder.status = status;
872+
return holder;
866873
}
867-
holder.originalStatus = testResult.getStatus();
868-
holder.status = status;
869-
return holder;
870874
}
871875

872876
private static void updateStatusHolderAccordingToTestResult(

testng-core/src/main/java/org/testng/internal/invokers/TestMethodWithDataProviderMethodWorker.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public List<ITestResult> call() {
7474
XmlSuite suite = m_testContext.getSuite().getXmlSuite();
7575

7676
final ITestInvoker.FailureContext failure = new ITestInvoker.FailureContext();
77-
failure.count = m_failureCount;
77+
failure.count.set(m_failureCount);
7878
try {
7979
tmpResults.add(
8080
m_testInvoker.invokeTestMethod(
@@ -92,15 +92,16 @@ public List<ITestResult> call() {
9292
suite,
9393
failure));
9494
} finally {
95-
m_failureCount = failure.count;
95+
m_failureCount = failure.count.get();
9696
if (failure.instances.isEmpty()) {
9797
m_testResults.addAll(tmpResults);
9898
} else {
9999
for (Object instance : failure.instances) {
100100
List<ITestResult> retryResults = Lists.newArrayList();
101101

102102
m_failureCount =
103-
m_testInvoker.retryFailed(
103+
m_testInvoker
104+
.retryFailed(
104105
new Builder()
105106
.usingInstance(instance)
106107
.forTestMethod(m_testMethod)
@@ -115,7 +116,8 @@ public List<ITestResult> call() {
115116
retryResults,
116117
m_failureCount,
117118
m_testContext)
118-
.count;
119+
.count
120+
.get();
119121
m_testResults.addAll(retryResults);
120122
}
121123
}

testng-core/src/test/java/test/dataprovider/DataProviderTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
import test.dataprovider.issue2819.TestClassUsingDataProviderRetrySample;
4848
import test.dataprovider.issue2819.TestClassWithMultipleRetryImplSample;
4949
import test.dataprovider.issue2888.SkipDataProviderSample;
50+
import test.dataprovider.issue2934.TestCaseSample;
51+
import test.dataprovider.issue2934.TestCaseSample.CoreListener;
52+
import test.dataprovider.issue2934.TestCaseSample.ToggleDataProvider;
5053

5154
public class DataProviderTest extends SimpleBaseTest {
5255

@@ -592,4 +595,78 @@ public void ensureParametersCopiedOnConfigFailures() {
592595
testNG.run();
593596
assertThat(listener.getParameters()).containsExactlyElementsOf(Arrays.asList(1, 2, 3, 4, 5));
594597
}
598+
599+
@Test(description = "GITHUB-2934")
600+
public void ensureParallelDataProviderWithRetryAnalyserWorks() {
601+
runTest(true);
602+
}
603+
604+
@Test(description = "GITHUB-2934")
605+
public void ensureSequentialDataProviderWithRetryAnalyserWorks() {
606+
runTest(false);
607+
}
608+
609+
private static void runTest(boolean isParallel) {
610+
TestNG testng = create(TestCaseSample.class);
611+
CoreListener listener = new CoreListener();
612+
ToggleDataProvider transformer = new ToggleDataProvider(isParallel);
613+
testng.addListener(listener);
614+
testng.addListener(transformer);
615+
testng.setVerbose(2);
616+
testng.run();
617+
SoftAssertions softly = new SoftAssertions();
618+
619+
softly
620+
.assertThat(listener.totalTests())
621+
.withFailMessage("We should have had 9 test results in total.")
622+
.isEqualTo(9);
623+
624+
// Assert passed tests
625+
softly
626+
.assertThat(listener.getPassedTests())
627+
.withFailMessage("We should have had ONLY 1 passed test.")
628+
.hasSize(1);
629+
listener
630+
.getPassedTests()
631+
.forEach(
632+
each ->
633+
softly
634+
.assertThat(each.wasRetried())
635+
.withFailMessage(
636+
"Passed test " + stringify(each) + " should NOT have been retried")
637+
.isFalse());
638+
639+
// Assert failed tests
640+
assertThat(listener.getFailedTests())
641+
.withFailMessage("We should have had 2 failed tests.")
642+
.hasSize(2);
643+
listener
644+
.getFailedTests()
645+
.forEach(
646+
each ->
647+
softly
648+
.assertThat(each.wasRetried())
649+
.withFailMessage(
650+
"Failed test " + stringify(each) + " should NOT have been retried")
651+
.isFalse());
652+
653+
// Assert skipped tests
654+
assertThat(listener.getSkippedTests())
655+
.withFailMessage("We should have had 6 skipped tests due to retries.")
656+
.hasSize(6);
657+
listener
658+
.getSkippedTests()
659+
.forEach(
660+
each ->
661+
softly
662+
.assertThat(each.wasRetried())
663+
.withFailMessage(
664+
"Skipped test " + stringify(each) + " should have been retried")
665+
.isTrue());
666+
softly.assertAll();
667+
}
668+
669+
private static String stringify(ITestResult itr) {
670+
return "(" + Arrays.toString(itr.getParameters()) + ")";
671+
}
595672
}

0 commit comments

Comments
 (0)