F# 使用FsCheck生成复杂对象时IEnumerable ArgumentException不一致

F# 使用FsCheck生成复杂对象时IEnumerable ArgumentException不一致,f#,fscheck,F#,Fscheck,问题 在F#中,我使用FsCheck生成一个对象(然后我在Xunit测试中使用它,但是我可以完全在Xunit之外重新创建,所以我想我们可以忘记Xunit)。在FSI中运行20次新一代 50%的时间,发电成功运行 25%的时间,这一代人抛出: System.ArgumentException: The input must be non-negative. Parameter name: index > at Microsoft.FSharp.Collections.SeqModul

问题

在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
    
    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
    
    该对象必须遵循以下规则才能有效:

  • 所有InitEvents必须位于所有RefEvents之前
  • 所有InitEvents字符串必须是唯一的
  • 所有RefEvent名称必须具有较早的对应InitEvent
  • 但是,如果某些InitEvents没有稍后相应的RefEvents,也可以
  • 但是,如果多个RefEvents具有相同的名称,则没有关系
  • 工作环境

    如果我让生成器调用一个返回有效对象的函数并执行Gen.constant(函数),我就不会遇到异常,但这不是运行FsCheck的方式!:)

    //
    ///这是一个100%可靠的非发电机等效物
    /// 
    让随机流大小=
    //样本的有效名称
    让name=Gen.sample size Arb.generate |>List.distinct
    //初始化事件
    让initEvents=names |>List.map(有趣的名字->名字|>InitEvent)
    //参考事件
    让createRefEvent name=name |>RefEvent
    让genRefEvent=createRefEvent Gen.elements名称
    设refEvents=Gen.sample size genRefEvent
    //结合
    Seq.append initEvents refEvents
    类型MyGenerator=
    静态成员流()={
    使用
    覆盖x.Generator=Gen.size(乐趣大小->Gen.constant(随机流大小))
    }
    //反复运行以下两行始终有效
    仲裁寄存器()
    设foo=Gen.sample 10 10 Arb.generate
    
    正确的道路?

    我似乎无法完全避免生成常量(需要在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>
    
    类型MyGenerators=
    静态成员流()={
    使用
    覆盖x.发电机=发电机尺寸(有趣的尺寸->
    //样本的有效名称
    让name=Gen.sample size Arb.generate |>List.distinct
    //生成初始化
    让genInits=Gen.constant(name |>List.map InitEvent)|>Gen.map List.toSeq
    //生成引用
    让makeRef name=name |>RefEvent
    让genName=Gen.elements名称
    让genRef=makeRef-genName
    Seq.append genInits(genRef |>Gen.listOf)
    )
    }
    //重复运行以下两行会导致不一致的错误
    //如果我不重新注册我的发电机,我总是得到相同的样本。
    //这是因为FsCheck试图确定吗?
    仲裁寄存器()
    设foo=Gen.sample 10 10 Arb.generate
    
    我已经检查的内容

    • 抱歉,在最初的问题中忘记提到,我试图在Interactive中调试,由于行为不一致,所以有点难以追踪。然而,当异常出现时,它似乎介于生成器代码的结尾和生成样本的要求之间——而FsCheck正在执行生成但是,它似乎试图处理一个格式错误的序列。我进一步假设这是因为我对生成器的编码不正确
    • 这表明可能存在类似的情况。我已尝试在上述简化所基于的真实测试中,通过Resharper test runner以及Xunit的console test runner运行Xunit测试。两个运行程序表现出相同的行为,因此问题出在其他地方
    • 其他“如何生成…”问题,如和处理创建复杂度较低的对象。第一个问题对获取我的代码有很大帮助,第二个问题提供了一个急需的示例Arb.convert,但是如果我从“常量”转换,则Arb.convert没有意义随机生成的名称列表。这一切似乎都回到了这一点——需要生成随机名称,然后从中提取以生成一组完整的InitEvents,以及一些引用“常量”列表的RefEvents序列,这与我遇到的任何内容都不匹配
    • 我已经浏览了我能找到的大多数FsCheck生成器示例,包括FsCheck中包含的示例:这些示例也不处理需要内部一致性的对象,并且似乎不适用于这种情况,尽管它们总体上很有帮助
    • 也许这意味着我正在从一个无用的角度来处理对象的生成。如果有一种不同的方法来生成一个遵循上述规则的对象,我愿意切换到它
    • 进一步远离这个问题,我看到其他SO帖子大致上说“如果你的对象有这样的限制,那么当你收到一个无效的对象时会发生什么?也许你需要重新思考这个对象的使用方式,以便更好地处理无效的情况。”例如,如果我能够动态初始化RefEvent中一个以前从未见过的名称,那么首先给出InitEvent的所有需求都将消失——问题优雅地简化为一系列随机名称的RefEvent。我对这种解决方案持开放态度,但它需要一些返工——从长远来看,可能是这样的值得。同时,问题仍然是,如何使用FsCheck可靠地生成符合上述规则的复杂对象
    谢谢

    编辑:尝试解决

    • Mark Seemann答案中的代码是有效的,但产生的obje略有不同
      /// <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>
      
      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>
      
      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
                  }
          }
      
      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
                  }
         }
      
      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
                  }
         }
      
      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
                  }
         }
      
      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 }
      
      > let uniqueStrings = ["foo"; "bar"; "baz"];;
      val uniqueStrings : string list = ["foo"; "bar"; "baz"]
      
      > let sortValues = [42; 1337; 42];;    
      val sortValues : int list = [42; 1337; 42]
      
      > List.zip uniqueStrings sortValues;;
      val it : (string * int) list = [("foo", 42); ("bar", 1337); ("baz", 42)]
      
      > List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;;
      val it : string list = ["foo"; "baz"; "bar"]
      
      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
      
      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 }
      
      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 @>