How to assign environment variable such that no test running at the same time has the same value?

Confusing question…but hear me out, please.

I have JUnit tests that unfortunately rely on the focus of the Swing window that is created. I am using Xvfb and I start it with a DISPLAY variable (example: “:70”). I set the DISPLAY variable in gradle for the test to test { environment 'DISPLAY', ':70' }

This all is fine if I have one instance of Xvfb running and the JUnit tests are running serially. The tests all share the same Xvfb instance and DISPLAY values and none steal each other’s focus as they are run serially.

The problem I have now is: if I have two Xvfb servers set to DISPLAY “:70” and “:80” , respectively, and I have maxParallelForks set to 2, how do I give each test a DISPLAY value such that no test with the same value are run at the same time? Is it possible for a Fork to run all its tests with a fixed DISPLAY value?

I was thinking that I could do this by extending the Test task and then have the copyTo method check a lock file for an available DISPLAY variable. If one is available, assign that to the test environment, otherwise wait for a test to run and write back to the lock file. Something like that…a synchronized way of locking the values across Test Executors.

Sounds ugly though and I was hoping for better ideas.

I would try to do this at the test level.

Here’s an idea for using JUnit Rules to set environment variables:

So you could go about this a few ways:

  • Gradle starts up N-instances of Xvfb and the test code knows how to acquire/reserve/release each instance. The “start Xvfb” task could be run manually for people to test from their IDE. After running the tests, Gradle can stop the Xvfb instances that were started.
  • The tests do similar to above (look for an instance of Xvfb), but if it cannot find one, the test will start Xvfb. The test can then tear it down at the end.

There are other variations of the above that allow you to start/stop Xvfb’s to make it easier to run tests in an ad-hoc manner.

Thanks for your response.
Is there a way to have gradle handle the acquire/reserve/release part? All that really means is to do the “bookkeeping” for the value of the DISPLAY environment.

Is there a way for an individual test to get assigned a DISPLAY value based on which Fork (I believe it is called a Test Executor) it will run on? This will ensure no test with the same DISPLAY value would be run at the same time.

Not at the moment. Gradle treats test workers as identical, so there’s no coordination point for that right now.

Here’s a similar question / answer on stack overflow

That is helpful. I tried using beforeSuite and I see how the Test task’s copyTo method is called beforehand. So the environment variables are all setup for the Test Executor prior to any DSL hooks such as beforeSuite.

Ok, well thank you for the replies/ideas and at least I now have ruled a few things out.
:fist_right: :sparkles: :fist_left:

Hello,
It looks like for Gradle 4.7 the copyTo for a Task is called only once.
This is messing up my workaround. I had a unique DISPLAY setting assigned per JavaForkOptions in the Task copyTo method.
It looks like the code paths have changed a bit.

@Lance Do you have any other ideas?

The call stack for 4.2 is on top and 4.7 is on the bottom:

    at MyTestTask.copyTo
    at org.gradle.api.internal.tasks.testing.worker.ForkingTestClassProcessor.forkProcess(ForkingTestClassProcessor.java:80)
    at org.gradle.api.internal.tasks.testing.worker.ForkingTestClassProcessor.processTestClass(ForkingTestClassProcessor.java:69)
    at org.gradle.api.internal.tasks.testing.processors.RestartEveryNTestClassProcessor.processTestClass(RestartEveryNTestClassProcessor.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.FailureHandlingDispatch.dispatch(FailureHandlingDispatch.java:29)
    at org.gradle.internal.dispatch.AsyncDispatch.dispatchMessages(AsyncDispatch.java:133)
    at org.gradle.internal.dispatch.AsyncDispatch.access$000(AsyncDispatch.java:34)
    at org.gradle.internal.dispatch.AsyncDispatch$1.run(AsyncDispatch.java:73)
    at org.gradle.internal.operations.BuildOperationIdentifierPreservingRunnable.run(BuildOperationIdentifierPreservingRunnable.java:39)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
    at java.lang.Thread.run(Thread.java:748)

4.7 call stack

    at MyTestTask.copyTo
    at org.gradle.api.tasks.testing.Test.createTestExecutionSpec(Test.java:553)
    at org.gradle.api.tasks.testing.Test.createTestExecutionSpec(Test.java:136)
    at org.gradle.api.tasks.testing.AbstractTestTask.executeTests(AbstractTestTask.java:434)
    at org.gradle.api.tasks.testing.Test.executeTests(Test.java:583)
    at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:46)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
    at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:794)
    at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:761)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:124)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:97)
    at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:113)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:95)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:73)
    at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
    at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:101)
    at org.gradle.api.internal.tasks.execution.FinalizeInputFilePropertiesTaskExecuter.execute(FinalizeInputFilePropertiesTaskExecuter.java:44)
    at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:91)
    at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:62)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
    at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:256)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:317)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:309)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:185)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:97)
    at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:249)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:238)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:663)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:596)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)

Perhaps you haven’t called test.forkEvery so there’s only one fork for all tests?

I am calling “forkEvery 1” as I did before. The only difference is gradle 4.2 vs 4.7.

I know a new JVM is being started for each JUnit test because I set the remote debug options and I see:
“Listening for transport dt_socket at address: …”
(in this case the port is 0, so the kernel chooses)

I can come up with a toy example that sets a unique DISPLAY value (it really can be any system property) per Test Executor/ test JVM if you think that would help.

Extending the Test class and implementing copyTo was always a hack and I’m guessing a change in Gradle internals has stopped the hack from working somewhere between 4.2 and 4.7

The “proper” fix is to add hooks in Gradle to support customising the fork. You could raise an issue requesting the following methods be added to the Test task.

beforeFork(Action<JavaForkOptions> action)
afterFork(Action<JavaForkOptions> action) 

With these hooks you wouldn’t need to extend the Test class and disable the default test task either

I appreciate the help.
I made a feature request here: https://github.com/gradle/gradle/issues/5304
:vulcan_salute: