【发布时间】: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
对象必须遵循以下规则才有效:
- 所有 InitEvents 必须在所有 RefEvents 之前
- 所有 InitEvents 字符串必须是唯一的
- 所有 RefEvent 名称必须有一个更早的对应 InitEvent
- 但如果某些 InitEvents 后面没有对应的 RefEvents 也没关系
- 但如果多个 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,我跑了一圈,不知道从哪里下车! :)