Properly deal with attributes (#14)

This commit is contained in:
Patrick Stevens
2024-06-04 23:27:27 +01:00
committed by GitHub
parent 2f9772007a
commit 31b76f2f97

View File

@@ -34,28 +34,33 @@ type SingleTestMethod =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module SingleTestMethod = module SingleTestMethod =
let parse (parentCategories : string list) (method : MethodInfo) : SingleTestMethod option = let parse
let isTest, hasSource, hasData, modifiers, categories, repeat, comb = (parentCategories : string list)
((false, None, None, [], [], None, None), method.CustomAttributes) (method : MethodInfo)
||> Seq.fold (fun (isTest, hasSource, hasData, mods, cats, repeat, comb) attr -> (attrs : CustomAttributeData list)
: SingleTestMethod option * CustomAttributeData list
=
let remaining, isTest, hasSource, hasData, modifiers, categories, repeat, comb =
(([], false, None, None, [], [], None, None), attrs)
||> List.fold (fun (remaining, isTest, hasSource, 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"
(true, hasSource, hasData, mods, cats, repeat, comb) (remaining, true, hasSource, 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 -> (isTest, hasSource, Some [ List.ofSeq args ], mods, cats, repeat, comb) | None -> (remaining, isTest, hasSource, Some [ List.ofSeq args ], mods, cats, repeat, comb)
| Some existing -> | Some existing ->
(isTest, hasSource, Some ((List.ofSeq args) :: existing), mods, cats, repeat, comb) (remaining, isTest, hasSource, 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 match hasSource with
| None -> (isTest, Some arg, hasData, mods, cats, repeat, comb) | None -> (remaining, isTest, Some arg, hasData, mods, cats, repeat, comb)
| Some existing -> | Some existing ->
failwith failwith
$"Unexpectedly got multiple different sources for test %s{method.Name} (%s{existing}, %s{arg})" $"Unexpectedly got multiple different sources for test %s{method.Name} (%s{existing}, %s{arg})"
@@ -65,78 +70,84 @@ module SingleTestMethod =
|> Seq.tryHead |> Seq.tryHead
|> Option.map (_.Value >> unbox<string>) |> Option.map (_.Value >> unbox<string>)
(isTest, hasSource, hasData, (Modifier.Explicit reason) :: mods, cats, repeat, comb) (remaining, isTest, hasSource, 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>)
(isTest, hasSource, hasData, (Modifier.Ignored reason) :: mods, cats, repeat, comb) (remaining, isTest, hasSource, 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>
(isTest, hasSource, hasData, mods, category :: cats, repeat, comb) (remaining, isTest, hasSource, 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>
(isTest, hasSource, hasData, mods, cats, Some repeat, comb) (remaining, isTest, hasSource, 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 -> (isTest, hasSource, hasData, mods, cats, repeat, Some Combinatorial.Combinatorial) | None ->
(remaining, isTest, hasSource, 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 -> (isTest, hasSource, hasData, mods, cats, repeat, Some Combinatorial.Sequential) | None ->
(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}"
| _ -> (isTest, hasSource, hasData, mods, cats, repeat, comb) | _ -> (attr :: remaining, isTest, hasSource, hasData, mods, cats, repeat, comb)
) )
match isTest, hasSource, hasData, modifiers, categories, repeat, comb with let test =
| _, Some _, Some _, _, _, _, _ -> match isTest, hasSource, hasData, modifiers, categories, repeat, comb with
failwith $"Test %s{method.Name} unexpectedly has both TestData and TestCaseSource; not currently supported" | _, Some _, Some _, _, _, _, _ ->
| false, None, None, [], _, _, _ -> None failwith
| _, Some source, None, mods, categories, repeat, comb -> $"Test %s{method.Name} unexpectedly has both TestData and TestCaseSource; not currently supported"
{ | false, None, None, [], _, _, _ -> None
Kind = TestKind.Source source | _, Some source, None, mods, categories, repeat, comb ->
Method = method {
Modifiers = mods Kind = TestKind.Source source
Categories = categories @ parentCategories Method = method
Repeat = repeat Modifiers = mods
Combinatorial = comb Categories = categories @ parentCategories
} Repeat = repeat
|> Some Combinatorial = comb
| _, None, Some data, mods, categories, repeat, comb -> }
{ |> Some
Kind = TestKind.Data data | _, None, Some data, mods, categories, repeat, comb ->
Method = method {
Modifiers = mods Kind = TestKind.Data data
Categories = categories @ parentCategories Method = method
Repeat = repeat Modifiers = mods
Combinatorial = comb Categories = categories @ parentCategories
} Repeat = repeat
|> Some Combinatorial = comb
| true, None, None, mods, categories, repeat, comb -> }
{ |> Some
Kind = TestKind.Single | true, None, None, mods, categories, repeat, comb ->
Method = method {
Modifiers = mods Kind = TestKind.Single
Categories = categories @ parentCategories Method = method
Repeat = repeat Modifiers = mods
Combinatorial = comb Categories = categories @ parentCategories
} Repeat = repeat
|> Some Combinatorial = comb
| false, None, None, _ :: _, _, _, _ -> }
failwith |> Some
$"Unexpectedly got test modifiers but no test settings on '%s{method.Name}', which you probably didn't intend." | false, None, None, _ :: _, _, _, _ ->
failwith
$"Unexpectedly got test modifiers but no test settings on '%s{method.Name}', which you probably didn't intend."
test, remaining
type TestFixture = type TestFixture =
{ {
@@ -391,47 +402,73 @@ module TestFixture =
(TestFixture.Empty parentType.Name, parentType.GetRuntimeMethods ()) (TestFixture.Empty parentType.Name, parentType.GetRuntimeMethods ())
||> Seq.fold (fun state mi -> ||> Seq.fold (fun state mi ->
if ((state, []), mi.CustomAttributes)
mi.CustomAttributes ||> Seq.fold (fun (state, unrecognisedAttrs) attr ->
|> Seq.exists (fun attr -> attr.AttributeType.FullName = "NUnit.Framework.OneTimeSetUpAttribute") match attr.AttributeType.FullName with
then | "NUnit.Framework.OneTimeSetUpAttribute" ->
match state.OneTimeSetUp with match state.OneTimeSetUp with
| None -> | Some _existing -> failwith "Multiple OneTimeSetUp methods found"
| None ->
{ state with
OneTimeSetUp = Some mi
},
unrecognisedAttrs
| "NUnit.Framework.OneTimeTearDownAttribute" ->
match state.OneTimeTearDown with
| Some _existing -> failwith "Multiple OneTimeTearDown methods found"
| None ->
{ state with
OneTimeTearDown = Some mi
},
unrecognisedAttrs
| "NUnit.Framework.TearDownAttribute" ->
{ state with { state with
OneTimeSetUp = Some mi TearDown = mi :: state.TearDown
} },
| Some _existing -> failwith "Multiple OneTimeSetUp methods found" unrecognisedAttrs
elif | "NUnit.Framework.SetUpAttribute" ->
mi.CustomAttributes
|> Seq.exists (fun attr -> attr.AttributeType.FullName = "NUnit.Framework.OneTimeTearDownAttribute")
then
match state.OneTimeTearDown with
| None ->
{ state with { state with
OneTimeTearDown = Some mi SetUp = mi :: state.SetUp
} },
| Some _existing -> failwith "Multiple OneTimeTearDown methods found" unrecognisedAttrs
elif | "NUnit.Framework.TestFixtureSetUpAttribute" ->
mi.CustomAttributes failwith "TestFixtureSetUp is not supported (upstream has deprecated it; use OneTimeSetUp)"
|> Seq.exists (fun attr -> attr.AttributeType.FullName = "NUnit.Framework.TearDownAttribute") | "NUnit.Framework.TestFixtureTearDownAttribute" ->
then failwith "TestFixtureTearDown is not supported (upstream has deprecated it; use OneTimeTearDown)"
{ state with | "NUnit.Framework.RetryAttribute" ->
TearDown = mi :: state.TearDown failwith "RetryAttribute is not supported. Don't write flaky tests."
} | "NUnit.Framework.RandomAttribute" ->
elif failwith "RandomAttribute is not supported. Use a property-based testing framework like FsCheck."
mi.CustomAttributes | "NUnit.Framework.AuthorAttribute"
|> Seq.exists (fun attr -> attr.AttributeType.FullName = "NUnit.Framework.SetUpAttribute") | "NUnit.Framework.CultureAttribute"
then | "NUnit.Framework.DescriptionAttribute" ->
{ state with // ignoring for now: metadata only
SetUp = mi :: state.SetUp state, unrecognisedAttrs
} | _ -> state, attr :: unrecognisedAttrs
else )
match SingleTestMethod.parse categories mi with |> fun (state, unrecognised) ->
| Some test -> let state, unrecognised =
{ state with match SingleTestMethod.parse categories mi unrecognised with
Tests = test :: state.Tests | Some test, unrecognised ->
} { state with
| None -> state Tests = test :: state.Tests
},
unrecognised
| None, unrecognised -> state, unrecognised
unrecognised
|> List.filter (fun attr ->
attr.AttributeType.FullName.StartsWith ("NUnit.Framework.", StringComparison.Ordinal)
)
|> function
| [] -> ()
| unrecognised ->
unrecognised
|> Seq.map (fun x -> x.AttributeType.FullName)
|> String.concat ", "
|> failwithf "Unrecognised attributes: %s"
state
) )
module Program = module Program =