問題
F# では、FsCheck を使用してオブジェクトを生成しています (Xunit テストで使用していますが、Xunit の外部で完全に再作成できるため、Xunit のことは忘れてもかまいません)。FSI で世代を 20 回実行すると、
- 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 FsCheck.GenBuilder.bind@62.Invoke(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 FsCheck.Gen.SequenceToList@297.Invoke(Int32 n, StdGen r) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 297 at FsCheck.GenBuilder.bind@62.Invoke(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 FsCheck.GenBuilder.bind@62.Invoke(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 FsCheck.Gen.SequenceToList@297.Invoke(Int32 n, StdGen r) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 297 at FsCheck.GenBuilder.bind@62.Invoke(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
オブジェクトが有効であるためには、次の規則に従う必要があります。
- すべての InitEvent は、すべての RefEvent の前に来る必要があります
- すべての 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>
壊れた正しい道?
定数を生成することから完全に逃れることはできないようです (名前のリストを InitEvent の外部に格納して、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 が生成を実行している間、不正なシーケンスを処理しようとしているようです。さらに、これはジェネレーターのコーディングが間違っているためだと思います。
- FsCheck を使用した IndexOutOfRangeExceptionは、同様の状況の可能性を示唆しています。上記の単純化が基づいている実際のテストで、 Resharper テスト ランナーとXunit のコンソール テスト ランナーの両方を介して Xunit テストを実行しようとしました。どちらのランナーも同じ動作を示すため、問題は別の場所にあります。
- FsCheckでは、負でないフィールドを含むテスト レコードを生成する方法は? FsCheck で「複雑な」オブジェクトを生成するにはどうすればよいですか? 複雑さの少ないオブジェクトの作成を扱います。最初のものは私が持っているコードにたどり着くのに非常に役立ちました.生成された名前。それはすべてそれに戻っているようです-ランダムな名前を作成する必要があり、それから引き出されて InitEvents の完全なセットを作成し、RefEvents のいくつかのシーケンスを作成します。どちらも「定数」リストを参照しますが、そうではありません。私がまだ出会ったことのないものと一致します。
- FsCheck に含まれている例を含め、見つけることができる FsCheck ジェネレーターのほとんどの例を調べました : https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs内部の一貫性を必要とするオブジェクトを扱い、全体的には役に立ちましたが、このケースには当てはまらないようです。
- おそらくこれは、役に立たない観点からオブジェクトの生成に取り組んでいることを意味します。上記のルールに従うオブジェクトを生成する別の方法がある場合は、それに切り替えることにオープンです。
- 問題からさらに後退して、「オブジェクトにそのような制限がある場合、無効なオブジェクトを受け取るとどうなりますか?おそらく、無効なオブジェクトをより適切に処理するために、このオブジェクトが消費される方法を再考する必要があります。」ケース。」たとえば、RefEvent でこれまで見たことのない名前をオンザフライで初期化できた場合、最初に InitEvent を指定する必要はなくなります。名前。私はこの種の解決策を受け入れますが、少しやり直す必要があります-長期的には、それだけの価値があるかもしれません. それまでの間、FsCheck を使用して上記の規則に従う複雑なオブジェクトを確実に生成するにはどうすればよいかという疑問が残ります。
ありがとう!
EDIT(S): 解決の試み
Mark Seemann's answer のコードは機能しますが、私が探していたものとはわずかに異なるオブジェクトが生成されます (オブジェクト ルールが明確ではありませんでしたが、明確になったことを願っています)。彼の作業コードを私のジェネレーターに入れます:
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 が 1 つだけ存在するオブジェクトが生成されます。名前ごとに複数の 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 回のサンプル実行のうち、3 回しか機能しませんでしたが (10 回から減少)、不十分な数の要素が 8 回スローされ、The input must be non-negativeが 9 回スローされました。これらの変更により、エッジ ケースが当たる確率が2倍。これで、エラーのあるコードの非常に小さなセクションに到達しました。
マークは、変更された要件に対応するために、別のバージョンで迅速に対応しました。
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
}
}
マーク・シーマンに感謝!