Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ repositories {
snapshotsOnly()
}
}
maven(url = "https://central.sonatype.com/repository/maven-snapshots") {
mavenContent {
includeGroupByRegex("org\\.junit.*")
snapshotsOnly()
}
}
}

val moduleSourceSet = sourceSets.create("module") {
Expand Down Expand Up @@ -109,7 +115,7 @@ dependencies {
}
}

testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation(libs.versions.latestJUnit.map { "org.junit.jupiter:junit-jupiter:$it" })
testImplementation("org.junit.platform:junit-platform-testkit")
testImplementation(libs.mockito.junit.jupiter)
testImplementation(libs.assertj.core)
Expand All @@ -123,21 +129,22 @@ dependencies {
testRuntimeOnly("org.apache.logging.log4j:log4j-jul")

testFixturesImplementation(libs.junit4)
testFixturesImplementation(libs.versions.latestJUnit.map { "org.junit.platform:junit-platform-engine:$it" })
testFixturesRuntimeOnly("org.junit.platform:junit-platform-console")
}

tasks {
listOf(compileJava, compileTestFixturesJava).forEach { task ->
task.configure {
options.release.set(8)
if (javaToolchainVersion >= JavaLanguageVersion.of(20)) {
// `--release=8` is deprecated on JDK 20 and later
options.compilerArgs.add("-Xlint:-options")
}
compileJava {
options.release.set(8)
if (javaToolchainVersion >= JavaLanguageVersion.of(20)) {
// `--release=8` is deprecated on JDK 20 and later
options.compilerArgs.add("-Xlint:-options")
}
}
compileTestJava {
options.release.set(17)
listOf(compileTestJava, compileTestFixturesJava).forEach { task ->
task.configure {
options.release.set(17)
}
}
named<JavaCompile>(moduleSourceSet.compileJavaTaskName) {
options.release.set(9)
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[versions]
latestTestNG = "7.11.0" # Keep in sync with TestContext.java and README.adoc
snapshotTestNG = "7.12.0-SNAPSHOT"
latestJUnit = "6.0.0-SNAPSHOT"

[libraries]
assertj-core = { module = "org.assertj:assertj-core", version = "3.27.3" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
import org.testng.IAlterSuiteListener;
import org.testng.IClassListener;
import org.testng.IConfigurationListener;
import org.testng.IInvokedMethod;
import org.testng.IInvokedMethodListener;
import org.testng.ITestClass;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.xml.XmlSuite;

abstract class DefaultListener implements IClassListener, ITestListener, IConfigurationListener, IAlterSuiteListener {
abstract class DefaultListener
implements IClassListener, ITestListener, IConfigurationListener, IAlterSuiteListener, IInvokedMethodListener {

@Override
public void alter(List<XmlSuite> suites) {
Expand Down Expand Up @@ -99,4 +102,20 @@ public void beforeConfiguration(ITestResult tr) {
@Override
public void beforeConfiguration(ITestResult tr, ITestNGMethod tm) {
}

@Override
public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {
}

@Override
public void afterInvocation(IInvokedMethod method, ITestResult testResult) {
}

@Override
public void beforeInvocation(IInvokedMethod method, ITestResult testResult, ITestContext context) {
}

@Override
public void afterInvocation(IInvokedMethod method, ITestResult testResult, ITestContext context) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;
import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL;
import static org.junit.platform.engine.TestExecutionResult.aborted;
import static org.junit.platform.engine.TestExecutionResult.failed;
import static org.junit.platform.engine.TestExecutionResult.successful;
Expand All @@ -27,14 +28,17 @@
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.stream.Stream;

import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;
import org.testng.IInvokedMethod;
import org.testng.ITestClass;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.SkipException;
import org.testng.annotations.CustomAttribute;

class ExecutionListener extends DefaultListener {
Expand All @@ -46,13 +50,30 @@ class ExecutionListener extends DefaultListener {
private final Map<ClassDescriptor, Set<ITestResult>> classLevelFailureResults = new ConcurrentHashMap<>();

private final EngineExecutionListener delegate;
private final BooleanSupplier cancellationToken;
private final TestNGEngineDescriptor engineDescriptor;

ExecutionListener(EngineExecutionListener delegate, TestNGEngineDescriptor engineDescriptor) {
private volatile SkipException skipException;

ExecutionListener(EngineExecutionListener delegate, BooleanSupplier cancellationToken,
TestNGEngineDescriptor engineDescriptor) {
this.delegate = delegate;
this.cancellationToken = cancellationToken;
this.engineDescriptor = engineDescriptor;
}

@Override
public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {
if (cancellationToken.getAsBoolean()) {
SkipException exception = skipException;
if (exception == null) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't have to be strictly thread-safe. My intention is to avoid creating loads of exceptions with strack traces but whether it's 1 or 2 is irrelevant

exception = new SkipException("Execution cancelled");
skipException = exception;
}
throw exception;
}
}

@Override
public void onBeforeClass(ITestClass testClass) {
ClassDescriptor classDescriptor = requireNonNull(engineDescriptor.findClassDescriptor(testClass.getRealClass()),
Expand Down Expand Up @@ -218,7 +239,11 @@ private TestDescriptorFactory getTestDescriptorFactory() {
}

public TestExecutionResult toEngineResult() {
return toTestExecutionResult(engineLevelFailureResults);
TestExecutionResult testExecutionResult = toTestExecutionResult(engineLevelFailureResults);
if (testExecutionResult.getStatus() == SUCCESSFUL && skipException != null) {
return aborted(skipException);
}
return testExecutionResult;
}

private TestExecutionResult toTestExecutionResult(Set<ITestResult> results) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
import static org.testng.internal.RuntimeBehavior.TESTNG_MODE_DRYRUN;

import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;

import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.EngineExecutionListener;
Expand All @@ -26,6 +29,7 @@
import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver;
import org.testng.CommandLineArgs;
import org.testng.ITestNGListener;
import org.testng.SkipException;
import org.testng.TestNG;
import org.testng.annotations.DataProvider;
import org.testng.xml.XmlSuite.ParallelMode;
Expand Down Expand Up @@ -159,14 +163,22 @@ public TestDescriptor discover(EngineDiscoveryRequest request, UniqueId uniqueId
@Override
public void execute(ExecutionRequest request) {
EngineExecutionListener listener = request.getEngineExecutionListener();
BooleanSupplier cancellationToken = getCancellationToken(request);
TestNGEngineDescriptor engineDescriptor = (TestNGEngineDescriptor) request.getRootTestDescriptor();
listener.executionStarted(engineDescriptor);
engineDescriptor.prepareExecution();
ExecutionListener executionListener = new ExecutionListener(listener, engineDescriptor);
ExecutionListener executionListener = new ExecutionListener(listener, cancellationToken, engineDescriptor);
List<String> methodNames = engineDescriptor.getQualifiedMethodNames();
if (!methodNames.isEmpty()) {
configureAndRun(request.getConfigurationParameters(), executionListener, testMethods(methodNames),
Phase.EXECUTION);
try {
configureAndRun(request.getConfigurationParameters(), executionListener, testMethods(methodNames),
Phase.EXECUTION);
}
catch (SkipException e) {
if (!cancellationToken.getAsBoolean()) {
throw e;
}
}
}
listener.executionFinished(engineDescriptor, executionListener.toEngineResult());
}
Expand Down Expand Up @@ -207,6 +219,18 @@ private static void withTemporarySystemProperty(String key, String value, Runnab
}
}

private static BooleanSupplier getCancellationToken(ExecutionRequest request) {
return ReflectionSupport.findMethod(ExecutionRequest.class, "getCancellationToken") //
.map(method -> ReflectionSupport.invokeMethod(method, request)) //
.flatMap(TestNGTestEngine::toBooleanSupplier) //
.orElse(() -> false);
}

private static Optional<BooleanSupplier> toBooleanSupplier(Object cancellationToken) {
return ReflectionSupport.findMethod(cancellationToken.getClass(), "isCancellationRequested") //
.map(method -> () -> (boolean) ReflectionSupport.invokeMethod(method, cancellationToken));
}

interface Configurer {

static Configurer testClasses(Class<?>[] testClasses) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
import static org.junit.platform.launcher.TagFilter.includeTags;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
import static org.junit.platform.launcher.core.LauncherExecutionRequestBuilder.request;

import java.util.Map;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -209,18 +210,26 @@ void discoversAllClassesViaPackageSelector() {

@Test
void supportsPostDiscoveryFilters() {
var request = request().selectors(selectClass(SimpleTestCase.class)).filters(includeTags("bar")).build();
var launcher = LauncherFactory.create(
LauncherConfig.builder().enableTestEngineAutoRegistration(false).addTestEngines(testEngine).build());
var listener = new SummaryGeneratingListener();

var testPlan = launcher.discover(request);
launcher.execute(testPlan, listener);
var discoveryRequest = request() //
.selectors(selectClass(SimpleTestCase.class)) //
.filters(includeTags("bar")) //
.build();
var testPlan = launcher.discover(discoveryRequest);

var rootIdentifier = getOnlyElement(testPlan.getRoots());
var classIdentifier = getOnlyElement(testPlan.getChildren(rootIdentifier));
var methodIdentifier = getOnlyElement(testPlan.getChildren(classIdentifier));
assertThat(methodIdentifier.getDisplayName()).isEqualTo("successful");

var listener = new SummaryGeneratingListener();
var executionRequest = request(testPlan) //
.listeners(listener) //
.build();
launcher.execute(executionRequest);

assertThat(listener.getSummary().getTestsStartedCount()).isEqualTo(1);
assertThat(listener.getSummary().getTestsSucceededCount()).isEqualTo(1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import static org.junit.support.testng.engine.TestContext.testNGVersion;

import java.util.Map;

import example.basics.CancellingTestCase;
import example.basics.CustomAttributeTestCase;
import example.basics.ExpectedExceptionsTestCase;
import example.basics.InheritingSubClassTestCase;
import example.basics.NestedTestClass;
import example.basics.ParallelExecutionTestCase;
import example.basics.PostCancellationTestCase;
import example.basics.RetriedTestCase;
import example.basics.SimpleTestCase;
import example.basics.SuccessPercentageTestCase;
Expand All @@ -46,13 +49,17 @@
import example.configuration.methods.FailingBeforeClassConfigurationMethodTestCase;
import example.dataproviders.DataProviderMethodTestCase;

import org.apache.maven.artifact.versioning.ComparableVersion;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.Filter;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.descriptor.MethodSource;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.testkit.engine.Event;
import org.testng.SkipException;
import org.testng.internal.thread.ThreadTimeoutException;

Expand Down Expand Up @@ -330,4 +337,45 @@ void onlyExecutesNestedTestClassesThatMatchClassNameFilter() {
results.testEvents().assertStatistics(stats -> stats.started(1).finished(1));
}

@Test
void supportsCancellation() {
CancellingTestCase.cancellationToken = CancellationToken.create();
try {
var results = testNGEngine() //
.selectors(selectClass(CancellingTestCase.class), selectClass(PostCancellationTestCase.class)) //
.cancellationToken(CancellingTestCase.cancellationToken) //
.execute();

results.allEvents().assertEventsMatchExactly( //
event(engine(), started()), //
event(testClass(CancellingTestCase.class), started()), //
event(test("method"), started()), //
event(test("method"), finishedSuccessfully()), //
event(test("method"), started()), //
event(test("method"),
abortedWithReason(instanceOf(SkipException.class), message("Execution cancelled"))), //
event(testClass(CancellingTestCase.class), finishedSuccessfully()), //
event(testClass(PostCancellationTestCase.class), started()), //
event(test("method:test"), started()), //
event(test("method:test"), expectedDownstreamTestReportedResultForPriorCancellation()), //
event(testClass(PostCancellationTestCase.class), finishedSuccessfully()), //
event(engine(), abortedWithReason(instanceOf(SkipException.class), message("Execution cancelled"))));
}
finally {
CancellingTestCase.cancellationToken = null;
}
}

private static Condition<Event> expectedDownstreamTestReportedResultForPriorCancellation() {
var currentVersion = testNGVersion();
if (currentVersion.compareTo(new ComparableVersion("7.0")) < 0) {
return abortedWithReason(
message(it -> it.contains("depends on not successfully finished methods in group \"cancellation\"")));
}
if (currentVersion.compareTo(new ComparableVersion("7.4")) < 0) {
return finishedSuccessfully();
}
return abortedWithReason(instanceOf(SkipException.class), message("Execution cancelled"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@

import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.List;

import org.junit.jupiter.api.DisplayNameGenerator;

class TestNGVersionAppendingDisplayNameGenerator extends DisplayNameGenerator.Standard {

@Override
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
var regularDisplayName = super.generateDisplayNameForMethod(testClass, testMethod);
public String generateDisplayNameForMethod(List<Class<?>> enclosingInstanceTypes, Class<?> testClass,
Method testMethod) {
var regularDisplayName = super.generateDisplayNameForMethod(enclosingInstanceTypes, testClass, testMethod);
return MessageFormat.format("{0} [{1}]", regularDisplayName, TestContext.testNGVersion());
}

Expand Down
Loading