Несогласованный 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 FsCheck.Gen.go@290-1[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 FsCheck.Gen.sample@155[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>.$FSI_0026.main@() 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 FsCheck.Gen.go@290-1[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 FsCheck.Gen.sample@155[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>.$FSI_0025.main@() 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

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

  1. Все InitEvents должны предшествовать всем RefEvents.
  2. Все строки InitEvents должны быть уникальными
  3. Все имена RefEvent должны иметь более раннее соответствующее InitEvent
  4. Но это нормально, если некоторые InitEvents НЕ имеют более поздних соответствующих RefEvents
  5. Но это нормально, если несколько RefEvents имеют одно и то же имя

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

Если у меня есть генератор, вызывающий функцию, которая возвращает действительный объект и выполняет Gen.constant (функцию), я никогда не сталкиваюсь с исключениями, но 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>

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

  • Извините, забыл упомянуть в исходном вопросе, что я пытался отладить в интерактивном режиме, и из-за непоследовательного поведения его довольно сложно отследить. Однако, когда возникают исключения, кажется, что это находится между концом моего кода генератора и тем, что запрашивает сгенерированные образцы - в то время как FsCheck выполняет генерацию, похоже, он пытается обработать искаженную последовательность. Я также предполагаю, что это потому, что я неправильно закодировал генератор.
  • IndexOutOfRangeException с использованием FsCheck предполагает потенциально аналогичную ситуацию. Я пробовал запускать свои тесты Xunit как через средство запуска тестов Resharper, так и через средство запуска тестов консоли Xunit на реальных тестах, на которых основано вышеупомянутое упрощение. Оба бегуна демонстрируют идентичное поведение, поэтому проблема в другом.
  • Другие вопросы типа «Как создать ...», например Как сгенерировать тестовую запись с неотрицательными полями в 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. Я пытаюсь настроить код, чтобы получить несколько событий RefEvent для каждого имени, и не всем именам необходимо иметь событие RefEvent. Пример: 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.

ОКОНЧАТЕЛЬНЫЙ КОД. Очень незначительная настройка, позволяющая дублировать события RefEvent:

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
            }
   }

Большое спасибо Марку Земанну!


person chryosolo    schedule 10.03.2016    source источник
comment
Вот предложение: что произойдет, если в последней строке вашего генератора вы используете List.append вместо Seq.append?   -  person Fyodor Soikin    schedule 10.03.2016
comment
Спасибо за предложение, я просто попытался превратить общий тип потока в Event list вместо Event seq, что означает, что в конце моего генератора я вместо этого делаю List.append - конечный результат - такое же поведение, такое же несогласованность, одни и те же классы, связанные с последовательностями, хотя теперь это список. Я не уверен, что это за последовательность, возможно, внутреннее устройство FsCheck.Gen?   -  person chryosolo    schedule 10.03.2016
comment
Трассировка стека указывает на GenBuilder.bind @ 62 github.com/ fscheck / FsCheck / blob / master / src / FsCheck / Gen.fs # L61, который является полностью общим и вообще не ссылается ни на что последовательность-y. Это заставляет меня думать, что функции последовательности происходят из аргументов GenBuilder.bind. Первоначально я думал, что аргументы предоставлены вами, но теперь, когда у вас нет никаких seq, эта теория не работает.   -  person Fyodor Soikin    schedule 10.03.2016
comment
Еще не получил вычислительных выражений, я не уверен, что здесь делает FsCheck. Глядя вниз на один и два уровня стека вызовов, вы попадаете во вложенную рекурсивную функцию, которая, кажется, преобразует последовательность в список по одному элементу за раз, со строкой 295 github.com/fscheck/FsCheck/blob/master/src/FsCheck/Gen.fs#L295 вызов привязки по каждому пункту? Первоначальная последовательность, переданная в функцию SequenceToList в соответствии со стеком вызовов, снова связана с @ 62, я попал в круг, и я не уверен, с чего мне выйти! :)   -  person chryosolo    schedule 10.03.2016


Ответы (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 values ​​on в другом порядке. Для этого вы можете сгенерировать 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

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


Более слабые требования

Основываясь на более свободных требованиях, изложенных в комментариях к этому ответу, вот обновленный генератор, который извлекает значения из строк 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 @>

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

person Mark Seemann    schedule 10.03.2016
comment
Приносим извинения за резкость, которой мешает длина комментария! :) Ваш код работает как есть, но это не совсем то, что я ищу. Мне было непонятно - нет необходимости для каждого Init иметь соответствующий Ref, и может быть несколько Refs для любого данного Init. Я пытаюсь настроить ваш код, но я еще не получил gen {...} - let, похоже, сохраняет результат в области Gen, а let! снижает результат до значения, на котором основано Gen. (Drop - это противоположность подъему значения в Gen). Я на правильном пути? Моя первая попытка все еще наталкивается на ту же ошибку, так что хотя бы отточить проблему! - person chryosolo; 11.03.2016
comment
Идеально! Я очень признателен за ваше время, потраченное на подготовку полезного и информативного ответа. Вы дали мне много поводов для размышлений! - person chryosolo; 11.03.2016