junit-team / junit5

✅ The 5th major version of the programmer-friendly testing framework for Java and the JVM
https://junit.org
Eclipse Public License 2.0
6.43k stars 1.49k forks source link

JUnit parallel executor running too many tests with a fixed policy. #3108

Open OPeyrusse opened 1 year ago

OPeyrusse commented 1 year ago

JUnit only parallel executor service relies on a ForkJoinPool. Unfortunately, this does not play well with code also using ForkJoinPools.

The observed issue is that, when activating parallel tests, JUnit uses ForkJoinPoolHierarchicalTestExecutorService. However, our tests are also using ForkJoinPools and ForkJoinTasks. The orchestration of the test awaits for the completion of those tasks before moving on in the tests. But the issue is that ForkJoinTask and ForkJoinWorkerThread are capable of detecting the use of the FJP framework (here for example) and react to it. As JUnit tasks and the project tasks are in different ForkJoinPools, they cannot help each other. This only results in more tests being started by already running and incomplete tests.

This can be an issue when tests are resource sensitive. For example, it may not be possible to open too many connections to a Database. Tough not explicitly illustrated in this project, we also faced StackOverflowError because of recursive test executions in a single worker. In the following logs, produced by the reproducing project, we can see that two workers are recursively starting tests before finishing any:

ForkJoinPool-1-worker-1 starting a new test. Running: 2
ForkJoinPool-1-worker-2 starting a new test. Running: 1
ForkJoinPool-1-worker-1 starting a new test. Running: 3
ForkJoinPool-1-worker-1 starting a new test. Running: 4
ForkJoinPool-1-worker-1 starting a new test. Running: 5
ForkJoinPool-1-worker-2 starting a new test. Running: 6
ForkJoinPool-1-worker-1 starting a new test. Running: 7
ForkJoinPool-1-worker-1 starting a new test. Running: 8
ForkJoinPool-1-worker-1 starting a new test. Running: 9
ForkJoinPool-1-worker-2 starting a new test. Running: 10
ForkJoinPool-1-worker-1 starting a new test. Running: 11
ForkJoinPool-1-worker-1 starting a new test. Running: 12
ForkJoinPool-1-worker-1 starting a new test. Running: 14
ForkJoinPool-1-worker-2 starting a new test. Running: 13
ForkJoinPool-1-worker-1 starting a new test. Running: 15
ForkJoinPool-1-worker-1 starting a new test. Running: 16
ForkJoinPool-1-worker-1 starting a new test. Running: 17
ForkJoinPool-1-worker-1 starting a new test. Running: 18
ForkJoinPool-1-worker-2 starting a new test. Running: 19
ForkJoinPool-1-worker-1 starting a new test. Running: 20

worker-1 started 15 tests, worker-2 started 5 tests

In actual code, given the location of the point of respawn, this can generate very large stacks.

An alternative implementation of the executor service, as shown in this PR, using a standard Thread Executor, would not show similar issues, at the expense of not ideally orchestrating multiple executions.

Let's note that this is a very sneaky issue. Even in this project, we can see that the call to ForkJoinTask#get is not visible in the JUnit method. And it can be as deep as we want in the stack, even hidden to some users as it is happening in a used framework or tool. It may not be always possible for a user to detect that pattern.

Steps to reproduce

See this project https://github.com/OPeyrusse/junitforkjoinpool After building the project, run the test class TestCalculator (or look at the README if changed since opening this issue).

Context

Deliverables

OPeyrusse commented 1 year ago

Some additional thoughts: Looking at the code and being used to work with ForkJoinPool, I can guess some benefits of using the ForkJoin framework. It would naturally focus on sub-tasks to complete the execution of a given class or parameterized tests before moving to other classes. However, IMO, the mistake is that a ForkJoinTask should only run CPU intensive tasks. JUnit cannot know the content of tests. Some may be real unit tests, only depending on the CPU. Some may be real unit tests but reading the disk. Some may be more integration tests, making network calls using whatever framework. In those cases, for defensive measures, users of the ForkJoin framework have to rely on ManagedBlock. The drawback of Managed blocker is that they start new threads.

Yet another solution could be to use a ForkJoinPool for scheduling of the tests and a standard executor for the execution of the tests.

