ScalaCheck сжимает данные команды при тестировании с отслеживанием состояния

При выполнении тестирования с отслеживанием состояния с помощью ScalaCheck библиотека может уменьшить количество команд, необходимых для поиска определенной ошибки. Как в примере счетчика из руководства пользователя: https://github.com/typelevel/scalacheck/blob/master/doc/UserGuide.md. Но что, если команды принимают аргументы, и я хочу, чтобы ScalaCheck также уменьшал данные внутри команд? См. сценарий, в котором я тестирую счетчик ниже:

package Counter

case class Counter() {
  private var n = 1
  def increment(incrementAmount: Int) = {
    if (n%100!=0) {
      n += incrementAmount
    }
  }
  def get(): Int = n
}

Счетчик запрограммирован с ошибкой. Он не должен увеличиваться на указанную сумму, если n%100 == 0. Таким образом, если значение n равно x*100, где x — любое положительное целое число, счетчик не увеличивается. Я тестирую счетчик с помощью теста состояния ScalaCheck ниже:

import Counter.Counter

import org.scalacheck.commands.Commands
import org.scalacheck.{Gen, Prop}
import scala.util.{Success, Try}

object CounterCommands extends Commands {
  type State = Int
  type Sut = Counter

  def canCreateNewSut(newState: State, initSuts: Traversable[State],
                      runningSuts: Traversable[Sut]): Boolean = true
  def newSut(state: State): Sut = new Counter
  def destroySut(sut: Sut): Unit = ()
  def initialPreCondition(state: State): Boolean = true
  def genInitialState: Gen[State] = Gen.const(1)
  def genCommand(state: State): Gen[Command] = Gen.oneOf(Increment(Gen.chooseNum(1, 200000).sample.get), Get)

  case class Increment(incrementAmount: Int) extends UnitCommand {
    def run(counter: Sut) = counter.increment(incrementAmount)
    def nextState(state: State): State = {state+incrementAmount}
    def preCondition(state: State): Boolean = true
    def postCondition(state: State, success: Boolean) = success
  }

  case object Get extends Command {
    type Result = Int
    def run(counter: Sut): Result = counter.get()
    def nextState(state: State): State = state
    def preCondition(state: State): Boolean = true
    def postCondition(state: State, result: Try[Int]): Prop = result == Success(state)
  }
}

Каждый раз, когда выбирается команда увеличения, ей дается произвольное целое число от 1 до 200000 в качестве аргумента. Запуск теста дал следующий результат:

! Falsified after 28 passed tests.
> Labels of failing property:
initialstate = 1
seqcmds = (Increment(1); Increment(109366); Increment(1); Increment(1); Inc
  rement(104970); Increment(27214); Increment(197045); Increment(1); Increm
  ent(54892); Get => 438600)
> ARG_0: Actions(1,List(Increment(1), Increment(109366), Increment(1), Incr
  ement(1), Increment(104970), Increment(27214), Increment(197045), Increme
  nt(1), Increment(54892), Get),List())
> ARG_0_ORIGINAL: Actions(1,List(Get, Get, Increment(1), Increment(109366),
   Get, Get, Get, Get, Increment(1), Get, Increment(1), Increment(104970),
  Increment(27214), Get, Increment(197045), Increment(1), Increment(54892),
   Get, Get, Get, Get, Get, Increment(172491), Get, Increment(6513), Get, I
  ncrement(57501), Increment(200000)),List())

ScalaCheck действительно уменьшил количество команд, необходимых для поиска ошибки (как видно из ARG_0), но не уменьшил данные внутри команд. В итоге он получил гораздо большее значение счетчика (438600), чем то, что действительно необходимо для поиска ошибки. Если бы первой команде увеличения было дано 99 в качестве аргумента, ошибка была бы найдена.

Есть ли в ScalaCheck способ сжать данные внутри команд при выполнении тестов с отслеживанием состояния? Используемая версия ScalaCheck — 1.14.1.

РЕДАКТИРОВАТЬ: я попытался упростить ошибку (и увеличить ее, только если n!=10) и добавил усадку, предложенную Леви, но все равно не смог заставить ее работать. Весь исполняемый код можно увидеть ниже:

package LocalCounter

import org.scalacheck.commands.Commands
import org.scalacheck.{Gen, Prop, Properties, Shrink}
import scala.util.{Success, Try}

case class Counter() {
  private var n = 1
  def increment(incrementAmount: Int) = {
    if (n!=10) {
      n += incrementAmount
    }
  }
  def get(): Int = n
}

object CounterCommands extends Commands {
  type State = Int
  type Sut = Counter

  def canCreateNewSut(newState: State, initSuts: Traversable[State],
                      runningSuts: Traversable[Sut]): Boolean = true
  def newSut(state: State): Sut = new Counter
  def destroySut(sut: Sut): Unit = ()
  def initialPreCondition(state: State): Boolean = true
  def genInitialState: Gen[State] = Gen.const(1)
  def genCommand(state: State): Gen[Command] = Gen.oneOf(Increment(Gen.chooseNum(1, 40).sample.get), Get)

  case class Increment(incrementAmount: Int) extends UnitCommand {
    def run(counter: Sut) = counter.increment(incrementAmount)
    def nextState(state: State): State = {state+incrementAmount}
    def preCondition(state: State): Boolean = true
    def postCondition(state: State, success: Boolean) = success
  }

  case object Get extends Command {
    type Result = Int
    def run(counter: Sut): Result = counter.get()
    def nextState(state: State): State = state
    def preCondition(state: State): Boolean = true
    def postCondition(state: State, result: Try[Int]): Prop = result == Success(state)
  }

  implicit val shrinkCommand: Shrink[Command] = Shrink({
      case Increment(amt) => Shrink.shrink(amt).map(Increment(_))
      case Get => Stream.empty
  })
}

object CounterCommandsTest extends Properties("CounterCommands") {
  CounterCommands.property().check()
}

Запуск кода дал следующий результат:

! Falsified after 4 passed tests.
> Labels of failing property:
initialstate = 1
seqcmds = (Increment(9); Increment(40); Get => 10)
> ARG_0: Actions(1,List(Increment(9), Increment(40), Get),List())
> ARG_0_ORIGINAL: Actions(1,List(Increment(9), Increment(34), Increment(40)
  , Get),List())

Это не минимальный пример.


person Villain    schedule 19.11.2020    source источник


Ответы (1)


Вы должны иметь возможность определить пользовательский Shrink для Command в следующих строках:

implicit val shrinkCommand: Shrink[Command] = Shrink({
  case Increment(amt) => shrink(amt).map(Increment(_))
  case Get => Stream.empty
}

Обратите внимание, что, поскольку Stream устарел в Scala 2.13, вам может потребоваться отключить предупреждения в Scala 2.13 (Scalacheck 1.15 позволит использовать LazyList для определения сжатия).

person Levi Ramsey    schedule 19.11.2020
comment
Я попытался поместить это в объект CounterCommands, но похоже, что он все еще не сжимается. Должен ли он быть где-то еще или как-то зарегистрирован? - person Villain; 20.11.2020
comment
Здравствуйте, Леви, я добавил упрощенный пример в исходный текст (после редактирования) с помощью вашего термоусадчика, но я все еще не мог заставить его работать. Я что-то упустил здесь? - person Villain; 27.11.2020