用 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:
【Kotlin】
repositories {
mavenCentral()
}
dependencies {
// Lincheck dependency
testImplementation("org.jetbrains.kotlinx:lincheck:2.30")
// This dependency allows you to work with kotlin.test and JUnit:
testImplementation("junit:junit:4.13")
}
【Groovy】
repositories {
mavenCentral()
}
dependencies {
// Lincheck dependency
testImplementation "org.jetbrains.kotlinx:lincheck:2.30"
// 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.
- Generates several random concurrent scenarios with the specified
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
.
- T2: The second thread starts the
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:
import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.annotations.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*
import org.junit.*
import java.util.concurrent.*
class ConcurrentDequeTest {
private val deque = ConcurrentLinkedDeque<Int>()
@Operation
fun addFirst(e: Int) = deque.addFirst(e)
@Operation
fun addLast(e: Int) = deque.addLast(e)
@Operation
fun pollFirst() = deque.pollFirst()
@Operation
fun pollLast() = deque.pollLast()
@Operation
fun peekFirst() = deque.peekFirst()
@Operation
fun peekLast() = deque.peekLast()
@Test
fun modelCheckingTest() = ModelCheckingOptions().check(this::class)
}
Run modelCheckingTest()
. The test will fail with the following output:
= Invalid execution results =
| ---------------------------------------- |
| Thread 1 | Thread 2 |
| ---------------------------------------- |
| addLast(22): void | |
| ---------------------------------------- |
| pollFirst(): 22 | addFirst(8): void |
| | peekLast(): 22 [-,1] |
| ---------------------------------------- |
---
All operations above the horizontal line | ----- | happen before those below the line
---
Values in "[..]" brackets indicate the number of completed operations
in each of the parallel threads seen at the beginning of the current operation
---
The following interleaving leads to the error:
| --------------------------------------------------------------------------------------------------------------------------------- |
| Thread 1 | Thread 2 |
| --------------------------------------------------------------------------------------------------------------------------------- |
| pollFirst() | |
| pollFirst(): 22 at ConcurrentDequeTest.pollFirst(ConcurrentDequeTest.kt:17) | |
| first(): Node@1 at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:915) | |
| item.READ: null at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:917) | |
| next.READ: Node@2 at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:925) | |
| item.READ: 22 at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:917) | |
| prev.READ: null at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:919) | |
| switch | |
| | addFirst(8): void |
| | peekLast(): 22 |
| compareAndSet(Node@2,22,null): true at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:920) | |
| unlink(Node@2) at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:921) | |
| result: 22 | |
| --------------------------------------------------------------------------------------------------------------------------------- |
Next step
Choose your testing strategy and configure test execution.