Skip to content

Enable JDK promise support and start to support reactive pipeline all the way - #6012

Open
rmannibucau wants to merge 1 commit into
junit-team:mainfrom
rmannibucau:dev/reactve-support
Open

rmannibucau wants to merge 1 commit into
junit-team:mainfrom
rmannibucau:dev/reactve-support

Conversation

@rmannibucau

Copy link
Copy Markdown
Contributor

proposed solution to #5292


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@FroMage FroMage left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is much larger than my proposal, and more complete in terms of supporting async types for parallel execution.

Unlike my proposal though, it does not support pluggable async types, so as such it is not very reactive-framework-friendly.

It also does not support a pluggable way to offload the original test method onto the proper reactive context (the event loop), but I have to investigate if this could be achieved by a JUnit interceptor (depends if they have access to the method return value, I guess).

static final AtomicInteger beforeEachCount = new AtomicInteger();
static final AtomicInteger afterEachCount = new AtomicInteger();

@BeforeEach

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Are lifecycle methods that return an async type also supported?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Very good point and no I missed it, will work on that

*/
public static boolean isFullySupported(Class<?> type) {
Preconditions.notNull(type, "type must not be null");
return CompletionStage.class.isAssignableFrom(type) || Future.class.isAssignableFrom(type);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The problem with this is that it artificially limits the usable async types. We don't use those types in Quarkus, we use Uni, in Vert.x they use a different Future, in Spring they use Mono

All of which can be converter to a CompletionStage easily, but this PR is missing the SPI to achieve it.

Otherwise every test will have to end with .convertToCompletionStage() which is just unnecessary boilerplate for users.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, my approach is that it is less critical to have to add a call which doesn't break the execution model than to not be able to use a programming model at all but agree we'll need to enable what you PR goal is. I just didn't want to do it before the API/SPI is fully reactive friendly cause it is likely it misses the line otherwise (it is already a lot of rework in this PR to say it differently).

does it sound ok to finish this one then add "your" feature on top - and I totally agree it will be better but what I'm not sure today is that a new API will be needed, I hope we can make it functional with interceptors or something existing even if totally unsure yet?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Well, if you don't introduce a new API/SPI, you'll have to use converting libraries such as https://github.com/smallrye/smallrye-reactive-converters/tree/main/reactive-converters which would be fine for us.

@rmannibucau
rmannibucau force-pushed the dev/reactve-support branch 4 times, most recently from ead98ab to 1349e04 Compare August 27, 2026 22:00
Signed-off-by: Romain Manni-Bucau <rmannibucau@gmail.com>
@testlens-app

testlens-app Bot commented Aug 27, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

⚠️ TestLens detected flakiness ⚠️

Test Summary

CI / Build / Linux > :platform-tooling-support-tests:test

Test Runs Flakiness
MemoryCleanupTests > runsWithSmallHeapSize(OutputFiles, OutputFiles) ⚠️ 1% 🟡

🏷️ Commit: e92a1b3
▶️ Tests: 47512 executed
⚪️ Checks: 15/15 completed

Test Failures

MemoryCleanupTests > runsWithSmallHeapSize(OutputFiles, OutputFiles) (:platform-tooling-support-tests:test in CI / Build / Linux)
org.opentest4j.AssertionFailedError: execution timed out after 30000 ms
	at org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(Assertions.java:3635)
	at platform.tooling.support.tests.MemoryCleanupTests.runsWithSmallHeapSize(MemoryCleanupTests.java:50)
Caused by: org.junit.jupiter.api.timeout.PreemptiveTimeoutUtils$ExecutionTimeoutException: Execution timed out in thread junit-timeout-thread-1
	at java.base/jdk.internal.misc.Unsafe.park(Native Method)
	at java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:520)
	at java.base/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:4364)
	at java.base/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:4310)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1752)
	at java.base/java.lang.ProcessImpl.waitFor(ProcessImpl.java:423)
	at org.junit.platform.tests.process.WatchedProcess.waitFor(WatchedProcess.java:34)
	at org.junit.platform.tests.process.ProcessStarter.startAndWait(ProcessStarter.java:77)
	at platform.tooling.support.tests.MemoryCleanupTests.executeWithSmallHeapSize(MemoryCleanupTests.java:89)
	at platform.tooling.support.tests.MemoryCleanupTests.lambda$runsWithSmallHeapSize$0(MemoryCleanupTests.java:50)

Learn more about TestLens at testlens.app/docs.

@rmannibucau

Copy link
Copy Markdown
Contributor Author

up?

@marcphilipp

Copy link
Copy Markdown
Member

@rmannibucau Thanks for the PR! Since #5292 is waiting for additional interest from the community, reviewing this PR is not a priority for us at the moment.

@FroMage

FroMage commented Sep 11, 2026

Copy link
Copy Markdown

@rmannibucau I just tested it and it does work for us. It's a little less than what I provided in my PR in the sense that we can't control the execution of the test method to offload it to another thread (we can only convert its return value), but we can achieve that offloading via another sort of interceptor, so we can work around it.

@rmannibucau

Copy link
Copy Markdown
Contributor Author

@FroMage

we can't control the execution of the test method to offload it to another thread

my idea was that the promise controls the thread so it was built-in by design, did I miss sthg?

@FroMage

FroMage commented Sep 11, 2026

Copy link
Copy Markdown

As far as I can see, I can only control what happens in the CompletionStage, which means the test method is still executed outside of my control. There's two phases happening in the test method: the test method itself, which creates the CompletionStage, and the body of the CompletionStage which is executed later. I can only control the second half, not the first.

But again, I can work around that.

The other thing I can think of is that CompletionStage is eager, so converting from our promise type to it will trigger subscription and execution, but again, in this case I don't think it's an actual issue.

@rmannibucau

Copy link
Copy Markdown
Contributor Author

@FroMage yep but the first one is the trigger which must not block (else it is pointless to use this programming model) so second is sufficient from my view and is always workaround-able using supplyAsync(,pool)

@FroMage

FroMage commented Sep 11, 2026

Copy link
Copy Markdown

It's not just about blocking: in our case the creation of these async tasks must happen on an event loop, so we need to redirect the execution of the test method on the event loop, because we can start making those promises. Anyway, as I said, we can work around it using other kinds of interceptors. The biggest hurdle was the method return type being disallowed.

@rmannibucau

Copy link
Copy Markdown
Contributor Author

@FroMage the point is more that you can always do it in a promise or using an injected helper which does facade a queue push/pop using completionstages. That said I get it but also brings the context in the loop issue generally speaking so maybe your interceptor (or a parameter/field injection) is saner 🤔 . Anyway if this PR already enables your case I see it as a baby step forward ;)

@FroMage

FroMage commented Sep 11, 2026

Copy link
Copy Markdown

Yeah, as I said, at this point, both my PR (less complete, but smaller) or yours achieve our goals, so I'm good with either one, I just hope we can get any merged soon so I don't have to commit atrocious classloader mocks to workaround this 😬

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants