Multiple TestCaseSources (#26)

This commit is contained in:
Patrick Stevens
2024-06-05 17:27:17 +01:00
committed by GitHub
parent 298dca94ea
commit f6e907395c
4 changed files with 91 additions and 80 deletions

View File

@@ -9,6 +9,27 @@ module TestCaseData =
let dataSourceRaw = [ 3, "hi", [| 4.0 |] ; -10, "bye", null ] let dataSourceRaw = [ 3, "hi", [| 4.0 |] ; -10, "bye", null ]
let dataSource = dataSourceRaw |> List.map TestCaseData
[<TestCaseSource(nameof dataSource)>]
let ``Consume test data`` (i : int, s : string, arr : float[]) =
lock testCasesSeen (fun () -> testCasesSeen.Add (i, s, arr))
let multipleSources = ResizeArray ()
let dataSource2Raw = [ 5, "egg", [| -1.3 ; 4.5 |] ; 100, "mycroft", [||] ]
let dataSource2 = dataSource2Raw |> List.map TestCaseData
[<TestCaseSource(nameof dataSource)>]
[<TestCaseSource(nameof dataSource2)>]
let ``Consume test data from multiple sources`` (i : int, s : string, arr : float[]) =
lock multipleSources (fun () -> multipleSources.Add (i, s, arr))
let optional = [ Some "hi" ; None ] |> List.map TestCaseData
[<TestCaseSource(nameof optional)>]
let ``Consume options`` (s : string option) : unit = s |> shouldEqual s
[<OneTimeTearDown>] [<OneTimeTearDown>]
let tearDown () = let tearDown () =
testCasesSeen testCasesSeen
@@ -16,13 +37,7 @@ module TestCaseData =
|> List.sortBy (fun (a, _, _) -> a) |> List.sortBy (fun (a, _, _) -> a)
|> shouldEqual (dataSourceRaw |> List.sortBy (fun (a, _, _) -> a)) |> shouldEqual (dataSourceRaw |> List.sortBy (fun (a, _, _) -> a))
let dataSource = dataSourceRaw |> List.map TestCaseData multipleSources
|> Seq.toList
[<TestCaseSource(nameof dataSource)>] |> List.sortBy (fun (a, _, _) -> a)
let ``Consume test data`` (i : int, s : string, arr : float[]) = |> shouldEqual ((dataSourceRaw @ dataSource2Raw) |> List.sortBy (fun (a, _, _) -> a))
lock testCasesSeen (fun () -> testCasesSeen.Add (i, s, arr))
let optional = [ Some "hi" ; None ] |> List.map TestCaseData
[<TestCaseSource(nameof optional)>]
let ``Consume options`` (s : string option) : unit = s |> shouldEqual s

View File

@@ -10,7 +10,7 @@ type Modifier =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
type TestKind = type TestKind =
| Single | Single
| Source of string | Source of string list
| Data of obj list list | Data of obj list list
type Combinatorial = type Combinatorial =

View File

@@ -12,82 +12,77 @@ module SingleTestMethod =
(attrs : CustomAttributeData list) (attrs : CustomAttributeData list)
: SingleTestMethod option * CustomAttributeData list : SingleTestMethod option * CustomAttributeData list
= =
let remaining, isTest, hasSource, hasData, modifiers, categories, repeat, comb = let remaining, isTest, sources, hasData, modifiers, categories, repeat, comb =
(([], false, None, None, [], [], None, None), attrs) (([], false, [], None, [], [], None, None), attrs)
||> List.fold (fun (remaining, isTest, hasSource, hasData, mods, cats, repeat, comb) attr -> ||> List.fold (fun (remaining, isTest, sources, hasData, mods, cats, repeat, comb) attr ->
match attr.AttributeType.FullName with match attr.AttributeType.FullName with
| "NUnit.Framework.TestAttribute" -> | "NUnit.Framework.TestAttribute" ->
if attr.ConstructorArguments.Count > 0 then if attr.ConstructorArguments.Count > 0 then
failwith "Unexpectedly got arguments to the Test attribute" failwith "Unexpectedly got arguments to the Test attribute"
(remaining, true, hasSource, hasData, mods, cats, repeat, comb) (remaining, true, sources, hasData, mods, cats, repeat, comb)
| "NUnit.Framework.TestCaseAttribute" -> | "NUnit.Framework.TestCaseAttribute" ->
let args = attr.ConstructorArguments |> Seq.map _.Value |> Seq.toList let args = attr.ConstructorArguments |> Seq.map _.Value |> Seq.toList
match hasData with match hasData with
| None -> (remaining, isTest, hasSource, Some [ List.ofSeq args ], mods, cats, repeat, comb) | None -> (remaining, isTest, sources, Some [ List.ofSeq args ], mods, cats, repeat, comb)
| Some existing -> | Some existing ->
(remaining, isTest, hasSource, Some ((List.ofSeq args) :: existing), mods, cats, repeat, comb) (remaining, isTest, sources, Some ((List.ofSeq args) :: existing), mods, cats, repeat, comb)
| "NUnit.Framework.TestCaseSourceAttribute" -> | "NUnit.Framework.TestCaseSourceAttribute" ->
let arg = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<string> let arg = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<string>
match hasSource with (remaining, isTest, arg :: sources, hasData, mods, cats, repeat, comb)
| None -> (remaining, isTest, Some arg, hasData, mods, cats, repeat, comb)
| Some existing ->
failwith
$"Unexpectedly got multiple different sources for test %s{method.Name} (%s{existing}, %s{arg})"
| "NUnit.Framework.ExplicitAttribute" -> | "NUnit.Framework.ExplicitAttribute" ->
let reason = let reason =
attr.ConstructorArguments attr.ConstructorArguments
|> Seq.tryHead |> Seq.tryHead
|> Option.map (_.Value >> unbox<string>) |> Option.map (_.Value >> unbox<string>)
(remaining, isTest, hasSource, hasData, (Modifier.Explicit reason) :: mods, cats, repeat, comb) (remaining, isTest, sources, hasData, (Modifier.Explicit reason) :: mods, cats, repeat, comb)
| "NUnit.Framework.IgnoreAttribute" -> | "NUnit.Framework.IgnoreAttribute" ->
let reason = let reason =
attr.ConstructorArguments attr.ConstructorArguments
|> Seq.tryHead |> Seq.tryHead
|> Option.map (_.Value >> unbox<string>) |> Option.map (_.Value >> unbox<string>)
(remaining, isTest, hasSource, hasData, (Modifier.Ignored reason) :: mods, cats, repeat, comb) (remaining, isTest, sources, hasData, (Modifier.Ignored reason) :: mods, cats, repeat, comb)
| "NUnit.Framework.CategoryAttribute" -> | "NUnit.Framework.CategoryAttribute" ->
let category = let category =
attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<string> attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<string>
(remaining, isTest, hasSource, hasData, mods, category :: cats, repeat, comb) (remaining, isTest, sources, hasData, mods, category :: cats, repeat, comb)
| "NUnit.Framework.RepeatAttribute" -> | "NUnit.Framework.RepeatAttribute" ->
match repeat with match repeat with
| Some _ -> failwith $"Got RepeatAttribute multiple times on %s{method.Name}" | Some _ -> failwith $"Got RepeatAttribute multiple times on %s{method.Name}"
| None -> | None ->
let repeat = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<int> let repeat = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<int>
(remaining, isTest, hasSource, hasData, mods, cats, Some repeat, comb) (remaining, isTest, sources, hasData, mods, cats, Some repeat, comb)
| "NUnit.Framework.CombinatorialAttribute" -> | "NUnit.Framework.CombinatorialAttribute" ->
match comb with match comb with
| Some _ -> | Some _ ->
failwith $"Got CombinatorialAttribute or SequentialAttribute multiple times on %s{method.Name}" failwith $"Got CombinatorialAttribute or SequentialAttribute multiple times on %s{method.Name}"
| None -> | None ->
(remaining, isTest, hasSource, hasData, mods, cats, repeat, Some Combinatorial.Combinatorial) (remaining, isTest, sources, hasData, mods, cats, repeat, Some Combinatorial.Combinatorial)
| "NUnit.Framework.SequentialAttribute" -> | "NUnit.Framework.SequentialAttribute" ->
match comb with match comb with
| Some _ -> | Some _ ->
failwith $"Got CombinatorialAttribute or SequentialAttribute multiple times on %s{method.Name}" failwith $"Got CombinatorialAttribute or SequentialAttribute multiple times on %s{method.Name}"
| None -> | None -> (remaining, isTest, sources, hasData, mods, cats, repeat, Some Combinatorial.Sequential)
(remaining, isTest, hasSource, hasData, mods, cats, repeat, Some Combinatorial.Sequential)
| s when s.StartsWith ("NUnit.Framework", StringComparison.Ordinal) -> | s when s.StartsWith ("NUnit.Framework", StringComparison.Ordinal) ->
failwith $"Unrecognised attribute on function %s{method.Name}: %s{attr.AttributeType.FullName}" failwith $"Unrecognised attribute on function %s{method.Name}: %s{attr.AttributeType.FullName}"
| _ -> (attr :: remaining, isTest, hasSource, hasData, mods, cats, repeat, comb) | _ -> (attr :: remaining, isTest, sources, hasData, mods, cats, repeat, comb)
) )
let test = let test =
match isTest, hasSource, hasData, modifiers, categories, repeat, comb with match isTest, sources, hasData, modifiers, categories, repeat, comb with
| _, Some _, Some _, _, _, _, _ -> | _, _ :: _, Some _, _, _, _, _ ->
failwith failwith
$"Test %s{method.Name} unexpectedly has both TestData and TestCaseSource; not currently supported" $"Test %s{method.Name} unexpectedly has both TestData and TestCaseSource; not currently supported"
| false, None, None, [], _, _, _ -> None | false, [], None, [], _, _, _ -> None
| _, Some source, None, mods, categories, repeat, comb -> | _, _ :: _, None, mods, categories, repeat, comb ->
{ {
Kind = TestKind.Source source Kind = TestKind.Source sources
Method = method Method = method
Modifiers = mods Modifiers = mods
Categories = categories @ parentCategories Categories = categories @ parentCategories
@@ -95,7 +90,7 @@ module SingleTestMethod =
Combinatorial = comb Combinatorial = comb
} }
|> Some |> Some
| _, None, Some data, mods, categories, repeat, comb -> | _, [], Some data, mods, categories, repeat, comb ->
{ {
Kind = TestKind.Data data Kind = TestKind.Data data
Method = method Method = method
@@ -105,7 +100,7 @@ module SingleTestMethod =
Combinatorial = comb Combinatorial = comb
} }
|> Some |> Some
| true, None, None, mods, categories, repeat, comb -> | true, [], None, mods, categories, repeat, comb ->
{ {
Kind = TestKind.Single Kind = TestKind.Single
Method = method Method = method
@@ -115,7 +110,7 @@ module SingleTestMethod =
Combinatorial = comb Combinatorial = comb
} }
|> Some |> Some
| false, None, None, _ :: _, _, _, _ -> | false, [], None, _ :: _, _, _, _ ->
failwith failwith
$"Unexpectedly got test modifiers but no test settings on '%s{method.Name}', which you probably didn't intend." $"Unexpectedly got test modifiers but no test settings on '%s{method.Name}', which you probably didn't intend."

View File

@@ -192,52 +192,53 @@ module TestFixture =
|> TestMemberFailure.Malformed |> TestMemberFailure.Malformed
|> Error |> Error
|> Seq.singleton |> Seq.singleton
| TestKind.Source s, None -> | TestKind.Source sources, None ->
let args =
test.Method.DeclaringType.GetProperty (
s,
BindingFlags.Public
||| BindingFlags.NonPublic
||| BindingFlags.Instance
||| BindingFlags.Static
)
seq { seq {
// Might not be an IEnumerable of a reference type. for source in sources do
// Concretely, `FSharpList<HttpStatusCode> :> IEnumerable<obj>` fails. let args =
for arg in args.GetValue (null : obj) :?> System.Collections.IEnumerable do test.Method.DeclaringType.GetProperty (
yield source,
match arg with BindingFlags.Public
| :? Tuple<obj, obj> as (a, b) -> ||| BindingFlags.NonPublic
runOne setUp tearDown test.Method containingObject [| a ; b |] ||| BindingFlags.Instance
| :? Tuple<obj, obj, obj> as (a, b, c) -> ||| BindingFlags.Static
runOne setUp tearDown test.Method containingObject [| a ; b ; c |] )
| :? Tuple<obj, obj, obj, obj> as (a, b, c, d) ->
runOne setUp tearDown test.Method containingObject [| a ; b ; c ; d |]
| arg ->
let argTy = arg.GetType ()
if argTy.FullName = "NUnit.Framework.TestCaseData" then // Might not be an IEnumerable of a reference type.
let argsMem = // Concretely, `FSharpList<HttpStatusCode> :> IEnumerable<obj>` fails.
argTy.GetMethod ( for arg in args.GetValue (null : obj) :?> System.Collections.IEnumerable do
"get_Arguments", yield
BindingFlags.Public match arg with
||| BindingFlags.Instance | :? Tuple<obj, obj> as (a, b) ->
||| BindingFlags.FlattenHierarchy runOne setUp tearDown test.Method containingObject [| a ; b |]
) | :? Tuple<obj, obj, obj> as (a, b, c) ->
runOne setUp tearDown test.Method containingObject [| a ; b ; c |]
| :? Tuple<obj, obj, obj, obj> as (a, b, c, d) ->
runOne setUp tearDown test.Method containingObject [| a ; b ; c ; d |]
| arg ->
let argTy = arg.GetType ()
if isNull argsMem then if argTy.FullName = "NUnit.Framework.TestCaseData" then
failwith "Unexpectedly could not call `.Arguments` on TestCaseData" let argsMem =
argTy.GetMethod (
"get_Arguments",
BindingFlags.Public
||| BindingFlags.Instance
||| BindingFlags.FlattenHierarchy
)
runOne if isNull argsMem then
setUp failwith "Unexpectedly could not call `.Arguments` on TestCaseData"
tearDown
test.Method runOne
containingObject setUp
(argsMem.Invoke (arg, [||]) |> unbox<obj[]>) tearDown
else test.Method
runOne setUp tearDown test.Method containingObject [| arg |] containingObject
|> normaliseError (argsMem.Invoke (arg, [||]) |> unbox<obj[]>)
else
runOne setUp tearDown test.Method containingObject [| arg |]
|> normaliseError
} }
) )
|> Seq.concat |> Seq.concat