OPeyrusse commented 1 year ago

There is also a short snippet reproducing this behavior here. However, the current choice was to say that the doc will be update to say the configuration limits the number of threads, not the number of tests. This issue focuses on the number of tests being run, as it is the actual problem at stake, leading to StackOverflowErrors.

OPeyrusse commented 1 year ago

To add to this, it is not currently possible to have parallel tests without being tricked by JUnit current implementation. It is either sequential tests or parallel tests that must take care of what they do.

marcphilipp commented 1 year ago

@OPeyrusse Do the features released as part of 5.9.2 help in your case?

OPeyrusse commented 1 year ago

Hello @marcphilipp , nope it will not help. The feature advertised was already one we looked at, when it was at the stage of PR. If you look again, you will see that only two threads from the ForkJoinPool are being used. Tough not tested, I would even say that with only one thread, you can reproduce the problem above. As stated in my description, it comes from the FJ framework detecting the use of ForkJoinPool in a ForkJoinThread, leading it to attempt some work instead of waiting. So limiting the number of threads in the pool or its saturation will not resolve the issue. Here, the issue is that too many tests are started.

I can give you an analogy with the following story: There is a factory that can manufacture cars. It takes time, more or less depending on the car models. Building a car is the analogy to running a single test. The factory has enough workers to build several cars concurrently. There are also clerks coming to the factory with orders to manufacture cars. Those are JUnit ForkJoinTasks, produced after discovering that tests must be run. When a clerk arrives at the factory, it delivers the order to the factory, that immediately starts working on it. The factory has chosen, for company reasons, to operate using the FJP methodology (they are smart). The weird thing is that the clerk sees that, and because the clerk company is also using the FJP methodology, the clerk decides not to stay idle and do something while waiting for the car to be made. Obviously, the clerk cannot join the factory workers - it is not its job - so to do something, the clerk picks the next task available to him, which is go back to the office to pick another order. Because clerks make faster trips to the office than the factory can produce cars, the factory becomes overflowed with orders - which, in the computer world, translates to memory starvation, stack overflow, etc

I hope this helps understand the logic triggering the issue :smiley:

marcphilipp commented 1 year ago

Nice analogy. I wish the car factory wouldn't leak implementation details... 😉

Yet another solution could be to use a ForkJoinPool for scheduling of the tests and a standard executor for the execution of the tests.

I think we can consider that. Would you like to give it a shot?

OPeyrusse commented 1 year ago

Sure, that would be interesting. If you have some guidance on how to write it - and mostly how to write the associated tests - I am ready to try it.

marcphilipp commented 1 year ago

@OPeyrusse In ParallelExecutionIntegrationTests we have tests that check that tests are run on different threads by collecting the names of those threads (see the nested ThreadReporter class). I think for this, all we'd need to verify is that they are not ForkJoinWorkerThread.

Generally, I think we should make this new behavior opt-in via a configuration parameter since it introduces additional overhead.

OPeyrusse commented 1 year ago

Thanks for the tips. I will look at it in the coming days.

OPeyrusse commented 1 year ago

Just a quick update: I have not abandoned nor forgotten this issue. It is simply that I am currently taking care of my children during the holiday season here. I already have ideas on how to proceed and will start on this next week :smile:

asardaes commented 1 year ago

If I may ask, do you think it would be possible to allow custom HierarchicalTestExecutorService implementations? I imagine it would be possible to simply extend the configuration of the dynamic strategy to allow choosing between predefined implementations, but I would personally like more control over the thread pool in a programmatic way. I think the custom strategy could then check if junit.jupiter.execution.parallel.config.custom.class is an instance of ParallelExecutionConfigurationStrategy or HierarchicalTestExecutorService, or something like that.

marcphilipp commented 1 year ago

@asardaes That's an interesting idea. Could you please raise a new issue for it?

asardaes commented 1 year ago

@marcphilipp since you mentioned @OPeyrusse's implementation should be opt-in, I think it's worth discussing in the context of this issue, killing 2 birds with 1 stone. I gave it some more thought, let me explain what I have in mind.

A new configuration parameter could be added, say junit.jupiter.execution.parallel.executor.class. If this is not set, it defaults to either org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService or org.junit.platform.engine.support.hierarchical.ForkJoinPoolHierarchicalTestExecutorService depending on the value of junit.jupiter.execution.parallel.enabled. With this new parameter, users could either choose the "standard executor" implemented by @OPeyrusse or a custom one they themselves implement; if the users specify a custom one, they could decide whether to ignore junit.jupiter.execution.parallel.enabled or not (well, a custom one can basically ignore all parameters and hard-code everything anyway, but that's up to the user).

To instantiate junit.jupiter.execution.parallel.executor.class, all implementations would need some convention that could be used by JupiterTestEngine (and any other custom engine that wants to support different executors). I imagine it would be easiest to require them to have a no-arg constructor, and then adding a default configure(ConfigurationParameters) method to HierarchicalTestExecutorService. This would allow custom implementations to read custom configuration parameters, but it would mean the executors included out of the box in JUnit would have to check if junit.jupiter.execution.parallel.config.strategy=custom and instantiate junit.jupiter.execution.parallel.config.custom.class themselves.

I'm not so sure about the instantiation/configuration convention. Given that both included executor services are marked as stable, I imagine they need to keep existing constructors, but adding a new one and having a default configure method in the interface should be fine, no?

And for completeness, the logic in a test engine would have to be roughly:

if (junit.jupiter.execution.parallel.executor.class is set) {
    // instantiate it and call its configure method
} else {
    // check junit.jupiter.execution.parallel.enabled
    // and instantiate an included Executor,
    // possibly with a custom junit.jupiter.execution.parallel.config.custom.class
}
OPeyrusse commented 1 year ago

@asardaes do you have a special use-case in mind, driving your question?

I am asking because my idea for this issue was to have two different ways of running the tests: one inlining the execution withing the ForkJoinPool, and one submitting a task to an external Executor and waiting for the result. Behind this, though I don't know the historical reasons for creating a test executor on top of the ForkJoinPool, I assume that it comes from its work-stealing feature. It can benefit to test execution when you launch a group of tests - often those under a class - as the executor should focus on executing those tests in parallel instead of starting the tests of the next class. I want to keep this hypothetical design choice. Therefore, my task is only to separate the way one individual test is running. If we wanted some extension, I could open this to load a user-selected class, from its name, as you suggested. But it would only affect individual test execution, not the general scheduling of all the tests. Would that suits your needs?

asardaes commented 1 year ago

@OPeyrusse no, I don't think that would fit what I had in mind, but if your implementation doesn't need a completely new HierarchicalTestExecutorService but rather does something else inside an existing one, then I guess my idea really would require a separate issue.

Personally I'm just experimenting with kotlin coroutines, so I don't need a custom test engine, I just want a custom executor.

OPeyrusse commented 1 year ago

@asardaes If you say so. Not being a core developer of JUnit, I cannot decide what to do. I will let @marcphilipp decide what to do and will start going forward with my plan, that is not creating a new executor.

marcphilipp commented 1 year ago

@asardaes I think what you describe in https://github.com/junit-team/junit5/issues/3108#issuecomment-1537380013 makes a lot of sense. However, as you said in https://github.com/junit-team/junit5/issues/3108#issuecomment-1540593193, this issue seems more like making an existing executor behave differently based on a different configuration parameter. So I think a new issue for your use case would be the way to go.

OPeyrusse commented 1 year ago

It is now obvious to me that I will not find time to work on this in the coming special. My personal situation is special but has a stable rhythm, and that rhythm does not allow me enough time to work on this issue. While I am still very interested, I prefer play the card of honesty and say that I cannot commit on work for now. When there is a change in my schedule, I will come back and see if help is still needed :smile:

marcphilipp commented 1 year ago

@OPeyrusse No worries and thanks for letting us know!

Is anyone else interested in picking it up in the meantime?

shans96 commented 1 year ago

Hi, keen to try resolving this issue. Am I right in saying that this functionality should be configured via ConsoleLauncher?

marcphilipp commented 1 year ago

@shans96 It should be opt-in via a new configuration parameter so it would work from the ConsoleLauncher but also other use cases such as IDEs and build tools.

Vampire commented 1 year ago

Please also have in mind, that this is not something Jupiter specific. I right now have a similar / related problem. I'm using Spock (another JUnit platform engine) to run some tests. These tests (or rather one component of the SUT) heavily use CompletableFutures and join() on them to then verify the result. This means while the parallelism is set to 6, basically all tests are run in parallel as all tests quickly get to a join() which causes further tests to be started. This frys the CPU and makes the whole computer unusable. For some reason setting the maxPoolSize does not really help. It caused less tests to run in parallel, but it also seems to just lead to some deadlock or at least I was too impatient to wait for the test to continue.

With the solution suggested here above, running the actual test on a different thread pool with a maximum thread amount of parallelism, it works much better. Without the maximum thread amount you get the exact same problem again if you use various resource locks. I simulated it using a Spock iteration interceptor that does the actual test execution on a different thread and waits for it to finish without the FJP starting additional threads. With that it works as expected.

From a user perspective, this default behavior was highly confusing and non-obvious, and it will hit anyone who is testing something using ForkJoinPool.managedBlock transitively like CompletableFuture#join or CompletableFuture#get or Phaser or whatever. So maybe it could be reconsidered whether the new behavior should become the default, at least if the additional overhead is not too significant.

asardaes commented 1 year ago

@OPeyrusse @shans96 are you certain the changes mentioned above won't help? (They will soon also apply to the dynamic strategy, see #3206) I did some experiments and it seems to me that if you set the maximum pool size equal to parallelism and allow saturation, the fork-join pool will not compensate. I'm not sure right now if it matters, but I set minRunnable=0 when instantiating the pool. This of course assumes Java 9+.

OPeyrusse commented 1 year ago

Hello @asardaes, yes I am absolutely sure that the solution of #3206 does not help because there is no compensation mechanism nor creation of new thread nor anything like that at play. If you run my reproducing project and look at the stacktrace, or just look again at the output in the OP, you will see that a single thread is starting many tests without completing anyone. That's because new tests are started when calling ForkJoinTask#get or ForkJoinTask#join inside a test. The same thread is beginning many tests. So no, limiting the number of threads will not solve this problem.

It is also worth noting that adding a mechanism that would limit the number of tests running simultaneously - using something like a Semaphore - would not work. It would create a deadlock because the thread would be block at some point and would have to unroll the stack to continue working on the tests already started. (A drawing would really help here but I don't have right now the tools to do it. I will try later) To me, this is a limitation of the ForkJoin framework, not a flaw. One must have it in mind when using this framewok.

So, again, to answer your post, no, it won't help

Vampire commented 1 year ago

I can also confirm again like I just did in my last comment. Setting the max pool size and saturation does not work. The only way I could mitigate was to make sure the actual test execution is run in a separate thread pool that is not a ForkJoinPool and that has the max threads set to parallelism, so that the test scheduling can use the work-stealing, but the test execution is not.

In my Spock-using project I have a Spock extension that creates a thread pool using threadPool = Executors.newFixedThreadPool(runnerConfiguration.parallel.parallelExecutionConfiguration.parallelism) and adds an iteration interceptor that does

spec.allFeatures*.addIterationInterceptor {
    threadPool.submit(it::proceed).get()
}

and with that it behaves as expected, effectively doing the solution suggested here above.

You can most probably do something similar for Jupiter tests. Looking through the Jupiter docs I found https://junit.org/junit5/docs/current/user-guide/#extensions-intercepting-invocations where it is demonstrated how to do the actual test execution in the Swing EDT. This is exactly what you also need here for the work-around I used in Spock, so probably something like

@Override
public void interceptTestMethod(Invocation<Void> invocation,
        ReflectiveInvocationContext<Method> invocationContext,
        ExtensionContext extensionContext) throws Throwable {
    threadPool.submit(invocation::proceed).get();
}
shans96 commented 1 year ago

Just adding to the above, my current understanding of the problem is the same as @OPeyrusse and @Vampire. The saturate parameter makes it possible to have a ForkJoinPool run with less threads than the target number of runnable threads, but the problem here is that multiple tests are being executed without awaiting completion of others due to ForkJoinPool being written in a way that allows it to detect ongoing work in other instances of ForkJoinPool or ForkJoinWorkerThread. So you could set a parallelism of 2 in your JUnit configuration, allow saturation of JUnit's ForkJoinPoolExecutor via some predicate, and set minRunnable to 0, but once your tests execute, more tests will start being executed on the same thread because details of work are being leaked between the ForkJoinPools of your logic and JUnit's executor.

My current idea is to take inspiration from #2805 to create an executor which runs tests in a fixed thread pool, and that should allow code using ForkJoinPool to be tested in parallel without causing scheduling issues.

Vampire commented 1 year ago

Just to clarify again. In my tests no other ForkJoinPool is involved. Just CompletableFutures that behave differently if waited for on a fork-join thread, and also the locks for exclusive resources.

asardaes commented 1 year ago

To clarify, I think it's fine to implement a different approach if possible (in fact, I've experimented a bit with that as well), I just thought it would be good to avoid it if possible. I've looked through all the comments and mentioned code, but maybe there's more to the other arguments in the ForkJoinPool constructor. Here's an experiment I did (in Kotlin, I hope that's ok):

class SleepingAction(private val sleepMs: Long) : RecursiveAction() {
    override fun compute() {
        println("${Instant.now()} - sleeping $sleepMs ms in thread ${Thread.currentThread().id}")
        Thread.sleep(sleepMs)
        println("${Instant.now()} - woke up after $sleepMs ms in thread ${Thread.currentThread().id}")
    }
}

class SecondPoolAction(private val secondPool: ForkJoinPool) : RecursiveAction() {
    override fun compute() {
        secondPool.submit(object : RecursiveAction() {
            override fun compute() {
                try {
                    val sleepingActions = listOf(
                        SleepingAction(1_000L),
                        SleepingAction(2_000L),
                    )
                    println("${Instant.now()} - nested forking in thread ${Thread.currentThread().id}")
                    sleepingActions.forEach { it.fork() }
                    sleepingActions.reversed().forEach { it.join() }
                    println("${Instant.now()} - nested done in thread ${Thread.currentThread().id}")
                } finally {
                    secondPool.shutdownNow()
                }
            }
        }).get()
    }
}

fun main() {
    val pool = ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false, 0, 3, 0, { true }, 10L, TimeUnit.SECONDS)
    val secondPool = ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false, 0, 2, 0, { true }, 10L, TimeUnit.SECONDS)
    try {
        pool.invoke(object : RecursiveAction() {
            override fun compute() {
                val sleepingActions = listOf(
                    SecondPoolAction(secondPool),
                    SleepingAction(1_500L),
                    SleepingAction(4_000L),
                )
                println("${Instant.now()} - forking in thread ${Thread.currentThread().id}")
                sleepingActions.forEach { it.fork() }
                sleepingActions.reversed().forEach { it.join() }
                println("${Instant.now()} - done in thread ${Thread.currentThread().id}")
            }
        })
    } finally {
        pool.shutdownNow()
    }
}

And one potential outcome:

2023-05-25T17:58:21.392305940Z - forking in thread 16
2023-05-25T17:58:21.402393533Z - nested forking in thread 18
2023-05-25T17:58:21.403428288Z - sleeping 1000 ms in thread 19
2023-05-25T17:58:21.402992225Z - sleeping 4000 ms in thread 16
2023-05-25T17:58:21.403326525Z - sleeping 2000 ms in thread 18
2023-05-25T17:58:22.407123722Z - woke up after 1000 ms in thread 19
2023-05-25T17:58:23.407246362Z - woke up after 2000 ms in thread 18
2023-05-25T17:58:23.407451367Z - nested done in thread 18
2023-05-25T17:58:23.408582931Z - sleeping 1500 ms in thread 17 // <--------------------------
2023-05-25T17:58:24.908845221Z - woke up after 1500 ms in thread 17
2023-05-25T17:58:25.407028902Z - woke up after 4000 ms in thread 16
2023-05-25T17:58:25.407199927Z - done in thread 16

Note that the line I point to doesn't start immediately after the second pool's future's get is called.

From what I see, maximumPoolSize could even be larger, the critical thing seems to be minimumRunnable - setting it to 0 or 1 behaves as above, but anything larger than that causes the same problem discussed in this issue.

Vampire commented 1 year ago

