使用 FsCheck 生成复杂对象时不一致的 IEnumerable ArgumentException
Inconsistent IEnumerable ArgumentException while generating a complex object using FsCheck
问题
在 F# 中,我使用 FsCheck 生成一个对象(然后我在 Xunit 测试中使用它,但我可以完全在 Xunit 之外重新创建,所以我想我们可以忘记 Xunit)。 运行 FSI生成20次,
- 50% 的时间,生成 运行s 成功。
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
对象必须遵循以下规则才有效:
- 所有 InitEvents 必须在所有 RefEvents 之前出现
- 所有 InitEvents 字符串必须是唯一的
- 所有 RefEvent 名称必须有一个更早的对应 InitEvent
- 但如果某些 InitEvents 没有后来对应的 RefEvents 也没关系
- 但是多个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>
我已经检查过的内容
- 抱歉,忘记在原始问题中提及我已尝试在交互式中调试,并且由于行为不一致,很难找到。然而,当异常发生时,它似乎在我的生成器代码的末尾和生成样本的要求之间——当 FsCheck 正在生成时,它似乎正在尝试处理格式错误的序列。我进一步假设这是因为我对生成器的编码不正确。
- IndexOutOfRangeException using FsCheck 表示可能存在类似情况。我已经通过 Resharper 测试 运行ner 以及 Xunit 的控制台测试 运行ner[ 尝试了 运行ning 我的 Xunit 测试=102=] 基于上述简化所基于的实际测试。两个 运行ners 表现出相同的行为,所以问题出在其他地方。
- 其他 "How do I generate..." 问题,例如 In FsCheck, how to generate a test record with non-negative fields? and How does one generate a "complex" object in FsCheck? 涉及复杂性较低的对象的创建。第一个对获取我的代码有很大帮助,第二个给出了 Arb.convert 的急需示例,但 Arb.convert 没有如果我是从 "constant" 随机生成的名称列表转换过来的,请注意。这一切似乎都回到了那个——需要随机命名,然后从中提取以制作一组完整的 InitEvents 和一些 RefEvents 序列,两者都引用回 "constant" 列表,没有与我遇到的任何事情都不匹配。
- 我浏览了我能找到的大多数 FsCheck 生成器示例,包括 FsCheck 中包含的示例:https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs这些也不处理需要内部一致性的对象,而且似乎不适用于在这种情况下,尽管它们总体上是有帮助的。
- 也许这意味着我正在从一个没有帮助的角度来处理对象的生成。如果有不同的方法来生成遵循上述规则的对象,我愿意切换到它。
- 进一步回避这个问题,我看到其他 SO 帖子大致说 "If your object has such restrictions, then what happens when you receive an invalid object? Perhaps you need to rethink the way this object is consumed to better handle invalid cases." 例如,如果我能够在 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
}
}
这会产生一个对象,其中每个 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 @>
这三个属性在我的机器上都通过了。
问题
在 F# 中,我使用 FsCheck 生成一个对象(然后我在 Xunit 测试中使用它,但我可以完全在 Xunit 之外重新创建,所以我想我们可以忘记 Xunit)。 运行 FSI生成20次,
- 50% 的时间,生成 运行s 成功。
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
对象必须遵循以下规则才有效:
- 所有 InitEvents 必须在所有 RefEvents 之前出现
- 所有 InitEvents 字符串必须是唯一的
- 所有 RefEvent 名称必须有一个更早的对应 InitEvent
- 但如果某些 InitEvents 没有后来对应的 RefEvents 也没关系
- 但是多个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>
我已经检查过的内容
- 抱歉,忘记在原始问题中提及我已尝试在交互式中调试,并且由于行为不一致,很难找到。然而,当异常发生时,它似乎在我的生成器代码的末尾和生成样本的要求之间——当 FsCheck 正在生成时,它似乎正在尝试处理格式错误的序列。我进一步假设这是因为我对生成器的编码不正确。
- IndexOutOfRangeException using FsCheck 表示可能存在类似情况。我已经通过 Resharper 测试 运行ner 以及 Xunit 的控制台测试 运行ner[ 尝试了 运行ning 我的 Xunit 测试=102=] 基于上述简化所基于的实际测试。两个 运行ners 表现出相同的行为,所以问题出在其他地方。
- 其他 "How do I generate..." 问题,例如 In FsCheck, how to generate a test record with non-negative fields? and How does one generate a "complex" object in FsCheck? 涉及复杂性较低的对象的创建。第一个对获取我的代码有很大帮助,第二个给出了 Arb.convert 的急需示例,但 Arb.convert 没有如果我是从 "constant" 随机生成的名称列表转换过来的,请注意。这一切似乎都回到了那个——需要随机命名,然后从中提取以制作一组完整的 InitEvents 和一些 RefEvents 序列,两者都引用回 "constant" 列表,没有与我遇到的任何事情都不匹配。
- 我浏览了我能找到的大多数 FsCheck 生成器示例,包括 FsCheck 中包含的示例:https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs这些也不处理需要内部一致性的对象,而且似乎不适用于在这种情况下,尽管它们总体上是有帮助的。
- 也许这意味着我正在从一个没有帮助的角度来处理对象的生成。如果有不同的方法来生成遵循上述规则的对象,我愿意切换到它。
- 进一步回避这个问题,我看到其他 SO 帖子大致说 "If your object has such restrictions, then what happens when you receive an invalid object? Perhaps you need to rethink the way this object is consumed to better handle invalid cases." 例如,如果我能够在 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 } }
这会产生一个对象,其中每个 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 @>
这三个属性在我的机器上都通过了。