问题
在F#中,我使用FsCheck生成一个对象(我在Xunit测试中使用它,但我可以完全在Xunit之外重新创建,所以我认为我们可以忘记Xunit).在FSI中运行20代,
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
Run Code Online (Sandbox Code Playgroud)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
Run Code Online (Sandbox Code Playgroud)情况
对象如下:
type Event =
| InitEvent of string
| RefEvent of string
type Stream = Event seq
Run Code Online (Sandbox Code Playgroud)
该对象必须遵循这些规则才有效:
工作解决方案
如果我让生成器调用一个返回有效对象并执行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>
Run Code Online (Sandbox Code Playgroud)
破碎的正确方式?
我似乎无法完全摆脱生成常量(需要存储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>
Run Code Online (Sandbox Code Playgroud)
我已经检查过的东西
谢谢!
编辑(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
}
}
Run Code Online (Sandbox Code Playgroud)
这产生了一个对象,其中每个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
}
}
Run Code Online (Sandbox Code Playgroud)
修改后的代码仍然表现出不一致的行为.有趣的是,在20次样本运行中,只有3次运行(从10次开始),而元素数量不足被抛出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
}
}
Run Code Online (Sandbox Code Playgroud)
这允许一些名称没有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
}
}
Run Code Online (Sandbox Code Playgroud)
非常感谢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 }
Run Code Online (Sandbox Code Playgroud)
关于如何生成唯一字符串的半官方答案是生成一个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]
Run Code Online (Sandbox Code Playgroud)
你现在zip可以:
> List.zip uniqueStrings sortValues;;
val it : (string * int) list = [("foo", 42); ("bar", 1337); ("baz", 42)]
Run Code Online (Sandbox Code Playgroud)
在第二个元素上对这样的序列进行排序会给你一个随机洗牌的列表,然后你就map可以只得到第一个元素:
> List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;;
val it : string list = ["foo"; "baz"; "bar"]
Run Code Online (Sandbox Code Playgroud)
由于所有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
Run Code Online (Sandbox Code Playgroud)
这三个属性都在我的机器上传递.
根据本答案评论中概述的更宽松的要求,这里是一个更新的生成器,它从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 }
Run Code Online (Sandbox Code Playgroud)
这一次,uniqueStrings是一组非空字符串.
您可以使用Seq.map RefEvent生成所有有效的序列RefEvent基于值uniqueStrings,然后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 @>
Run Code Online (Sandbox Code Playgroud)
这三个属性都在我的机器上传递.