Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to run some tests isolated #2142

Closed
pbludov opened this issue Jan 3, 2020 · 12 comments · Fixed by #2382
Closed

Allow to run some tests isolated #2142

pbludov opened this issue Jan 3, 2020 · 12 comments · Fixed by #2382

Comments

@pbludov
Copy link

pbludov commented Jan 3, 2020

We have one test that changes the global state, for example

@Test
void localeTest() {
  Locale.setDefault(Locale.FRENCH);
  assertEquals("Bunjour", getMessage());
}

and hundreds of tests that reads the state:

@Test
void someTest() {
  assertEquals("Hello", getMessage());
}

After enabling parallel test execution our tests start to fail at random locations with messages like that:
[ERROR] message 0 expected:<...Annotation.java:17: [Annotation 'AnnotationAnnotation' have incorrect indentation level 2, expected level should be 0].> but was:<...Annotation.java:17: [Die Annotation 'AnnotationAnnotation' hat eine unerwartete Einrückungstiefe von 2 (erwartet: 0)].>

@Execution(ExecutionMode.SAME_THREAD) for the test localeTest won't help us, since the all other tests are able to run concurrent with every other test and with localeTest too.

This can be solved with @ResourceLock:

@Test
@Execution(ExecutionMode.SAME_THREAD)
@ResourceLock(value="global", mode=READ_WRITE)
void localeTest() {
  Locale.setDefault(Locale.FRENCH);
  assertEquals("Bunjour", getMessage());
}

@Test
@Execution(ExecutionMode.CONCURRENT)
@ResourceLock(value="global", mode=READ)
void test1() {
}

// ...

@Test
@Execution(ExecutionMode.CONCURRENT)
@ResourceLock(value="global", mode=READ)
void test100500() {
}

but this is error prone, since adding a new Test without this annotation will produce a flaky tests which may fail.

It will be much better to mark some tests as "incompatible with parallel execution at all":

@Test
@Execution(ExecutionMode.ALONE) // no other this will be executed in parallel 
void localeTest() {
 Locale.setDefault(Locale.FRENCH);
 assertEquals("Bunjour", getMessage());
}

As a workaround, it is possible to turn such a test into Junit 4 test, which will be executed in other universe, isolated from all other.

@marcphilipp
Copy link
Member

Reminds me of the discussion we had in #1634 (comment) and the following comments.

Do you have to set the Locale globally instead of passing it as a parameter to your code under test?

@pbludov
Copy link
Author

pbludov commented Jan 3, 2020

Do you have to set the Locale globally instead of passing it as a parameter to your code under test?

It is actually stored in a static variable. Yes, the code can be modified to solve this particular issue. But we have many similar cases, for example tests for the code that uses a class loader.

Reminds me of the discussion we had in #1634 (comment) and the following comments.

This is exactly what we are looking for. A way to run some tests that can change the environment separately from all tests that have read-only access to the environment.

@pauldingemans
Copy link

@pbludov What setting do you use for parallel execution of your top level classes? Do they run in parallel as well or sequentially? When they run sequentially, you could consider to move the test which alter the system state to a separate top level class in which you execute all methods sequentially.

Another approach to this problem could be that all classes and tests annotated with ExecutionMode.SAME_THREAD are executed and completed before tests marked with ExecutionMode.CONCURRENT will start.

@pbludov
Copy link
Author

pbludov commented Mar 1, 2020

The config is

          <properties>
            <!-- execute top-level classes in parallel but methods in same thread -->
            <configurationParameters>
              junit.jupiter.execution.parallel.enabled = true
              junit.jupiter.execution.parallel.mode.classes.default = concurrent
              junit.jupiter.execution.parallel.mode.default = same_thread
            </configurationParameters>
          </properties>

The behavior I'm currently observing:

  • all tests annotated ExecutionMode.SAME_THREAD are executed sequentially;
  • all tests without explicit annotations are executed simultaneously with tests from the ExecutionMode.SAME_THREAD set, but in parallel threads.

Did I understand correctly that if I explicitly mark all tests as either ExecutionMode.SAME_THREAD or ExecutionMode.CONCURRENT, then no other tests will be run simultaneously with the tests from the ExecutionMode.SAME_THREAD set?

@pauldingemans
Copy link

Did I understand correctly that if I explicitly mark all tests as either ExecutionMode.SAME_THREAD or ExecutionMode.CONCURRENT, then no other tests will be run simultaneously with the tests from the ExecutionMode.SAME_THREAD set?

