Подтвердить что ты не робот

Несогласованное IEnumerable ArgumentException при создании сложного объекта с использованием FsCheck

Проблема

В F # я использую FsCheck для создания объекта (который затем я использую в тесте Xunit, но я могу полностью воссоздать вне Xunit, поэтому я думаю, что мы можем забыть о Xunit). Выполнение генерации 20 раз в FSI,

  • 50% времени, генерация выполняется успешно.
  • В 25% случаев генерация генерирует:

    System.ArgumentException: The input must be non-negative.
    Parameter name: index
    >    at Microsoft.FSharp.Collections.SeqModule.Item[T](Int32 index, IEnumerable`1 source)
       at [email protected](Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at [email protected][b](FSharpList`1 gs, FSharpList`1 acc, Int32 size, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 295
       at [email protected](Int32 n, StdGen r) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 297
       at [email protected](Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at [email protected][a](Int32 size, Gen`1 gn, Int32 i, StdGen seed, FSharpList`1 samples) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 157
       at FsCheck.Gen.Sample[a](Int32 size, Int32 n, Gen`1 gn) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 155
       at <StartupCode$FSI_0026>[email protected]() in C:\projects\Alberta\Core\TestFunc\Script.fsx:line 57
    Stopped due to error
    
  • В 25% случаев генерация генерирует:

    System.ArgumentException: The input sequence has an insufficient number of elements.
    Parameter name: index
    >    at Microsoft.FSharp.Collections.IEnumerator.nth[T](Int32 index, IEnumerator`1 e)
       at Microsoft.FSharp.Collections.SeqModule.Item[T](Int32 index, IEnumerable`1 source)
       at [email protected](Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at [email protected][b](FSharpList`1 gs, FSharpList`1 acc, Int32 size, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 295
       at [email protected](Int32 n, StdGen r) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 297
       at [email protected](Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at [email protected][a](Int32 size, Gen`1 gn, Int32 i, StdGen seed, FSharpList`1 samples) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 157
       at FsCheck.Gen.Sample[a](Int32 size, Int32 n, Gen`1 gn) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 155
       at <StartupCode$FSI_0025>[email protected]() in C:\projects\Alberta\Core\TestFunc\Script.fsx:line 57
    Stopped due to error
    

Ситуация

Объект выглядит следующим образом:

type Event =
| InitEvent of string
| RefEvent of string
type Stream = Event seq

Объект должен следовать следующим правилам:

  • Все InitEvents должны появиться перед всеми RefEvents
  • Все строки InitEvents должны быть уникальными
  • Все имена RefEvent должны иметь предыдущий соответствующий InitEvent
  • Но это нормально, если у некоторых InitEvents NOT есть соответствующие RefEvents
  • Но это нормально, если несколько RefEvents имеют одно и то же имя

Рабочее обходное решение

Если у меня есть генератор, вызывается функция, которая возвращает действительный объект и выполняет функцию Gen.constant(function), я никогда не сталкиваюсь с исключениями, но это не значит, что FsCheck предназначен для запуска!:)

/// <summary>
/// This is a non-generator equivalent which is 100% reliable
/// </summary>
let randomStream size =
   // valid names for a sample
   let names = Gen.sample size size Arb.generate<string> |> List.distinct
   // init events
   let initEvents = names |> List.map( fun name -> name |> InitEvent )
   // reference events
   let createRefEvent name = name |> RefEvent
   let genRefEvent = createRefEvent <!> Gen.elements names
   let refEvents = Gen.sample size size genRefEvent
   // combine
   Seq.append initEvents refEvents


type MyGenerators =
   static member Stream() = {
      new Arbitrary<Stream>() with
         override x.Generator = Gen.sized( fun size -> Gen.constant (randomStream size) )
   }

// repeatedly running the following two lines ALWAYS works
Arb.register<MyGenerators>()
let foo = Gen.sample 10 10 Arb.generate<Stream>

Разбитый правильный путь?

Я не могу полностью отвлечься от генерации константы (нужно сохранить список имен за пределами InitEvents, чтобы генерация RefEvent могла получить от них, но я могу получить больше в соответствии с тем, как работают генераторы FsCheck:

type MyGenerators =
   static member Stream() = {
      new Arbitrary<Stream>() with
         override x.Generator = Gen.sized( fun size ->
            // valid names for a sample
            let names = Gen.sample size size Arb.generate<string> |> List.distinct
            // generate inits
            let genInits = Gen.constant (names |> List.map InitEvent) |> Gen.map List.toSeq
            // generate refs
            let makeRef name = name |> RefEvent
            let genName = Gen.elements names
            let genRef = makeRef <!> genName
            Seq.append <!> genInits <*> ( genRef |> Gen.listOf )
         )
   }

// repeatedly running the following two lines causes the inconsistent errors
// If I don't re-register my generator, I always get the same samples.
// Is this because FsCheck is trying to be deterministic?
Arb.register<MyGenerators>()
let foo = Gen.sample 10 10 Arb.generate<Stream>

Что я уже проверил

  • Извините, забыл упомянуть в оригинальном вопросе, что я пытался выполнить Debug in Interactive, и из-за несовместимого поведения его несколько сложно отследить. Однако, когда удалены исключения, он, кажется, находится между концом моего кода генератора и тем, что запрашивает сгенерированные сэмплы, - а FsCheck - ДЕЛАТЬ поколение, похоже, пытается обработать некорректную последовательность. Я также предполагаю, что это потому, что я неправильно закодировал генератор.
  • IndexOutOfRangeException с использованием FsCheck предполагает потенциально подобную ситуацию. Я пробовал запускать тесты Xunit как с помощью Тест-пилота Resharper, так и Xunit console test runner на реальных тестах, на которых основано вышеописанное упрощение. Оба бегуна демонстрируют одинаковое поведение, поэтому проблема в другом месте.
  • Другие вопросы "Как мне генерировать...", например В FsCheck, как создать тестовую запись с неотрицательными полями? и Как генерируется "сложный" объект в FsCheck? касаются создания объектов меньшей сложности. Первое было большой помощью для получения кода, который у меня есть, а второй дает очень необходимый пример Arb.convert, но Arb.convert не имеет смысла, если я перехожу из "постоянный" список случайно сгенерированных имен. Кажется, все возвращается к тому, что нужно делать случайные имена, которые затем извлекаются, чтобы создать полный набор InitEvents и некоторую последовательность RefEvents, и те, которые ссылаются на "постоянный" список, не сопоставьте все, что я еще встречал.
  • Я просмотрел большинство примеров генераторов FsCheck, которые я могу найти, включая включенные примеры в FsCheck: https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs Они также делают не имеют дело с объектом, нуждающимся в внутренней согласованности, и, похоже, не применимы к этому случаю, хотя они были полезны в целом.
  • Возможно, это означает, что я приближаюсь к поколению объекта с бесполезной перспективы. Если существует другой способ создания объекта, который следует приведенным выше правилам, я открыт для переключения на него.
  • Дальнейшая отступка от проблемы, я видел другие сообщения SO, которые грубо говорят: "Если у вашего объекта такие ограничения, то что происходит, когда вы получаете недопустимый объект? Возможно, вам нужно переосмыслить, как этот объект потребляется лучше обрабатывать недействительные случаи". Если, например, я смог инициализировать "на лету" никогда не замеченное имя в RefEvent, вся потребность в предоставлении InitEvent сначала исчезнет - проблема изящно сводится к просто последовательности RefEvents из случайных имя. Я открыт для такого решения, но для этого потребуется немного переделать - в конечном итоге это может стоить того. В то же время остается вопрос, как вы можете надежно генерировать сложный объект, который следует за приведенными выше правилами, используя FsCheck?

Спасибо!

РЕДАКТИРОВАТЬ (S): Попытки решить

  • Код в ответе Марка Зеемана работает, но дает немного другой объект, чем я искал (я был неясен в своих объектных правилах - теперь, надеюсь, уточняется). Ввод его рабочего кода в генератор:

    type MyGenerators =
       static member Stream() = {
          new Arbitrary<Stream>() with
             override x.Generator =
                gen {
                   let! uniqueStrings = Arb.Default.Set<string>().Generator
                   let initEvents = uniqueStrings |> Seq.map InitEvent
    
                   let! sortValues =
                      Arb.Default.Int32()
                      |> Arb.toGen
                      |> Gen.listOfLength uniqueStrings.Count
                   let refEvents =
                      Seq.zip uniqueStrings sortValues
                      |> Seq.sortBy snd
                      |> Seq.map fst
                      |> Seq.map RefEvent
    
                   return Seq.append initEvents refEvents
                }
        }
    

    Это дает объект, в котором каждый InitEvent имеет соответствующий RefEvent, и для каждого InitEvent есть только один RefEvent. Я пытаюсь настроить код, чтобы я мог получить несколько RefEvents для каждого имени, и не все имена должны иметь RefEvent. ex: Init foo, Init bar, Ref foo, Ref foo отлично действует. Попытка настроить его с помощью:

    type MyGenerators =
       static member Stream() = {
          new Arbitrary<Stream>() with
             override x.Generator =
                gen {
                   let! uniqueStrings = Arb.Default.Set<string>().Generator
                   let initEvents = uniqueStrings |> Seq.map InitEvent
    
                   // changed section starts
                   let makeRef name = name |> RefEvent
                   let genRef = makeRef <!> Gen.elements uniqueStrings
                   return! Seq.append initEvents <!> ( genRef |> Gen.listOf )
                   // changed section ends
                }
       }
    

    Измененный код по-прежнему демонстрирует противоречивое поведение. Интересно, что из 20 пробных прогонов всего три обработанных (по сравнению с 10), а недостаточное количество элементов было сброшено 8 раз и . Ввод должен быть неотрицательным. брошенных 9 раз - эти изменения сделали краевой случай более чем в два раза вероятнее всего. Теперь мы переходим к очень маленькому разделу кода с ошибкой.

  • Марк быстро ответил другой версией для удовлетворения измененных требований:

    type MyGenerators =
       static member Stream() = {
          new Arbitrary<Stream>() with
             override x.Generator =
                gen {
                   let! uniqueStrings = Arb.Default.NonEmptySet<string>().Generator
                   let initEvents = uniqueStrings.Get |> Seq.map InitEvent
    
                   let! refEvents =
                      uniqueStrings.Get |> Seq.map RefEvent |> Gen.elements |> Gen.listOf
    
                   return Seq.append initEvents refEvents
                }
       }
    

    Это позволило некоторым именам не иметь RefEvent.

ЗАКЛЮЧИТЕЛЬНЫЙ КОД Очень незначительная настройка делает это так, что могут возникать дубликаты RefEvents:

type MyGenerators =
   static member Stream() = {
      new Arbitrary<Stream>() with
         override x.Generator =
            gen {
               let! uniqueStrings = Arb.Default.NonEmptySet<string>().Generator
               let initEvents = uniqueStrings.Get |> Seq.map InitEvent

               let! refEvents =
                  //uniqueStrings.Get |> Seq.map RefEvent |> Gen.elements |> Gen.listOf
                  Gen.elements uniqueStrings.Get |> Gen.map RefEvent |> Gen.listOf

               return Seq.append initEvents refEvents
            }
   }

Большое спасибо Mark Seemann!

4b9b3361

Ответ 1

Gen

Здесь один из способов удовлетворения требований:

open FsCheck

let streamGen = gen {
    let! uniqueStrings = Arb.Default.Set<string>().Generator
    let initEvents = uniqueStrings |> Seq.map InitEvent

    let! sortValues =
        Arb.Default.Int32()
        |> Arb.toGen
        |> Gen.listOfLength uniqueStrings.Count
    let refEvents =
        Seq.zip uniqueStrings sortValues
        |> Seq.sortBy snd
        |> Seq.map fst
        |> Seq.map RefEvent

    return Seq.append initEvents refEvents }

Полуофициальный ответ о том, как создавать уникальные строки, должен генерировать Set<string>. Поскольку Set<'a> также реализует 'a seq, вы можете использовать на нем все обычные функции Seq.

Генерация значений InitEvent, то это простая операция map над уникальными строками.

Поскольку каждый RefEvent должен иметь соответствующий InitEvent, вы можете повторно использовать одни и те же уникальные строки, но вы можете захотеть указать значение RefEvent для параметра в другом порядке. Для этого вы можете сгенерировать sortValues, который представляет собой список случайных значений int. Этот список имеет ту же длину, что и набор строк.

В этот момент у вас есть список уникальных строк и список случайных целых чисел. Вот некоторые поддельные значения, которые иллюстрируют концепцию:

> let uniqueStrings = ["foo"; "bar"; "baz"];;
val uniqueStrings : string list = ["foo"; "bar"; "baz"]

> let sortValues = [42; 1337; 42];;    
val sortValues : int list = [42; 1337; 42]

Теперь вы можете zip их:

> List.zip uniqueStrings sortValues;;
val it : (string * int) list = [("foo", 42); ("bar", 1337); ("baz", 42)]

Сортировка такой последовательности на ее втором элементе даст вам случайный перетасованный список, а затем вы можете map только первый элемент:

> List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;;
val it : string list = ["foo"; "baz"; "bar"]

Поскольку все значения InitEvent должны появиться перед значениями RefEvent, вы можете добавить refEvents в initEvents и вернуть этот объединенный список.

Проверка

Вы можете проверить, что streamGen работает по назначению:

open FsCheck.Xunit
open Swensen.Unquote

let isInitEvent = function InitEvent _ -> true | _ -> false
let isRefEvent =  function RefEvent  _ -> true | _ -> false

[<Property(MaxTest = 100000)>]
let ``All InitEvents must come before all RefEvents`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        test <@ s |> Seq.skipWhile isInitEvent |> Seq.forall isRefEvent @>

[<Property(MaxTest = 100000)>]
let ``All InitEvents strings must be unique`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s |> Seq.choose (function InitEvent s -> Some s | _ -> None)
        let distinctStrings = initEventStrings |> Seq.distinct

        distinctStrings |> Seq.length =! (initEventStrings |> Seq.length)

[<Property(MaxTest = 100000)>]
let ``All RefEvent names must have an earlier corresponding InitEvent`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s
            |> Seq.choose (function InitEvent s -> Some s | _ -> None)
            |> Seq.sort
            |> Seq.toList
        let refEventStrings =
            s
            |> Seq.choose (function RefEvent s -> Some s | _ -> None)
            |> Seq.sort
            |> Seq.toList

        initEventStrings =! refEventStrings

Эти три свойства передаются на мою машину.


Требования к Looser

В соответствии с более слабыми требованиями, изложенными в комментариях к этому ответу, здесь обновляется генератор, который извлекает значения из строк initEvents:

open FsCheck

let streamGen = gen {
    let! uniqueStrings = Arb.Default.NonEmptySet<string>().Generator
    let initEvents = uniqueStrings.Get |> Seq.map InitEvent

    let! refEvents =
        uniqueStrings.Get |> Seq.map RefEvent |> Gen.elements |> Gen.listOf

    return Seq.append initEvents refEvents }

На этот раз uniqueStrings является непустым набором строк.

Вы можете использовать Seq.map RefEvent для генерации последовательности всех допустимых значений RefEvent на основе uniqueStrings, а затем Gen.elements для определения генератора допустимых значений RefEvent, которые извлекаются из этой последовательности действительных значений. Наконец, Gen.listOf создает списки значений, сгенерированных этим генератором.

Испытания

Эти тесты показывают, что streamGen генерирует значения в соответствии с правилами:

open FsCheck.Xunit
open Swensen.Unquote

let isInitEvent = function InitEvent _ -> true | _ -> false
let isRefEvent =  function RefEvent  _ -> true | _ -> false

[<Property(MaxTest = 100000)>]
let ``All InitEvents must come before all RefEvents`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        test <@ s |> Seq.skipWhile isInitEvent |> Seq.forall isRefEvent @>

[<Property(MaxTest = 100000)>]
let ``All InitEvents strings must be unique`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s |> Seq.choose (function InitEvent s -> Some s | _ -> None)
        let distinctStrings = initEventStrings |> Seq.distinct

        distinctStrings |> Seq.length =! (initEventStrings |> Seq.length)

[<Property(MaxTest = 100000)>]
let ``All RefEvent names must have an earlier corresponding InitEvent`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s
            |> Seq.choose (function InitEvent s -> Some s | _ -> None)
            |> Seq.sort
            |> Set.ofSeq

        test <@ s
                |> Seq.choose (function RefEvent s -> Some s | _ -> None)
                |> Seq.forall initEventStrings.Contains @>

Эти три свойства передаются на мою машину.