Spec-Zone.ru › Kotlin 1.9

Тестирование на стресс и проверка моделей

Lincheck предлагает две стратегии тестирования: тестирование на стресс и проверку моделей. Узнайте, что происходит под капотом обоих подходов, используя Counter , написанные в файле BasicCounterTest.kt в предыдущем шаге:

class Counter {
    @Volatile
    private var value = 0

    fun inc(): Int = ++value
    fun get() = value
}

Тестирование на стресс

Написание теста на стресс

Создайте конкурентный тест на стресс для Counter, следуя этим шагам:

  1. Создайте класс CounterTest.

  2. В этом классе добавьте поле c типа Counter, создав экземпляр в конструкторе.

  3. Перечислите операции счётчика и отметьте их аннотацией @Operation, делегируя их реализацию c.

  4. Укажите стратегию тестирования на стресс с помощью StressOptions().

  5. Вызовите функцию 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 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 может выполнять сгенерированные сценарии:

Stress execution of the Counter

Проверка моделей

Главная проблема, связанная с тестированием на стресс, заключается в том, что вы можете потратить часы на то, чтобы понять, как воспроизвести обнаруженный баг. Для этого 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 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)
}

Для использования стратегии проверки модели для Java 9 и выше, добавьте следующие свойства JVM:

--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
--add-exports java.base/jdk.internal.util=ALL-UNNAMED

Они необходимы, если код тестирования использует классы из пакета java.util, поскольку некоторые из них используют jdk.internal.misc.Unsafe или аналогичные внутренние классы под капотом. Если вы используете Gradle, добавьте следующие строки в build.gradle.kts:

tasks.withType<Test> { jvmArgs( "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED", "--add-exports", "java.base/jdk.internal.util=ALL-UNNAMED", "--add-exports", "java.base/sun.security.action=ALL-UNNAMED" ) }

Как работает проверка моделей

Большинство багов в сложных конкуретных алгоритмах можно воспроизвести с классическими перестановками, переключая выполнение с одного потока на другой. Кроме того, средства проверки моделей для слабых моделей памяти очень сложны, поэтому Lincheck использует ограниченную проверку моделей в рамках модели памяти последовательной согласованности.

Короче говоря, Lincheck анализирует все перестановки, начиная с одного переключения контекста, затем двух, продолжая этот процесс до тех пор, пока не будет проанализировано указанное число переключений. Эта стратегия позволяет найти некорректный график с минимально возможным числом переключений контекста, что упрощает дальнейшее исследование бага.

Для управления выполнением Lincheck вставляет специальные точки переключения в код тестирования. Эти точки определяют, где может быть выполнено переключение контекста. По существу, это обращения к общей памяти, такие как чтение или обновление полей и элементов массивов в JVM, а также wait/notify и park/unpark вызовы.

Поскольку стратегия проверки моделей контролирует выполнение, Lincheck может предоставить трассировку, которая приводит к неверной перестановке, что очень полезно на практике. Вы можете увидеть пример трассировки некорректного выполнения Counter в учебнике Написание вашего первого теста с помощью Lincheck.

Какая стратегия тестирования лучше?

Стратегия проверки моделей предпочтительнее для поиска багов в рамках модели памяти последовательной согласованности, поскольку она обеспечивает лучшую покрываемость и предоставляет трассировку некорректного выполнения, если ошибка найдена.

Хотя тестирование на стресс не гарантирует покрываемость, проверка алгоритмов на наличие багов, введённых низкоуровневыми эффектами, например, пропущенным модификатором volatile, по-прежнему полезна. Тестирование на стресс также очень помогает в обнаружении редких багов, для воспроизведения которых требуются многочисленные переключения контекста, и их невозможно проанализировать из-за текущих ограничений в стратегии проверки моделей.

Настройка стратегии тестирования

