【问题标题】:Inconsistent IEnumerable ArgumentException while generating a complex object using FsCheck使用 FsCheck 生成复杂对象时出现不一致的 IEnumerable ArgumentException
【发布时间】:2016-03-11 07:31:22
【问题描述】:

问题

在 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. 所有 InitEvents 必须在所有 RefEvents 之前
  2. 所有 InitEvents 字符串必须是唯一的
  3. 所有 RefEvent 名称必须有一个更早的对应 InitEvent
  4. 但如果某些 InitEvents 后面没有对应的 RefEvents 也没关系
  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>

破碎的正确的道路?

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

我已经检查过的内容

  • 抱歉,忘记在原始问题中提及我曾尝试在交互式中调试,并且由于不一致的行为,有点难以追踪。但是,当异常发生时,它似乎在我的生成器代码的结尾和要求生成的样本之间——而 FsCheck 正在生成,它似乎正在尝试处理格式错误的序列。我进一步假设这是因为我错误地对生成器进行了编码。
  • IndexOutOfRangeException using FsCheck 表明可能存在类似情况。我已经尝试通过 Resharper 测试运行器 以及 Xunit 的控制台测试运行器 在上述简化所基于的实际测试中运行我的 Xunit 测试。两个跑步者表现出相同的行为,所以问题出在其他地方。
  • 诸如In FsCheck, how to generate a test record with non-negative fields?How does one generate a "complex" object in FsCheck? 等其他“我如何生成...”问题涉及创建复杂度较低的对象。第一个对获取我拥有的代码有很大帮助,第二个提供了一个急需的 Arb.convert 示例,但如果我从随机生成的名称的“恒定”列表。一切似乎都回到了这一点——需要生成随机名称,然后从中提取出一组完整的 InitEvents,以及一些引用“常量”列表的 RefEvents 序列,并没有匹配我遇到的任何东西。
  • 我查看了我能找到的大多数 FsCheck 生成器示例,包括 FsCheck 中包含的示例:https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs 这些也不处理需要内部一致性的对象,并且似乎不适用于这种情况,甚至尽管它们总体上很有帮助。
  • 也许这意味着我正在从无益的角度接近对象的生成。如果有其他方法可以生成遵循上述规则的对象,我愿意切换到它。
  • 进一步远离这个问题,我看到其他 SO 帖子粗略地说“如果你的对象有这样的限制,那么当你收到一个无效的对象时会发生什么?也许你需要重新考虑这个对象的消费方式更好地处理无效案件。”例如,如果我能够在 RefEvent 中动态初始化一个从未见过的名称,那么首先给出 InitEvent 的整个需求就会消失——问题优雅地简化为一些随机的 RefEvent 序列姓名。我对这种解决方案持开放态度,但它需要一些返工——从长远来看,这可能是值得的。同时,问题仍然存在,如何使用 FsCheck 可靠地生成遵循上述规则的复杂对象?

谢谢!

编辑:尝试解决

  • 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 次样本运行中,只有 3 次有效(从 10 次下降),而 insufficient number of elements 被抛出 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
                }
       }
    

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

【问题讨论】:

  • 这里有一个建议:如果在生成器的最后一行使用List.append 而不是Seq.append,会发生什么?
  • 感谢您的建议,我只是尝试将整体 Stream 类型转换为 Event list 而不是 Event seq,这意味着在生成器的末尾我会使用 List.append 来代替 --最终结果是相同的行为、相同的不一致、相同的与序列相关的类,即使它现在是一个列表。我不确定序列是什么,可能是FsCheck.Gen的内部?
  • 堆栈跟踪指向GenBuilder.bind@62 github.com/fscheck/FsCheck/blob/master/src/FsCheck/Gen.fs#L61,它是完全通用的,根本不引用任何序列-y。这让我相信序列函数来自GenBuilder.bind 的参数。最初我认为参数是由你提供的,但现在你也没有任何seqs,这个理论不成立。
  • 还没有得到计算表达式,我不确定 FsCheck 在这里做什么。向下看调用堆栈一层和两层,将您置于一个嵌套递归函数中,该函数似乎一次将一个序列转换为一个列表,第 295 行 github.com/fscheck/FsCheck/blob/master/src/FsCheck/Gen.fs#L295 为每个项目调用绑定?根据调用栈传入SequenceToList函数的初始序列又是bind@62,我跑了一圈,不知道从哪里下车! :)

标签: f# fscheck


【解决方案1】:

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

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 }

semi-official answer on how to generate unique strings 是生成一个Set&lt;string&gt;。由于Set&lt;'a&gt; 也实现了'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,并返回此组合列表。

验证

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

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


更宽松的要求

根据 cmets 中对这个答案提出的更宽松的要求,这里有一个更新的生成器,它从 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 定义从该有效值序列中提取的有效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 @>

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

【讨论】:

  • 抱歉粗鲁,受评论长度的阻碍! :) 您的代码按原样工作,但不是我想要的。我不清楚——每个 Init 都不需要有一个匹配的 Ref,任何给定的 Init 都可以有多个 Ref。我正在尝试调整您的代码,但我还没有得到 gen {...} - let 似乎将结果保留在 Gen 的范围内,而 let! 将结果“下降”到一个值根是基于。 (Drop 与将值“提升”到 Gen 中相反)。我在正确的轨道上吗?我的第一次尝试仍然遇到同样的错误,所以至少要解决问题!
  • 完美!我非常感谢您花时间准备有用且内容丰富的答案。你让我思考了很多事情!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-01
  • 2020-01-14
  • 2021-07-30
  • 1970-01-01
相关资源
最近更新 更多