Напишите свой первый тест с Lincheck
Этот учебник демонстрирует, как написать свой первый тест Lincheck, настроить фреймворк Lincheck и использовать его базовый API. Вы создадите новый проект IntelliJ IDEA с некорректной реализацией конкурентного счётчика и напишете тест для него, после чего найдёте и проанализируете ошибку.
Создание проекта
Откройте существующий проект Kotlin в IntelliJ IDEA или создайте новый. При создании проекта используйте систему сборки Gradle.
В каталоге
src/main/kotlinоткройте файлmain.kt.-
Замените код в
main.ktследующей реализацией счётчика:class Counter { @Volatile private var value = 0 fun inc(): Int = ++value fun get() = value }Ваш тест Lincheck проверит, является ли счётчик потокобезопасным.
Добавление необходимых зависимостей
Откройте файл
build.gradle(.kts)и убедитесь, чтоmavenCentral()добавлен в список репозиториев.-
Добавьте следующие зависимости в конфигурацию Gradle:
repositories { mavenCentral() } dependencies { // Lincheck dependency testImplementation("org.jetbrains.kotlinx:lincheck:2.16") // 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.16" // This dependency allows you to work with kotlin.test and JUnit: testImplementation "junit:junit:4.13" }
Написание и запуск теста
-
В каталоге
src/test/kotlinсоздайте файлBasicCounterTest.ktи добавьте следующий код: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 }Этот тест Lincheck автоматически:
Генерирует несколько случайных конкурентных сценариев с указанными операциями
inc()иdec().Выполняет множество вызовов для каждого из сгенерированных сценариев.
Проверяет, что каждый результат вызова корректен.
-
Запустите тест выше, и вы увидите следующую ошибку:
= Invalid execution results = Parallel part: | inc(): 1 | inc(): 1 |
Здесь Lincheck обнаружил выполнение, нарушающее атомарность счётчика — два одновременных инкремента завершились одним и тем же результатом
1. Это означает, что один инкремент был утерян, и поведение счётчика некорректно.
Отслеживание некорректного выполнения
Помимо отображения результатов некорректного выполнения, Lincheck также может предоставить интерлевинг, который приводит к ошибке. Эта функция доступна с помощью стратегии тестирования моделирования, которая исследует множество выполнений с ограниченным числом переключений контекста.
-
Чтобы переключиться на стратегию тестирования, замените тип
optionsсStressOptions()наModelCheckingOptions(). Обновлённый классBasicCounterTestбудет выглядеть следующим образом:import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.check import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* import org.jetbrains.kotlinx.lincheck.verifier.* 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 getAndInc() = c.getAndInc() @Operation fun get() = c.get() @Test fun modelCheckingTest() = ModelCheckingOptions().check(this::class) } -
Запустите тест снова. Вы получите трассировку выполнения, которая приводит к некорректным результатам:
= Invalid execution results = Parallel part: | inc(): 1 | inc(): 1 | = The following interleaving leads to the error = Parallel part trace: | | inc() | | | inc(): 1 at BasicCounterTest.inc(BasicCounterTest.kt:11) | | | value.READ: 0 at Counter.inc(Counter.kt:5) | | | switch | | inc(): 1 | | | thread is finished | | | | value.WRITE(1) at Counter.inc(Counter.kt:5) | | | value.READ: 1 at Counter.inc(Counter.kt:5) | | | result: 1 | | | thread is finished |
Согласно трассировке, произошли следующие события:
T2: Второй поток начинает операцию
inc(), считывая текущее значение счётчика (value.READ: 0) и приостанавливается.T1: Первый поток выполняет
inc(), что возвращает1, и завершается.T2: Второй поток возобновляется и увеличивает ранее полученное значение счётчика, неправильно обновляя счётчик до
1.
Тестирование стандартной библиотеки Java
Теперь давайте найдём ошибку в стандартном классе Java ConcurrentLinkedDeque. Следующий тест Lincheck обнаруживает гонку между удалением и добавлением элемента в начало очереди:
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)
}
Запустите modelCheckingTest(). Тест завершится ошибкой с выводом:
= Invalid execution results = Init part: [addLast(4): void] Parallel part: | pollFirst(): 4 | addFirst(-4): void | | | peekLast(): 4 [-,1] | --- 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 = Parallel part trace: | pollFirst() | | | pollFirst(): 4 at ConcurrentDequeTest.pollFirst(ConcurrentDequeTest.kt:39) | | | 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: 4 at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:917) | | | prev.READ: null at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:919) | | | switch | | | | addFirst(-4): void | | | peekLast(): 4 | | | thread is finished | | compareAndSet(Node@2,4,null): true at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:920) | | | unlink(Node@2) at ConcurrentLinkedDeque.pollFirst(ConcurrentLinkedDeque.java:921) | | | result: 4 | | | thread is finished | |
Следующий шаг
Выберите стратегию тестирования и настройте выполнение теста.
См. также
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/introduction.html