Для настройки стратегии тестирования задайте параметры в классе <TestingMode>Options.

  1. Установите параметры для генерации и выполнения сценариев для CounterTest:

    import org.jetbrains.kotlinx.lincheck.annotations.*
    import org.jetbrains.kotlinx.lincheck.check
    import org.jetbrains.kotlinx.lincheck.strategy.stress.*
    import org.junit.*
    
    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
    }
    
  2. Запустите stressTest() снова, Lincheck сгенерирует сценарии, похожие на приведенный ниже:

    | ------------------- |
    | Thread 1 | Thread 2 |
    | ------------------- |
    | inc()    |          |
    | inc()    |          |
    | ------------------- |
    | get()    | inc()    |
    | inc()    | get()    |
    | ------------------- |
    | inc()    |          |
    | ------------------- |
    

    Здесь две операции до параллельной части, два потока для каждой из двух операций, а затем одна операция в конце.

Вы можете настроить тесты проверки моделей аналогичным образом.

Минимизация сценариев

Вы, возможно, уже заметили, что обнаруженные ошибки обычно представлены сценарием, меньшим, чем указанный в конфигурации теста. Lincheck пытается минимизировать ошибку, активно удаляя операцию, пока это возможно, не нарушая работоспособности теста.

Вот минимизированный сценарий для теста счётчика выше:

= Invalid execution results =
| ------------------- |
| Thread 1 | Thread 2 |
| ------------------- |
| inc()    | inc()    |
| ------------------- |

Поскольку анализ более мелких сценариев проще, минимизация сценариев включена по умолчанию. Чтобы отключить эту функцию, добавьте minimizeFailedScenario(false) в конфигурацию [Stress, ModelChecking]Options.

Ведение логов состояний структуры данных

Ещё одна полезная функция для отладки — ведение логов состояний. При анализе перестановки, приводящей к ошибке, вы обычно рисуете изменения структуры данных на листе бумаги, меняя состояние после каждого события. Чтобы автоматизировать эту процедуру, вы можете предоставить специальный метод, возвращающий представление String структуры данных, чтобы Lincheck печатал представление состояния после каждого события в перестановке, изменяющей структуру данных.

Для этого определите метод, который не принимает аргументы и отмечен аннотацией @StateRepresentation. Метод должен быть потокобезопасным, неблокирующим и никогда не изменять структуру данных.

  1. В примере 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 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)
    }
    
  2. Запустите modelCheckingTest() сейчас и проверьте состояния Counter , напечатанные в точках переключения, которые изменяют состояние счётчика (они начинаются с STATE:) :

    = Invalid execution results =
    | ------------------- |
    | Thread 1 | Thread 2 |
    | ------------------- |
    | STATE: 0            |
    | ------------------- |
    | inc(): 1 | inc(): 1 |
    | ------------------- |
    | STATE: 1            |
    | ------------------- |
    
    The following interleaving leads to the error:
    | -------------------------------------------------------------------- |
    | Thread 1 |                         Thread 2                          |
    | -------------------------------------------------------------------- |
    |          | inc()                                                     |
    |          |   inc(): 1 at CounterTest.inc(CounterTest.kt:10)          |
    |          |     value.READ: 0 at Counter.inc(BasicCounterTest.kt:10)  |
    |          |     switch                                                |
    | inc(): 1 |                                                           |
    | STATE: 1 |                                                           |
    |          |     value.WRITE(1) at Counter.inc(BasicCounterTest.kt:10) |
    |          |     STATE: 1                                              |
    |          |     value.READ: 1 at Counter.inc(BasicCounterTest.kt:10)  |
    |          |   result: 1                                               |
    | -------------------------------------------------------------------- |
    

В случае тестирования на стресс Lincheck отображает представление состояния непосредственно перед и после параллельной части сценария, а также в конце.

  • Получите полный код этих примеров

  • Посмотрите больше примеров тестов

Следующий шаг

Узнайте, как настроить аргументы, передаваемые операциям, и когда это может быть полезно.

Последнее изменение: 25 июля 2023 г.
Написание вашего первого теста с помощью 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API