I tried with removing my work-around and setting minimumRunnable to 0. It behaved "different", but still not as expected. The amount of tests increased slower, but still crept up to 16 or so with a parallelism of 6. And at some point everything was hanging and nothing continued.

asardaes commented 1 year ago

Oh ok, thanks for trying it, I didn't try anything more realistic so far.

asardaes commented 1 year ago

I know @shans96 also started looking at a fix for this, but I figured I can tell all of you about the PoC I've been experimenting with; you can see the commit linked above.

I wrote a couple of TODOs there, but they're both related to my understanding of CONCURRENT execution mode semantics. The current implementation checks execution mode in both submit and forkConcurrentTasks + executeNonConcurrentTasks, but in both scenarios, if the task is not concurrent, it executes it in the current thread, and that doesn't know if some other thread in the pool has stolen work and is executing another test, so is it really guaranteeing that no other test is running at the same time?

I added a method ExclusiveTask#await and use it instead of join in joinConcurrentTasksInReverseOrderToEnableWorkStealing - if the task should execute in a ForkJoinWorkerThread, await delegates to join, otherwise it delegates to compute. If I don't do it like this, the tests execute fine and are even show as PASSED in the terminal, but after that everything stalls and all threads in the fork-join pool remain parked, and I cannot figure out why. I tried to reproduce this outside of a JUnit 5 session and I couldn't.

Vampire commented 1 year ago

Doesn't it simply work like suggested above:

Yet another solution could be to use a ForkJoinPool for scheduling of the tests and a standard executor for the execution of the tests.

Which is exactly what I did in my described work-around, creating a fixed thread pool with parallelism size and doing the actual test execution on there? So far this works perfectly fine here.

asardaes commented 1 year ago

@Vampire yes, but I'm not sure I understand all the different scenarios where submit or invokeAll are used, so executing a non-concurrent task in the "current thread" can be tricky if there are 2 pools. Technically, my questions are also valid for the existing version to some extent.

Vampire commented 1 year ago

I'm pretty sure I'm just too naive. :-D I hoped it would more or less be done with executing the Node#around action in said extra thread pool or something like that. :-D

mufasa1 commented 1 year ago

@OPeyrusse Hi, any updates for this issue? Looking forward for your good news. I've been struggling about three weeks for this issue...

OPeyrusse commented 1 year ago

@OPeyrusse Hi, any updates for this issue? Looking forward for your good news. I've been struggling about three weeks for this issue...

Hello @mufasa1 , as stated in this previous comment, I stopped working on this isssue and since then, @shans96 offered to take it and already tried some fixes. I will let them comment on their progress :smiley:

mufasa1 commented 1 year ago

@OPeyrusse Thanks for your reminder. @shans96 Hi, is there any updates for this issue?

shans96 commented 1 year ago

Hi, apologies for the delay in reply on this one. I have been looking into this but could use some guidance; as a starting point I've been trying to find ways of writing some unit tests inside ParallelExecutionIntegrationTests to confirm that the new executor will work. However, I haven't been able to fully figure out how the integration tests work- i.e., how to call them with a specific executor. I think if I'm able to do that, it should be a lot easier to iterate on building a new test executor, so any advice would be welcome.

That said, I think the commit that @asardaes linked is further ahead than what I've done so far. Not sure if they're still working on it?

asardaes commented 1 year ago

Mm I wouldn't mind submitting a PR myself, but I also need some guidance from the maintainers with the questions I mentioned above. I suppose that could be discussed in the PR?

mufasa1 commented 1 year ago

@shans96 Hi, got this info from document and may I ask if it is possible to limt the maximum number of threads via jdk 8?or it can only be implemented in jdk9+. image

shans96 commented 1 year ago

Hi @mufasa1, I don't think the JDK 8 version of ForkJoinPool supports limiting the maximum number of threads, in JDK 9+ there's a constructor which takes a parameter maximumPoolSize, which does what you're looking for. ForkJoinPool in JDK 8 doesn't have the same one.

igogoka commented 9 months ago

I can also confirm again like I just did in my last comment. Setting the max pool size and saturation does not work. The only way I could mitigate was to make sure the actual test execution is run in a separate thread pool that is not a ForkJoinPool and that has the max threads set to parallelism, so that the test scheduling can use the work-stealing, but the test execution is not.

In my Spock-using project I have a Spock extension that creates a thread pool using threadPool = Executors.newFixedThreadPool(runnerConfiguration.parallel.parallelExecutionConfiguration.parallelism) and adds an iteration interceptor that does

spec.allFeatures*.addIterationInterceptor {
    threadPool.submit(it::proceed).get()
}

and with that it behaves as expected, effectively doing the solution suggested here above.

You can most probably do something similar for Jupiter tests. Looking through the Jupiter docs I found https://junit.org/junit5/docs/current/user-guide/#extensions-intercepting-invocations where it is demonstrated how to do the actual test execution in the Swing EDT. This is exactly what you also need here for the work-around I used in Spock, so probably something like

@Override
public void interceptTestMethod(Invocation<Void> invocation,
        ReflectiveInvocationContext<Method> invocationContext,
        ExtensionContext extensionContext) throws Throwable {
    threadPool.submit(invocation::proceed).get();
}

Please, write an example how I can set your sollution

Vampire commented 9 months ago

You just quoted the example?

igogoka commented 9 months ago

You just quoted the example? Where I should put this code "threadPool = Executors.newFixedThreadPool(runnerConfiguration.parallel.parallelExecutionConfiguration.parallelism)" to run fixed thread pools

Vampire commented 9 months ago

Wherever you need it. Depends on how you adapt the work-around, for example which test engine you are using. In my Spock project I have it in the start() method of a global extension.

igogoka commented 9 months ago

I use groovyVersion = '3.0.19', spockVersion = '2.4-M1-groovy-3.0', gebVersion = '6.0', seleniumVersion = '4.17.0'. In SpockConfig.groovy : runner { parallel { enabled true fixed 4 } } But "fixed 4" doesn't work. So you override start() method from IGlobalExtension? Can you show how did you that with all imports, please.

igogoka commented 9 months ago

Wherever you need it. Depends on how you adapt the work-around, for example which test engine you are using. In my Spock project I have it in the start() method of a global extension.

Can you show how you Override start() method?

Vampire commented 9 months ago
class FixJunit3108Extension implements IGlobalExtension {
    private static ExecutorService threadPool

    private final RunnerConfiguration runnerConfiguration

    FixJunit3108Extension(RunnerConfiguration runnerConfiguration) {
        this.runnerConfiguration = runnerConfiguration
    }

    @Override
    void start() {
        threadPool = Executors.newFixedThreadPool(runnerConfiguration.parallel.parallelExecutionConfiguration.parallelism)
    }

    @Override
    void visitSpec(SpecInfo spec) {
        // work-around for https://github.com/junit-team/junit5/issues/3108
        spec.allFeatures*.addIterationInterceptor {
            threadPool.submit(it::proceed).get()
        }
    }

    @Override
    void stop() {
        threadPool?.shutdown()
    }
}
mufasa1 commented 9 months ago
IGlobalExten

```groovy
class FixJunit3108Extension implements IGlobalExtension {
    private static ExecutorService threadPool

    private final RunnerConfiguration runnerConfiguration

    FixJunit3108Extension(RunnerConfiguration runnerConfiguration) {
        this.runnerConfiguration = runnerConfiguration
    }

    @Override
    void start() {
        threadPool = Executors.newFixedThreadPool(runnerConfiguration.parallel.parallelExecutionConfiguration.parallelism)
    }

    @Override
    void visitSpec(SpecInfo spec) {
        // work-around for https://github.com/junit-team/junit5/issues/3108
        spec.allFeatures*.addIterationInterceptor {
            threadPool.submit(it::proceed).get()
        }
    }

    @Override
    void stop() {
        threadPool?.shutdown()
    }
}

Hi, could you tell me where I can get this interface 'IGlobalExtension'? Is it in junit 5 anther issue branch?

Vampire commented 9 months ago

This is for Spock. I described above how you probably can do the same for Jupiter.