Проблема
В 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
Чтобы объект был действительным, он должен соответствовать следующим правилам:
- Все InitEvents должны предшествовать всем RefEvents.
- Все строки InitEvents должны быть уникальными
- Все имена RefEvent должны иметь более раннее соответствующее InitEvent
- Но это нормально, если некоторые InitEvents НЕ имеют более поздних соответствующих RefEvents
- Но это нормально, если несколько 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
}
}
Большое спасибо Марку Земанну!
List.append
вместоSeq.append
? - person Fyodor Soikin   schedule 10.03.2016Event list
вместоEvent seq
, что означает, что в конце моего генератора я вместо этого делаюList.append
- конечный результат - такое же поведение, такое же несогласованность, одни и те же классы, связанные с последовательностями, хотя теперь это список. Я не уверен, что это за последовательность, возможно, внутреннее устройствоFsCheck.Gen
? - person chryosolo   schedule 10.03.2016GenBuilder.bind
@ 62 github.com/ fscheck / FsCheck / blob / master / src / FsCheck / Gen.fs # L61, который является полностью общим и вообще не ссылается ни на что последовательность-y. Это заставляет меня думать, что функции последовательности происходят из аргументовGenBuilder.bind
. Первоначально я думал, что аргументы предоставлены вами, но теперь, когда у вас нет никакихseq
, эта теория не работает. - person Fyodor Soikin   schedule 10.03.2016