Несогласованное 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!
Ответы
Ответ 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 @>
Эти три свойства передаются на мою машину.