F# 使用FsCheck生成复杂对象时IEnumerable ArgumentException不一致
问题 在F#中,我使用FsCheck生成一个对象(然后我在Xunit测试中使用它,但是我可以完全在Xunit之外重新创建,所以我想我们可以忘记Xunit)。在FSI中运行20次新一代F# 使用FsCheck生成复杂对象时IEnumerable ArgumentException不一致,f#,fscheck,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.SeqModul
- 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
该对象必须遵循以下规则才能有效: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
- 所有InitEvents必须位于所有RefEvents之前
- 所有InitEvents字符串必须是唯一的
- 所有RefEvent名称必须具有较早的对应InitEvent
- 但是,如果某些InitEvents没有稍后相应的RefEvents,也可以
- 但是,如果多个RefEvents具有相同的名称,则没有关系 工作环境 如果我让生成器调用一个返回有效对象的函数并执行Gen.constant(函数),我就不会遇到异常,但这不是运行FsCheck的方式!:)
//
///这是一个100%可靠的非发电机等效物
///
让随机流大小=
//样本的有效名称
让name=Gen.sample size Arb.generate |>List.distinct
//初始化事件
让initEvents=names |>List.map(有趣的名字->名字|>InitEvent)
//参考事件
让createRefEvent name=name |>RefEvent
让genRefEvent=createRefEvent Gen.elements名称
设refEvents=Gen.sample size genRefEvent
//结合
Seq.append initEvents refEvents
类型MyGenerator=
静态成员流()={
使用
覆盖x.Generator=Gen.size(乐趣大小->Gen.constant(随机流大小))
}
//反复运行以下两行始终有效
仲裁寄存器()
设foo=Gen.sample 10 10 Arb.generate
正确的道路?
我似乎无法完全避免生成常量(需要在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>
类型MyGenerators=
静态成员流()={
使用
覆盖x.发电机=发电机尺寸(有趣的尺寸->
//样本的有效名称
让name=Gen.sample size Arb.generate |>List.distinct
//生成初始化
让genInits=Gen.constant(name |>List.map InitEvent)|>Gen.map List.toSeq
//生成引用
让makeRef name=name |>RefEvent
让genName=Gen.elements名称
让genRef=makeRef-genName
Seq.append genInits(genRef |>Gen.listOf)
)
}
//重复运行以下两行会导致不一致的错误
//如果我不重新注册我的发电机,我总是得到相同的样本。
//这是因为FsCheck试图确定吗?
仲裁寄存器()
设foo=Gen.sample 10 10 Arb.generate
我已经检查的内容
- 抱歉,在最初的问题中忘记提到,我试图在Interactive中调试,由于行为不一致,所以有点难以追踪。然而,当异常出现时,它似乎介于生成器代码的结尾和生成样本的要求之间——而FsCheck正在执行生成但是,它似乎试图处理一个格式错误的序列。我进一步假设这是因为我对生成器的编码不正确
- 这表明可能存在类似的情况。我已尝试在上述简化所基于的真实测试中,通过Resharper test runner以及Xunit的console test runner运行Xunit测试。两个运行程序表现出相同的行为,因此问题出在其他地方
- 其他“如何生成…”问题,如和处理创建复杂度较低的对象。第一个问题对获取我的代码有很大帮助,第二个问题提供了一个急需的示例Arb.convert,但是如果我从“常量”转换,则Arb.convert没有意义随机生成的名称列表。这一切似乎都回到了这一点——需要生成随机名称,然后从中提取以生成一组完整的InitEvents,以及一些引用“常量”列表的RefEvents序列,这与我遇到的任何内容都不匹配
- 我已经浏览了我能找到的大多数FsCheck生成器示例,包括FsCheck中包含的示例:这些示例也不处理需要内部一致性的对象,并且似乎不适用于这种情况,尽管它们总体上很有帮助
- 也许这意味着我正在从一个无用的角度来处理对象的生成。如果有一种不同的方法来生成一个遵循上述规则的对象,我愿意切换到它
- 进一步远离这个问题,我看到其他SO帖子大致上说“如果你的对象有这样的限制,那么当你收到一个无效的对象时会发生什么?也许你需要重新思考这个对象的使用方式,以便更好地处理无效的情况。”例如,如果我能够动态初始化RefEvent中一个以前从未见过的名称,那么首先给出InitEvent的所有需求都将消失——问题优雅地简化为一系列随机名称的RefEvent。我对这种解决方案持开放态度,但它需要一些返工——从长远来看,可能是这样的值得。同时,问题仍然是,如何使用FsCheck可靠地生成符合上述规则的复杂对象
- Mark Seemann答案中的代码是有效的,但产生的obje略有不同
/// <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>
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>
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 } }
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 } }
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 } }
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 } }
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 }
> let uniqueStrings = ["foo"; "bar"; "baz"];; val uniqueStrings : string list = ["foo"; "bar"; "baz"] > let sortValues = [42; 1337; 42];; val sortValues : int list = [42; 1337; 42]
> List.zip uniqueStrings sortValues;; val it : (string * int) list = [("foo", 42); ("bar", 1337); ("baz", 42)]
> List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;; val it : string list = ["foo"; "baz"; "bar"]
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
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 }
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 @>