Проблема
В 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!