12

問題

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

オブジェクトが有効であるためには、次の規則に従う必要があります。

  1. すべての InitEvent は、すべての RefEvent の前に来る必要があります
  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>

壊れた正しい道?

定数を生成することから完全に逃れることはできないようです (名前のリストを 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
            }
   }

マーク・シーマンに感謝!

4

1 に答える 1