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

chr*_*olo 12 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.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
    
    Run Code Online (Sandbox Code Playgroud)
  • 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
    
    Run Code Online (Sandbox Code Playgroud)

情况

对象如下:

type Event =
| InitEvent of string
| RefEvent of string
type Stream = Event seq
Run Code Online (Sandbox Code Playgroud)

该对象必须遵循这些规则才有效:

  1. 所有InitEvents必须在所有RefEvent之前
  2. 所有InitEvents字符串必须是唯一的
  3. 所有RefEvent名称必须具有较早的相应InitEvent
  4. 但是如果一些InitEvents没有以后相应的RefEvent就可以了
  5. 但是,如果多个RefEvent具有相同的名称,则可以

工作解决方案

如果我让生成器调用一个返回有效对象并执行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>
Run Code Online (Sandbox Code Playgroud)

破碎的正确方式?

我似乎无法完全摆脱生成常量(需要存储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>
Run Code Online (Sandbox Code Playgroud)

我已经检查过的东西

  • 对不起,忘了在原始问题中提到我曾尝试在Interactive中调试,并且由于行为不一致,因此有点难以追踪.然而,当异常命中时,它似乎在我的生成器代码的结尾和要求生成的样本之间 - 当FsCheck正在进行生成时,它似乎正在尝试处理格式错误的序列.我进一步假设这是因为我错误地编码了生成器.
  • 使用FsCheck的IndexOutOfRangeException建议可能类似的情况.我尝试通过Resharper测试运行器以及Xunit的控制台测试运行器在上述简化所基于的实际测试中运行我的Xunit测试.两个跑步者表现出相同的行为,因此问题出在其他地方.
  • 其他"我如何生成......"问题如In FsCheck,如何生成具有非负字段的测试记录?和一个人如何产生FsCheck一个"复杂"的对象?处理复杂程度较低的对象的创建.第一个是获得我所拥有的代码的一个很好的帮助,第二个给出了一个急需的Arb.convert示例,但如果我从一个随机的"常量"列表转换,Arb.convert没有意义生成的名字.这一切似乎都回到了这一点 - 需要制作随机名称,然后从中制作一套完整的InitEvent,以及一些引用回"常量"列表的RefEvent序列,匹配我遇到过的任何东西.
  • 我已经查看了我能找到的大多数FsCheck生成器示例,包括FsCheck中包含的示例:https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs 这些也没有处理需要内部一致性的对象,并且似乎不适用于这种情况,即使它们总体上有所帮助.
  • 也许这意味着我正在从一个无益的角度来看待对象的产生.如果有一种不同的方式来生成遵循上述规则的对象,我可以切换到它.
  • 进一步退出问题,我看到其他SO帖子大致上说"如果你的对象有这样的限制,那么当你收到一个无效的对象时会发生什么?也许你需要重新思考这个对象被消耗的方式来更好地处理无效案件".例如,如果我能够在RefEvent中即时初始化一个前所未见的名称,那么首先给出InitEvent的全部需求将会消失 - 问题优雅地简化为一些随机的RefEvents序列名称.我对这种解决方案持开放态度,但这需要一些返工 - 从长远来看,这可能是值得的.与此同时,问题仍然存在,您如何使用FsCheck可靠地生成遵循上述规则的复杂对象?

谢谢!

编辑(S):尝试解决

  • Mark Seemann的答案中的代码有效,但产生的对象与我想要的略有不同(我的目标规则中我不清楚 - 现在有希望澄清).将他的工作代码放在我的生成器中:

    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
                }
        }
    
    Run Code Online (Sandbox Code Playgroud)

    这产生了一个对象,其中每个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
                }
       }
    
    Run Code Online (Sandbox Code Playgroud)

    修改后的代码仍然表现出不一致的行为.有趣的是,在20次样本运行中,只有3次运行(从10次开始),而元素数量不足被抛出8次,输入必须是非负数被抛出9次 - 这些变化使边缘情况超过了被击中的可能性是其两倍.我们现在归结为一小部分错误的代码.

  • Mark很快回复了另一个版本以解决变更的要求:

    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
                }
       }
    
    Run Code Online (Sandbox Code Playgroud)

    这允许一些名称没有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
            }
   }
Run Code Online (Sandbox Code Playgroud)

非常感谢Mark Seemann!

Mar*_*ann 6

根

这是满足要求的一种方法:

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 }
Run Code Online (Sandbox Code Playgroud)

关于如何生成唯一字符串的半官方答案是生成一个Set<string>.既然Set<'a>也实现了'a seq,你可以使用它上面的所有常规Seq函数.

因此,生成InitEvent值是map对唯一字符串的简单操作.

由于每个都RefEvent必须具有相应的InitEvent字符串,因此您可以重复使用相同的唯一字符串,但您可能希望赋予RefEvent选项值以不同的顺序显示.为此,您可以生成sortValues,这是一个随机int值列表.此列表与字符串集的长度相同.

此时,您有一个唯一字符串列表和一个随机整数列表.以下是一些说明这个概念的假值:

> let uniqueStrings = ["foo"; "bar"; "baz"];;
val uniqueStrings : string list = ["foo"; "bar"; "baz"]

> let sortValues = [42; 1337; 42];;    
val sortValues : int list = [42; 1337; 42]
Run Code Online (Sandbox Code Playgroud)

你现在zip可以:

> List.zip uniqueStrings sortValues;;
val it : (string * int) list = [("foo", 42); ("bar", 1337); ("baz", 42)]
Run Code Online (Sandbox Code Playgroud)

在第二个元素上对这样的序列进行排序会给你一个随机洗牌的列表,然后你就map可以只得到第一个元素:

> List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;;
val it : string list = ["foo"; "baz"; "bar"]
Run Code Online (Sandbox Code Playgroud)

由于所有InitEvent值必须出现在之前RefEvent的值,你现在可以附加refEvents到initEvents,并返回该组合列表.

验证

您可以验证其是否streamGen按预期工作:

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
Run Code Online (Sandbox Code Playgroud)

这三个属性都在我的机器上传递.


更宽松的要求

根据本答案评论中概述的更宽松的要求,这里是一个更新的生成器,它从InitEvents字符串中绘制值:

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 }
Run Code Online (Sandbox Code Playgroud)

这一次,uniqueStrings是一组非空字符串.

您可以使用Seq.map RefEvent生成所有有效的序列RefEvent基于值uniqueStrings,然后Gen.elements以确定有效的生成RefEvent值从有效值的该序列绘制.最后,Gen.listOf创建该生成器生成的值列表.

测试

这些测试表明streamGen根据规则生成值:

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 @>
Run Code Online (Sandbox Code Playgroud)

这三个属性都在我的机器上传递.