Write your first test with Lincheck
This tutorial demonstrates how to write your first Lincheck test, set up the Lincheck framework, and use its basic API. You will create a new IntelliJ IDEA project with an incorrect concurrent counter implementation and write a test for it, finding and analyzing the bug afterward.
Create a project
Open an existing Kotlin project in IntelliJ IDEA or create a new one. When creating a project, use the Gradle build system.
Add required dependencies
Open the
build.gradle(.kts)
file and make sure thatmavenCentral()
is added to the repository list.Add the following dependencies to the Gradle configuration:
repositories { mavenCentral() } dependencies { // Lincheck dependency testImplementation("org.jetbrains.kotlinx:lincheck:2.34") // This dependency allows you to work with kotlin.test and JUnit: testImplementation("junit:junit:4.13") }repositories { mavenCentral() } dependencies { // Lincheck dependency testImplementation "org.jetbrains.kotlinx:lincheck:2.34" // This dependency allows you to work with kotlin.test and JUnit: testImplementation "junit:junit:4.13" }
Write a concurrent counter and run the test
In the
src/test/kotlin
directory, create aBasicCounterTest.kt
file and add the following code with a buggy concurrent counter and a Lincheck test for it:import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.junit.* class Counter { @Volatile private var value = 0 fun inc(): Int = ++value fun get() = value } class BasicCounterTest { private val c = Counter() // Initial state // Operations on the Counter @Operation fun inc() = c.inc() @Operation fun get() = c.get() @Test // JUnit fun stressTest() = StressOptions().check(this::class) // The magic button }This Lincheck test automatically:
Generates several random concurrent scenarios with the specified
inc()
andget()
operations.Performs a lot of invocations for each of the generated scenarios.
Verifies that each invocation result is correct.
Run the test above, and you will see the following error:
= Invalid execution results = | ------------------- | | Thread 1 | Thread 2 | | ------------------- | | inc(): 1 | inc(): 1 | | ------------------- |Here, Lincheck found an execution that violates the counter atomicity – two concurrent increments ended with the same result
1
. It means that one increment has been lost, and the behavior of the counter is incorrect.
Trace the invalid execution
Besides showing invalid execution results, Lincheck can also provide an interleaving that leads to the error. This feature is accessible with the model checking testing strategy, which examines numerous executions with a bounded number of context switches.
To switch the testing strategy, replace the
options
type fromStressOptions()
toModelCheckingOptions()
. The updatedBasicCounterTest
class will look like this:import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.check import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* import org.junit.* class Counter { @Volatile private var value = 0 fun inc(): Int = ++value fun get() = value } class BasicCounterTest { private val c = Counter() @Operation fun inc() = c.inc() @Operation fun get() = c.get() @Test fun modelCheckingTest() = ModelCheckingOptions().check(this::class) }Run the test again. You will get the execution trace that leads to incorrect results:
= Invalid execution results = | ------------------- | | Thread 1 | Thread 2 | | ------------------- | | inc(): 1 | inc(): 1 | | ------------------- | The following interleaving leads to the error: | --------------------------------------------------------------------- | | Thread 1 | Thread 2 | | --------------------------------------------------------------------- | | | inc() | | | inc(): 1 at BasicCounterTest.inc(BasicCounterTest.kt:18) | | | value.READ: 0 at Counter.inc(BasicCounterTest.kt:10) | | | switch | | inc(): 1 | | | | value.WRITE(1) at Counter.inc(BasicCounterTest.kt:10) | | | value.READ: 1 at Counter.inc(BasicCounterTest.kt:10) | | | result: 1 | | --------------------------------------------------------------------- |According to the trace, the following events have occurred:
T2: The second thread starts the
inc()
operation, reading the current counter value (value.READ: 0
) and pausing.T1: The first thread executes
inc()
, which returns1
, and finishes.T2: The second thread resumes and increments the previously obtained counter value, incorrectly updating the counter to
1
.
Test the Java standard library
Let's now find a bug in the standard Java's ConcurrentLinkedDeque
class. The Lincheck test below finds a race between removing and adding an element to the head of the deque:
Run modelCheckingTest()
. The test will fail with the following output: