sbt test execution
How to tame sbt with OOM while running tests
Introduction
sbt
is one of the official built tool for Scala
. An attempt to setup running large number of tests on sbt
can cause java.lang.OutOfMemoryError
.
JVMDUMP039I Processing dump event "systhrow", detail "java/lang/OutOfMemoryError" at 2019/09/16 13:26:33 - please wait.
JVMDUMP032I JVM requested System dump using '/path/to/app/core.20190916.132633.8122.0001.dmp' in response to an event
JVMDUMP010I System dump written to /path/to/app/core.20190916.132633.8122.0001.dmp
JVMDUMP032I JVM requested Heap dump using '/path/to/app/heapdump.20190916.132633.8122.0002.phd' in response to an event
JVMDUMP010I Heap dump written to /path/to/app/heapdump.20190916.132633.8122.0002.phd
JVMDUMP032I JVM requested Java dump using '/path/to/app/javacore.20190916.132633.8122.0003.txt' in response to an event
JVMDUMP010I Java dump written to /path/to/app/javacore.20190916.132633.8122.0003.txt
I did run into OutofMemory issue when I was testing akka actor-typed
using akka.actor.testkit.typed
.
This is due to multiple reasons.
- Parallel Execution
- Forking
- Grouping
Let’s try to understand each of this in better detail.
Parallel Execution
Parallel Execution enables sbt
to run multiple tests simultaneously. This is generally a good default choice. But, there may be times when the unit tests are pretty heavy to run. During these cases, disabling this is a better choice.
Test / parallelExecution := false
Forking
By default, the
run
task runs in the same JVM as sbt. Forking is required under certain circumstances, however. Or, you might want to fork Java processes when implementing new tasks. (Source SBT documentation)
During the case when tests are pretty heavy, its best to run this in fork
mode.
Test / fork := true
Grouping
Control over how tests are assigned to JVMs and what options to pass to those is available with
testGrouping
key
Grouping enables a way to size how much tests run in a single JVM. With forking
enabled, all the tests run into a single forked JVM
sequentially. This is reasonable. But, we would also like to say that every Test should spin as a brand new JVM.
Test / testGrouping := (definedTests in Test).value map { test =>
val options = ForkOptions().withRunJVMOptions(Vector.empty)
Tests.Group(
name = test.name,
tests = Seq(test),
runPolicy = Tests.SubProcess(options)
)
}