Currently not. I didn't make clear enough that this might be a possible way to implement this feature in JUnit instead of using ResourceLocks.

@vlsi
Copy link
Contributor

vlsi commented Mar 8, 2020

It looks like we need this in Apache JMeter as well.
One of the tests alters TimeZone.setDefault.
Note: the nature of the test is to ensure that the test would use default timezone for date calculations, so we can't really pass it as a parameter.

Is there a way to understand the effective locks used for the test?

For instance, what if I add an interface like the following

@ResourceLock(value = Resources.LOCALE, mode = ResourceAccessMode.READ)
@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ)
interface CommonReadLocks {
}

then I make the sensitive tests to implement that interface

and then add the relevant @ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ_WRITE) annotations to the test methods that do modify time zone.

Would that work?

I would expect that method-level annotations would override "inherited ones". So the engine should probably be able to schedule methods as appropriate.

The sad thing is it is not clear how to cross-check if annotations work as expected.

@marcphilipp
Copy link
Member

Is there a way to understand the effective locks used for the test?

We could log them or send report entries for them. Would that help you?

I would expect that method-level annotations would override "inherited ones". So the engine should probably be able to schedule methods as appropriate.

In order to avoid deadlocks, locks are currently pulled up to the first node in the test tree that declares a lock. In your example that would mean that all implementations of CommonReadLocks would be able to run in parallel unless they contain a method that uses mode READ_WRITE in which case the whole class would use READ_WRITE. There's a related open issue to make that smarter: #2038.

@nipafx
Copy link
Contributor

nipafx commented Aug 3, 2020

Over at JUnitPioneer (@vlsi, @pbludov: you may want to check out our @DefaultTimeZone and @DefaultLocale annotations 😊), we ran into a problem where we configure a stricter security manager for one of our tests and it leads to random problems in other tests. In this comment I wrote:

  1. There seems to be no simple way to express that, despite all the parallelism that was enabled for the other tests, the security-manager tests should be run on their own. The only solution seems to be resource locks that need to be applied to all tests.

  2. The security manager tests behave similarly to the many extensions we have that change global state (e.g. default locale) and arguably, the same problem can arise there: While we can use resource locks to keep tests using our extensions from treading on one another, we can't prevent "unextended" tests from running concurrently to extended ones. The user would have to apply respective annotations. Which sucks.

Right now it feels like this is a serious problem for all extensions that touch global state that unextended tests may also use: There is no way to run these concurrently without adding annotations to tests that are seemingly unrelated to the extended ones. Worse, it may be very hard to even find the ones that need to be forced into single threads.

I wonder whether an ExecutionMode like SingleThreaded would make sense.

While we arrived there independently of one another, our problem and analyses are identical and @pbludov suggested the same solution. I think this is worth looking into.

@marcphilipp
Copy link
Member

@leonard84 had a similar use case recently, right?

@leonard84
Copy link
Collaborator

Yes, I'm currently adding parallel support to Spock and we have an extension @ConfineMetaClassChanges for tests that manipulate one or more meta classes in groovy, this is by its nature a global change potentially affecting all other tests. The extension automatically sets the READ_WRITE lock for the MetaClassRegistry, but every other test would also need to acquire a READ lock for the same resource for it to be effective.

So a way to mark a test/class to run in a global exclusive mode would be a nice solution for this kind of problems.

A possible implementation would be that each test acquires either a READ or READ_WRITE lock to a special ___global___ resource, this is always done as the first lock. If nothing else is set only a READ lock is acquired, for exclusive tests you switch it to READ_WRITE mode. We would need to change the deadlock avoidance algorithm a bit to treat the global lock in a special way so as not to cause all tests to run in SAME_THREAD mode.

@nipafx
Copy link
Contributor

nipafx commented Aug 4, 2020

I'd really like to see an official solution, so maybe I'm shooting myself in the foot, but there's a fairly straightforward way for us to solve this without onboard support:

  • create our own @Test annotation and search/replace all imports
  • meta-annotate with READ access to custom resource lock, e.g. "SecurityManager", "MetaClassChanges ", etc.
  • request READ_WRITE access to that lock in respective tests

