Тестирование на устойчивость и проверка моделей
Lincheck предоставляет две стратегии тестирования: тестирование на устойчивость и проверку моделей. Узнайте, что происходит внутри обеих стратегий тестирования, используя пример Counter из предыдущего шага:
class Counter {
@Volatile
private var value = 0
fun inc(): Int = ++value
fun get() = value
}
Тестирование на устойчивость
Написание теста на устойчивость
Создайте конкурирующий тест на устойчивость для Counter, следуя этим шагам:
Создайте класс
CounterTest.В этом классе добавьте поле
cтипаCounter, создав экземпляр в конструкторе.Перечислите операции счётчика и пометьте их аннотацией
@Operation, делегировав их реализациюc.Укажите стратегию тестирования на устойчивость, используя
StressOptions().Вызовите функцию
StressOptions.check()для запуска теста.
Получившийся код будет выглядеть так:
import org.jetbrains.kotlinx.lincheck.annotations.*
import org.jetbrains.kotlinx.lincheck.check
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 CounterTest {
private val c = Counter() // Initial state
// Operations on the Counter
@Operation
fun inc() = c.inc()
@Operation
fun get() = c.get()
@Test // Run the test
fun stressTest() = StressOptions().check(this::class)
}
Как работает тестирование на устойчивость
Сначала Lincheck генерирует набор конкурирующих сценариев, используя операции, помеченные @Operation. Затем он запускает собственные потоки, синхронизируя их в начале, чтобы гарантировать, что операции начинаются одновременно. Наконец, Lincheck выполняет каждый сценарий в этих собственных потоках несколько раз, ожидая столкновения, которые приведут к некорректным результатам.
На рисунке ниже показана общая схема того, как Lincheck может выполнять сгенерированные сценарии:

Проверка моделей
Основная проблема с тестированием на устойчивость заключается в том, что вы можете потратить часы, пытаясь понять, как воспроизвести найденную ошибку. Чтобы помочь вам с этим, Lincheck поддерживает ограниченную проверку моделей, которая автоматически предоставляет переплетение для воспроизведения ошибок.
Тест проверки модели создается так же, как и тест на устойчивость. Просто замените StressOptions(), которые определяют стратегию тестирования, на ModelCheckingOptions().
Написание теста проверки модели
Чтобы изменить стратегию тестирования на устойчивость на проверку моделей, замените StressOptions() на ModelCheckingOptions() в вашем тесте:
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 CounterTest {
private val c = Counter() // Initial state
// Operations on the Counter
@Operation
fun inc() = c.inc()
@Operation
fun get() = c.get()
@Test // Run the test
fun modelCheckingTest() = ModelCheckingOptions().check(this::class)
}
Как работает проверка модели
Большинство ошибок в сложных конкурирующих алгоритмах могут быть воспроизведены с классическими переплетениями, меняя выполнение с одного потока на другой. Кроме того, средства проверки моделей для слабых моделей памяти очень сложны, поэтому Lincheck использует ограниченную проверку моделей в рамках модели памяти последовательной согласованности.
Короче говоря, Lincheck анализирует все переплетения, начиная с одного переключения контекста, затем двух и продолжая процесс до тех пор, пока не будет исследовано заданное число переплетений. Эта стратегия позволяет найти некорректный график с наименьшим возможным количеством переключений контекста, что делает дальнейшее исследование ошибки проще.
Для управления выполнением Lincheck вставляет специальные точки переключения в код тестирования. Эти точки определяют, где может произойти переключение контекста. По сути, это обращения к общей памяти, такие как чтение или обновление полей и элементов массива в JVM, а также wait/notify и park/unpark вызовы.
Поскольку стратегия проверки модели контролирует выполнение, Lincheck может предоставить трассировку, которая приводит к некорректной интерлейвинг, что крайне полезно на практике. Вы можете увидеть пример трассировки некорректного выполнения Counter в руководстве Написание первого теста с Lincheck.
Какая стратегия тестирования лучше?
Стратегия проверки моделей предпочтительнее для поиска ошибок в модели памяти последовательной согласованности, так как она обеспечивает лучшую охватность и предоставляет трассировку выполнения, если ошибка найдена.
Хотя тестирование на устойчивость не гарантирует охватности, проверка алгоритмов на ошибки, введенные низкоуровневыми эффектами, такими как пропущенный volatile модификатор, всё ещё полезна. Тестирование на устойчивость также отлично помогает в обнаружении редких ошибок, для воспроизведения которых требуется много переключений контекста, и их невозможно проанализировать из-за текущих ограничений стратегии проверки моделей.
Настройка стратегии тестирования
Для настройки стратегии тестирования установите параметры в классе <TestingMode>Options.
-
Установите параметры для генерации и выполнения сценариев для
CounterTest:import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.check import org.jetbrains.kotlinx.lincheck.strategy.stress.* import org.jetbrains.kotlinx.lincheck.verifier.* import org.junit.* class Counter { @Volatile private var value = 0 fun inc(): Int = ++value fun get() = value } class CounterTest { private val c = Counter() @Operation fun inc() = c.inc() @Operation fun get() = c.get() @Test fun stressTest() = StressOptions() // Stress testing options: .actorsBefore(2) // Number of operations before the parallel part .threads(2) // Number of threads in the parallel part .actorsPerThread(2) // Number of operations in each thread of the parallel part .actorsAfter(1) // Number of operations after the parallel part .iterations(100) // Generate 100 random concurrent scenarios .invocationsPerIteration(1000) // Run each generated scenario 1000 times .check(this::class) // Run the test } -
Запустите
stressTest()ещё раз, Lincheck сгенерирует сценарии, похожие на сценарий ниже:Init part: [inc(), inc()] Parallel part: | get() | inc() | | inc() | get() | Post part: [inc()]
Здесь две операции до параллельной части, два потока для каждой из двух операций, за которыми следует одна операция в конце.
Вы можете настроить тесты проверки моделей аналогичным образом.
Минимизация сценариев
Вы, возможно, уже заметили, что обнаруженные ошибки обычно представляются сценарием, меньшим, чем указано в настройках теста. Lincheck пытается минимизировать ошибку, активно удаляя операцию, пока это возможно, не нарушая провал теста.
Вот минимизированный сценарий для теста счётчика выше:
= Invalid execution results = Parallel part: | inc(): 1 | inc(): 1 |
Поскольку анализ меньших сценариев проще, минимизация сценариев включена по умолчанию. Чтобы отключить эту функцию, добавьте minimizeFailedScenario(false) в конфигурацию [Stress, ModelChecking]Options.
Ведение журнала состояний структуры данных
Ещё одна полезная функция для отладки — журналирование состояния. При анализе переплетения, приводящего к ошибке, вы обычно рисуете изменения структуры данных на листе бумаги, меняя состояние после каждого события. Чтобы автоматизировать этот процесс, вы можете предоставить специальный метод, который возвращает String представление структуры данных, поэтому Lincheck выводит представление состояния после каждого события в переплетении, которое изменяет структуру данных.
Для этого определите метод, который не принимает аргументов и помечен аннотацией @StateRepresentation. Метод должен быть потокобезопасным, неблокирующим и никогда не изменять структуру данных.
-
В примере
Counter, представлениеString— это просто значение счётчика. Таким образом, чтобы вывести состояния счётчика в трассировке, добавьте функциюstateRepresentation()вCounterTest:import org.jetbrains.kotlinx.lincheck.annotations.* import org.jetbrains.kotlinx.lincheck.check import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.* import org.junit.Test class Counter { @Volatile private var value = 0 fun inc(): Int = ++value fun get() = value } class CounterTest { private val c = Counter() @Operation fun inc() = c.inc() @Operation fun get() = c.get() @StateRepresentation fun stateRepresentation() = c.get().toString() @Test fun modelCheckingTest() = ModelCheckingOptions().check(this::class) } -
Запустите
modelCheckingTest()сейчас и проверьте состоянияCounter, напечатанные в точках переключения, которые изменяют состояние счётчика (они начинаются сSTATE:):= Invalid execution results = STATE: 0 Parallel part: | inc(): 1 | inc(): 1 | STATE: 1 = The following interleaving leads to the error = Parallel part trace: | | inc() | | | inc(): 1 at CounterTest.inc(CounterTest.kt:42) | | | value.READ: 0 at Counter.inc(CounterTest.kt:35) | | | switch | | inc(): 1 | | | STATE: 1 | | | thread is finished | | | | value.WRITE(1) at Counter.inc(CounterTest.kt:35) | | | STATE: 1 | | | value.READ: 1 at Counter.inc(CounterTest.kt:35) | | | result: 1 | | | thread is finished |
В случае тестирования на устойчивость Lincheck выводит представление состояния непосредственно перед и после параллельной части сценария, а также в конце.
Следующий шаг
Узнайте, как настроить аргументы, передаваемые операциям, и когда это может быть полезно.
См. также
Узнайте, как оптимизировать и увеличить охват стратегии проверки моделей с помощью модульного тестирования.
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/testing-strategies.html