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

Inconsistent IEnumerable ArgumentException while generating a complex object using FsCheck

问题

在 F# 中,我使用 FsCheck 生成一个对象(然后我在 Xunit 测试中使用它,但我可以完全在 Xunit 之外重新创建,所以我想我们可以忘记 Xunit)。 运行 FSI生成20次,

情况

对象如下:

type Event =
| InitEvent of string
| RefEvent of string
type Stream = Event seq

对象必须遵循以下规则才有效:

  1. 所有 InitEvents 必须在所有 RefEvents 之前出现
  2. 所有 InitEvents 字符串必须是唯一的
  3. 所有 RefEvent 名称必须有一个更早的对应 InitEvent
  4. 但如果某些 InitEvents 没有后来对应的 RefEvents 也没关系
  5. 但是多个RefEvent同名也没关系

解决方法

如果我让生成器调用一个 returns 有效对象的函数并执行 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>

破碎的正道?

我似乎无法完全避免生成常量(需要将名称列表存储在 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>

我已经检查过的内容

谢谢!

编辑(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
                }
        }
    

    这会产生一个对象,其中每个 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
                }
       }
    

    修改后的代码仍然表现出不一致的行为。有趣的是,在 20 个样本 运行 中,只有三个有效(从 10 个减少),而 元素数量不足 被抛出 8 次并且 输入must be non-negative 被抛出 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
                }
       }
    

    这允许某些名称没有 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!

一代

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

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 }

就是生成一个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]

您现在可以 zip 他们:

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

在第二个元素上对这样的序列进行排序将为您提供一个随机打乱的列表,然后您可以 map 仅对第一个元素进行排序:

> List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;;
val it : string list = ["foo"; "baz"; "bar"]

由于所有 InitEvent 值都必须在 RefEvent 值之前,您现在可以将 refEvents 附加到 initEvents 和 return 这个组合列表。

验证

您可以验证 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

这三个属性在我的机器上都通过了。


更宽松的要求

根据对此答案的评论中概述的宽松要求,这里有一个更新的生成器,它从 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 }

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

你可以使用Seq.map RefEvent根据uniqueStrings生成所有有效RefEvent值的序列,然后Gen.elements定义一个有效[=23]的生成器=] 从该有效值序列中提取的值。最后,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 @>

这三个属性在我的机器上都通过了。