nipafx pushed a commit to junit-pioneer/junit-pioneer that referenced this issue Aug 5, 2020
It must be safe to use Pioneer's extensions in a test suite that is
executed in parallel. This change enables that for all extensions
except TempDirectory (see #227). It does that in various ways.

# Resource locks

For extensions touching global state (like default locales or
environment variables), we've chosen the following approach:

* the extension acquires a read/write lock to the global resource
  (this prevents extended tests from running in parallel)
* we offer a `@Writes...` annotation that does the same thing, so
  users can annotate their tests that write to the same resource and
  prevent them from running in parallel with each other and with
  extended tests
* we offer a `@Reads...` annotation that acquires read access to the
  same lock, so users can make sure such tests do not run in parallel
  with tests that write to the same resource (they can run in
  parallel with one another, though)

# Verify, then modify

Most extensions verify their configuration at some point. It helps
with writing parallel tests for them if they do not change global
state until the configuration is verified. That particularly applies
to "store in beforeEach - restore in afterEach"-extensions! If they
fail after "store", they will still "restore" and thus potentially
create a race condition with other tests.

# Testing

To have a better chance to discover threading-related problems in our
extensions, we parallelize our own tests (configured in
`junit-platform.properties`). Ideally, we'd like to run them in
parallel _across_ and _within_ top-level classes, but unfortunately,
this leads to problems[1] when some test setups change global state
(like the security manager) that other tests rely on. As we see it,
the solution would be to force such tests onto a single thread, but
Jupiter has no such feature, yet[2]. While a homegrown solution is
possible[3], we wait for the discussion to resolve. We hence do not
parallelize across top-level classes - just within.

Closes: #131
Relates to #274
PR: #253

[1]: #253 (comment)
[2]: junit-team/junit5#2142
[3]: junit-team/junit5#2142 (comment)
@marcphilipp marcphilipp added this to the 5.7 M2 milestone Aug 13, 2020
@marcphilipp
Copy link
Member

@leonard84 and I started working on an implementation today. We added an @Isolated annotation to Jupiter which causes the outermost test class to be run in isolation even if the annotation is only used on a test method. That was necessary in order to avoid deadlocks since upgrading from a read lock to a write is not supported by the underlying ReentrantReadWriteLock implementation.

marcphilipp added a commit that referenced this issue Aug 15, 2020
This commit introduces a new global resource lock that all test
descriptors that are direct children of the engine descriptor acquire
by default in READ mode. Using the `@Isolated` annotation in the
Jupiter API causes the mode to be changed to READ_WRITE.

Resolves #2142.

Co-authored-by: Leonard Brünings <lord_damokles@gmx.net>
GrigoriyBeziuk pushed a commit to GrigoriyBeziuk/junit-pioneer that referenced this issue Jan 31, 2024
It must be safe to use Pioneer's extensions in a test suite that is
executed in parallel. This change enables that for all extensions
except TempDirectory (see #227). It does that in various ways.

# Resource locks

For extensions touching global state (like default locales or
environment variables), we've chosen the following approach:

* the extension acquires a read/write lock to the global resource
  (this prevents extended tests from running in parallel)
* we offer a `@Writes...` annotation that does the same thing, so
  users can annotate their tests that write to the same resource and
  prevent them from running in parallel with each other and with
  extended tests
* we offer a `@Reads...` annotation that acquires read access to the
  same lock, so users can make sure such tests do not run in parallel
  with tests that write to the same resource (they can run in
  parallel with one another, though)

# Verify, then modify

Most extensions verify their configuration at some point. It helps
with writing parallel tests for them if they do not change global
state until the configuration is verified. That particularly applies
to "store in beforeEach - restore in afterEach"-extensions! If they
fail after "store", they will still "restore" and thus potentially
create a race condition with other tests.

# Testing

To have a better chance to discover threading-related problems in our
extensions, we parallelize our own tests (configured in
`junit-platform.properties`). Ideally, we'd like to run them in
parallel _across_ and _within_ top-level classes, but unfortunately,
this leads to problems[1] when some test setups change global state
(like the security manager) that other tests rely on. As we see it,
the solution would be to force such tests onto a single thread, but
Jupiter has no such feature, yet[2]. While a homegrown solution is
possible[3], we wait for the discussion to resolve. We hence do not
parallelize across top-level classes - just within.

Closes: #131
Relates to #274
PR: #253

[1]: junit-pioneer/junit-pioneer#253 (comment)
[2]: junit-team/junit5#2142
[3]: junit-team/junit5#2142 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants