Compare commits

...

1 Commits

Author SHA1 Message Date
Patrick Stevens
9f5f22c644 Pull report generation into lib (#90) 2024-06-23 11:44:53 +01:00
6 changed files with 321 additions and 271 deletions

View File

@@ -0,0 +1,58 @@
namespace WoofWare.NUnitTestRunner
open System.Reflection
/// Attributes at the assembly level which control the behaviour of NUnit.
type AssemblyLevelAttributes =
{
/// How many tests can be running at once, if anything's running in parallel.
Parallelism : int option
/// Whether the tests in this assembly can be parallelised at all.
Parallelizable : Parallelizable<AssemblyParallelScope> option
}
[<RequireQualifiedAccess>]
module AssemblyLevelAttributes =
/// Reflectively obtain the values of any relevant assembly attributes.
let get (assy : Assembly) : AssemblyLevelAttributes =
((None, None), assy.CustomAttributes)
||> Seq.fold (fun (levelPar, par) attr ->
match attr.AttributeType.FullName with
| "NUnit.Framework.LevelOfParallelismAttribute" ->
let arg = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<int>
match levelPar with
| None -> (Some arg, par)
| Some existing ->
failwith $"Assembly %s{assy.Location} declares parallelism %i{arg} and also %i{existing}"
| "NUnit.Framework.NonParallelizableAttribute" ->
match levelPar with
| None -> (Some 1, par)
| Some existing ->
failwith
$"Assembly %s{assy.Location} declares non-parallelizable and also parallelism %i{existing}"
| "NUnit.Framework.ParallelizableAttribute" ->
match par with
| Some _ -> failwith "Got multiple Parallelize attributes in assembly"
| None ->
match attr.ConstructorArguments |> Seq.toList with
| [] -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures)
| [ v ] ->
match v.Value with
| :? int as v ->
match v with
| 512 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures)
| 256 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Children)
| 257 -> failwith "ParallelScope.All is invalid on assemblies; only Fixtures or Children"
| 1 -> failwith "ParallelScope.Self is invalid on assemblies; only Fixtures or Children"
| v -> failwith $"Could not recognise value %i{v} of parallel scope on assembly"
| v -> failwith $"Unexpectedly non-int value %O{v} of parallel scope on assembly"
| _ -> failwith "unexpectedly got multiple args to Parallelizable on assembly"
| _ -> levelPar, par
)
|> fun (par, canPar) ->
{
Parallelizable = canPar
Parallelism = par
}

View File

@@ -0,0 +1,243 @@
namespace WoofWare.NUnitTestRunner
open System
open System.Reflection
/// Methods for constructing TRX reports.
[<RequireQualifiedAccess>]
module BuildTrxReport =
/// Build a TRX report from the given results.
let build
(assy : Assembly)
(creationTime : DateTimeOffset)
(startTime : DateTimeOffset)
(results : FixtureRunResults list)
: TrxReport
=
let finishTime = DateTimeOffset.Now
let finishTimeHumanReadable = finishTime.ToString @"yyyy-MM-dd HH:mm:ss"
let nowMachine = finishTime.ToString @"yyyy-MM-dd_HH_mm_ss"
let testListId = Guid.NewGuid ()
let testDefinitions, testEntries =
results
|> List.collect (fun results -> results.IndividualTestRunMetadata)
|> List.map (fun (data, _) ->
let defn =
{
Name = data.TestName
Storage = assy.Location.ToLowerInvariant ()
Id = data.TestId
Execution =
{
Id = data.ExecutionId
}
TestMethod =
{
CodeBase = assy.Location
AdapterTypeName = Uri "executor://woofware/"
ClassName = data.ClassName
Name = data.TestName
}
}
let entry : TrxTestEntry =
{
TestListId = testListId
ExecutionId = data.ExecutionId
TestId = data.TestId
}
defn, entry
)
|> List.unzip
let hostname = Environment.MachineName
let settings =
{
Name = "default"
Id = Guid.NewGuid ()
Deployment =
{
RunDeploymentRoot = $"_%s{hostname}_%s{nowMachine}"
}
}
let testList : TrxTestListEntry =
{
Id = testListId
Name = "All"
}
let counters =
(TrxCounters.Zero, results)
// TODO: this is woefully inefficient
||> List.fold (fun counters results ->
let counters =
(counters, results.Failed)
||> List.fold (fun counters (_, _) ->
// TODO: the counters can be more specific about the failure mode
counters.AddFailed ()
)
let counters =
(counters, results.OtherFailures)
||> List.fold (fun counters _ ->
// TODO: the counters can be more specific about the failure mode
counters.AddFailed ()
)
(counters, results.Success)
||> List.fold (fun counters (_, success, _) ->
match success with
| TestMemberSuccess.Ok -> counters.AddPassed ()
| TestMemberSuccess.Ignored _
| TestMemberSuccess.Explicit _ -> counters.AddNotExecuted ()
| TestMemberSuccess.Inconclusive _ -> counters.AddInconclusive ()
)
)
// TODO: I'm sure we can do better than this; there's a whole range of possible
// states!
let outcome =
if counters.Failed > 0u then
TrxOutcome.Failed
else
TrxOutcome.Completed
let resultSummary : TrxResultsSummary =
{
Outcome = outcome
Counters = counters
Output =
{
StdOut = None
StdErr = None
ErrorInfo = None
}
RunInfos =
[
// TODO: capture stdout
]
}
let times : TrxReportTimes =
{
Creation = creationTime
Queuing = startTime
Start = startTime
Finish = finishTime
}
let magicGuid = Guid.Parse "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b"
let results =
results
|> List.collect (fun results -> results.IndividualTestRunMetadata)
|> List.map (fun (i, cause) ->
let exc =
match cause with
| Choice2Of3 _ -> None
| Choice1Of3 (TestMemberFailure.Malformed reasons) ->
{
StackTrace = None
Message = reasons |> String.concat "\n" |> Some
}
|> Some
| Choice1Of3 (TestMemberFailure.Failed fail)
| Choice1Of3 (TestMemberFailure.Failed fail)
| Choice1Of3 (TestMemberFailure.Failed fail) ->
((None, None), fail)
||> List.fold (fun (stackTrace, message) tf ->
match tf with
| TestFailure.TestFailed (UserMethodFailure.Threw (_, exc))
| TestFailure.SetUpFailed (UserMethodFailure.Threw (_, exc))
| TestFailure.TearDownFailed (UserMethodFailure.Threw (_, exc)) ->
let stackTrace =
match stackTrace with
| None -> (exc : Exception).ToString ()
| Some s -> s
(Some stackTrace, message)
| TestFailure.TestFailed (UserMethodFailure.ReturnedNonUnit (_, ret))
| TestFailure.SetUpFailed (UserMethodFailure.ReturnedNonUnit (_, ret))
| TestFailure.TearDownFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) ->
let newMessage = $"returned non-unit value %O{ret}"
let message =
match message with
| None -> newMessage
| Some message -> $"%s{message}\n%s{newMessage}"
(stackTrace, Some message)
)
|> fun (stackTrace, message) ->
{
StackTrace = stackTrace
Message = message
}
|> Some
| Choice3Of3 (UserMethodFailure.Threw (_, exc)) ->
{
StackTrace = (exc : Exception).ToString () |> Some
Message = None
}
|> Some
| Choice3Of3 (UserMethodFailure.ReturnedNonUnit (_, ret)) ->
{
Message = $"returned non-unit value %O{ret}" |> Some
StackTrace = None
}
|> Some
let outcome =
match cause with
| Choice1Of3 _ -> TrxTestOutcome.Failed
| Choice2Of3 TestMemberSuccess.Ok -> TrxTestOutcome.Passed
| Choice2Of3 (TestMemberSuccess.Inconclusive _) -> TrxTestOutcome.Inconclusive
| Choice2Of3 (TestMemberSuccess.Ignored _)
| Choice2Of3 (TestMemberSuccess.Explicit _) -> TrxTestOutcome.NotExecuted
// TODO: we can totally do better here, more fine-grained classification
| Choice3Of3 _ -> TrxTestOutcome.Failed
{
ExecutionId = i.ExecutionId
TestId = i.TestId
TestName = i.TestName
ComputerName = i.ComputerName
Duration = i.End - i.Start
StartTime = i.Start
EndTime = i.End
TestType = magicGuid
Outcome = outcome
TestListId = testListId
RelativeResultsDirectory = i.ExecutionId.ToString () // for some reason
Output =
match i.StdOut, i.StdErr, exc with
| None, None, None -> None
| stdout, stderr, exc ->
Some
{
TrxOutput.StdOut = stdout
StdErr = stderr
ErrorInfo = exc
}
}
)
{
Id = Guid.NewGuid ()
Name = $"@%s{hostname} %s{finishTimeHumanReadable}"
Times = times
Settings = settings
Results = results
TestDefinitions = testDefinitions
TestEntries = testEntries
TestLists = [ testList ]
ResultsSummary = resultSummary
}

View File

@@ -1,3 +1,11 @@
WoofWare.NUnitTestRunner.AssemblyLevelAttributes inherit obj, implements WoofWare.NUnitTestRunner.AssemblyLevelAttributes System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.AssemblyLevelAttributes System.IComparable, System.IComparable, System.Collections.IStructuralComparable
WoofWare.NUnitTestRunner.AssemblyLevelAttributes..ctor [constructor]: (int option, WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option)
WoofWare.NUnitTestRunner.AssemblyLevelAttributes.get_Parallelism [method]: unit -> int option
WoofWare.NUnitTestRunner.AssemblyLevelAttributes.get_Parallelizable [method]: unit -> WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option
WoofWare.NUnitTestRunner.AssemblyLevelAttributes.Parallelism [property]: [read-only] int option
WoofWare.NUnitTestRunner.AssemblyLevelAttributes.Parallelizable [property]: [read-only] WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option
WoofWare.NUnitTestRunner.AssemblyLevelAttributesModule inherit obj
WoofWare.NUnitTestRunner.AssemblyLevelAttributesModule.get [static method]: System.Reflection.Assembly -> WoofWare.NUnitTestRunner.AssemblyLevelAttributes
WoofWare.NUnitTestRunner.AssemblyParallelScope inherit obj, implements WoofWare.NUnitTestRunner.AssemblyParallelScope System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.AssemblyParallelScope System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 2 cases
WoofWare.NUnitTestRunner.AssemblyParallelScope+Tags inherit obj
WoofWare.NUnitTestRunner.AssemblyParallelScope+Tags.Children [static field]: int = 0
@@ -12,6 +20,8 @@ WoofWare.NUnitTestRunner.AssemblyParallelScope.get_Tag [method]: unit -> int
WoofWare.NUnitTestRunner.AssemblyParallelScope.IsChildren [property]: [read-only] bool
WoofWare.NUnitTestRunner.AssemblyParallelScope.IsFixtures [property]: [read-only] bool
WoofWare.NUnitTestRunner.AssemblyParallelScope.Tag [property]: [read-only] int
WoofWare.NUnitTestRunner.BuildTrxReport inherit obj
WoofWare.NUnitTestRunner.BuildTrxReport.build [static method]: System.Reflection.Assembly -> System.DateTimeOffset -> System.DateTimeOffset -> WoofWare.NUnitTestRunner.FixtureRunResults list -> WoofWare.NUnitTestRunner.TrxReport
WoofWare.NUnitTestRunner.ClassParallelScope inherit obj, implements WoofWare.NUnitTestRunner.ClassParallelScope System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.ClassParallelScope System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 4 cases
WoofWare.NUnitTestRunner.ClassParallelScope+Tags inherit obj
WoofWare.NUnitTestRunner.ClassParallelScope+Tags.All [static field]: int = 3

View File

@@ -36,6 +36,8 @@
<Compile Include="TestFixture.fs" />
<Compile Include="Xml.fs" />
<Compile Include="TrxReport.fs" />
<Compile Include="CreateTrxReport.fs" />
<Compile Include="AssemblyLevelAttributes.fs" />
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
@@ -51,7 +53,7 @@
<PackageReference Include="Myriad.SDK" Version="0.8.3" />
<PackageReference Include="WoofWare.Myriad.Plugins" Version="$(WoofWareMyriadPluginVersion)" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<MyriadSdkGenerator Include="$(NuGetPackageRoot)/woofware.myriad.plugins/$(WoofWareMyriadPluginVersion)/lib/net6.0/WoofWare.Myriad.Plugins.dll" />
</ItemGroup>

View File

@@ -1,5 +1,5 @@
{
"version": "0.13",
"version": "0.14",
"publicReleaseRefSpec": [
"^refs/heads/main$"
],
@@ -8,4 +8,4 @@
":/Directory.Build.props",
":/README.md"
]
}
}

View File

@@ -132,47 +132,10 @@ module Program =
let ctx = LoadContext (args.Dll, runtime, contexts)
let assy = ctx.LoadFromAssemblyPath args.Dll.FullName
let levelOfParallelism, par =
((None, None), assy.CustomAttributes)
||> Seq.fold (fun (levelPar, par) attr ->
match attr.AttributeType.FullName with
| "NUnit.Framework.LevelOfParallelismAttribute" ->
let arg = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox<int>
match levelPar with
| None -> (Some arg, par)
| Some existing ->
failwith $"Assembly %s{assy.Location} declares parallelism %i{arg} and also %i{existing}"
| "NUnit.Framework.NonParallelizableAttribute" ->
match levelPar with
| None -> (Some 1, par)
| Some existing ->
failwith
$"Assembly %s{assy.Location} declares non-parallelizable and also parallelism %i{existing}"
| "NUnit.Framework.ParallelizableAttribute" ->
match par with
| Some _ -> failwith "Got multiple Parallelize attributes in assembly"
| None ->
match attr.ConstructorArguments |> Seq.toList with
| [] -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures)
| [ v ] ->
match v.Value with
| :? int as v ->
match v with
| 512 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures)
| 256 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Children)
| 257 ->
failwith "ParallelScope.All is invalid on assemblies; only Fixtures or Children"
| 1 ->
failwith "ParallelScope.Self is invalid on assemblies; only Fixtures or Children"
| v -> failwith $"Could not recognise value %i{v} of parallel scope on assembly"
| v -> failwith $"Unexpectedly non-int value %O{v} of parallel scope on assembly"
| _ -> failwith "unexpectedly got multiple args to Parallelizable on assembly"
| _ -> levelPar, par
)
let attrs = AssemblyLevelAttributes.get assy
let levelOfParallelism =
match args.LevelOfParallelism, levelOfParallelism with
match args.LevelOfParallelism, attrs.Parallelism with
| None, None -> None
| Some taken, Some ignored ->
match args.Logging with
@@ -187,7 +150,7 @@ module Program =
let testFixtures = assy.ExportedTypes |> Seq.map TestFixture.parse |> Seq.toList
use par = new ParallelQueue (levelOfParallelism, par)
use par = new ParallelQueue (levelOfParallelism, attrs.Parallelizable)
let creationTime = DateTimeOffset.Now
@@ -206,233 +169,7 @@ module Program =
let results = results.Result |> Seq.concat |> List.ofSeq
let finishTime = DateTimeOffset.Now
let finishTimeHumanReadable = finishTime.ToString @"yyyy-MM-dd HH:mm:ss"
let nowMachine = finishTime.ToString @"yyyy-MM-dd_HH_mm_ss"
let testListId = Guid.NewGuid ()
let testDefinitions, testEntries =
results
|> List.collect (fun results -> results.IndividualTestRunMetadata)
|> List.map (fun (data, _) ->
let defn =
{
Name = data.TestName
Storage = assy.Location.ToLowerInvariant ()
Id = data.TestId
Execution =
{
Id = data.ExecutionId
}
TestMethod =
{
CodeBase = assy.Location
AdapterTypeName = Uri "executor://woofware/"
ClassName = data.ClassName
Name = data.TestName
}
}
let entry : TrxTestEntry =
{
TestListId = testListId
ExecutionId = data.ExecutionId
TestId = data.TestId
}
defn, entry
)
|> List.unzip
let hostname = Environment.MachineName
let settings =
{
Name = "default"
Id = Guid.NewGuid ()
Deployment =
{
RunDeploymentRoot = $"_%s{hostname}_%s{nowMachine}"
}
}
let testList : TrxTestListEntry =
{
Id = testListId
Name = "All"
}
let counters =
(TrxCounters.Zero, results)
// TODO: this is woefully inefficient
||> List.fold (fun counters results ->
let counters =
(counters, results.Failed)
||> List.fold (fun counters (_, _) ->
// TODO: the counters can be more specific about the failure mode
counters.AddFailed ()
)
let counters =
(counters, results.OtherFailures)
||> List.fold (fun counters _ ->
// TODO: the counters can be more specific about the failure mode
counters.AddFailed ()
)
(counters, results.Success)
||> List.fold (fun counters (_, success, _) ->
match success with
| TestMemberSuccess.Ok -> counters.AddPassed ()
| TestMemberSuccess.Ignored _
| TestMemberSuccess.Explicit _ -> counters.AddNotExecuted ()
| TestMemberSuccess.Inconclusive _ -> counters.AddInconclusive ()
)
)
// TODO: I'm sure we can do better than this; there's a whole range of possible
// states!
let outcome =
if counters.Failed > 0u then
TrxOutcome.Failed
else
TrxOutcome.Completed
let resultSummary : TrxResultsSummary =
{
Outcome = outcome
Counters = counters
Output =
{
StdOut = None
StdErr = None
ErrorInfo = None
}
RunInfos =
[
// TODO: capture stdout
]
}
let times : TrxReportTimes =
{
Creation = creationTime
Queuing = startTime
Start = startTime
Finish = finishTime
}
let magicGuid = Guid.Parse "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b"
let results =
results
|> List.collect (fun results -> results.IndividualTestRunMetadata)
|> List.map (fun (i, cause) ->
let exc =
match cause with
| Choice2Of3 _ -> None
| Choice1Of3 (TestMemberFailure.Malformed reasons) ->
{
StackTrace = None
Message = reasons |> String.concat "\n" |> Some
}
|> Some
| Choice1Of3 (TestMemberFailure.Failed fail)
| Choice1Of3 (TestMemberFailure.Failed fail)
| Choice1Of3 (TestMemberFailure.Failed fail) ->
((None, None), fail)
||> List.fold (fun (stackTrace, message) tf ->
match tf with
| TestFailure.TestFailed (UserMethodFailure.Threw (_, exc))
| TestFailure.SetUpFailed (UserMethodFailure.Threw (_, exc))
| TestFailure.TearDownFailed (UserMethodFailure.Threw (_, exc)) ->
let stackTrace =
match stackTrace with
| None -> (exc : Exception).ToString ()
| Some s -> s
(Some stackTrace, message)
| TestFailure.TestFailed (UserMethodFailure.ReturnedNonUnit (_, ret))
| TestFailure.SetUpFailed (UserMethodFailure.ReturnedNonUnit (_, ret))
| TestFailure.TearDownFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) ->
let newMessage = $"returned non-unit value %O{ret}"
let message =
match message with
| None -> newMessage
| Some message -> $"%s{message}\n%s{newMessage}"
(stackTrace, Some message)
)
|> fun (stackTrace, message) ->
{
StackTrace = stackTrace
Message = message
}
|> Some
| Choice3Of3 (UserMethodFailure.Threw (_, exc)) ->
{
StackTrace = (exc : Exception).ToString () |> Some
Message = None
}
|> Some
| Choice3Of3 (UserMethodFailure.ReturnedNonUnit (_, ret)) ->
{
Message = $"returned non-unit value %O{ret}" |> Some
StackTrace = None
}
|> Some
let outcome =
match cause with
| Choice1Of3 _ -> TrxTestOutcome.Failed
| Choice2Of3 TestMemberSuccess.Ok -> TrxTestOutcome.Passed
| Choice2Of3 (TestMemberSuccess.Inconclusive _) -> TrxTestOutcome.Inconclusive
| Choice2Of3 (TestMemberSuccess.Ignored _)
| Choice2Of3 (TestMemberSuccess.Explicit _) -> TrxTestOutcome.NotExecuted
// TODO: we can totally do better here, more fine-grained classification
| Choice3Of3 _ -> TrxTestOutcome.Failed
{
ExecutionId = i.ExecutionId
TestId = i.TestId
TestName = i.TestName
ComputerName = i.ComputerName
Duration = i.End - i.Start
StartTime = i.Start
EndTime = i.End
TestType = magicGuid
Outcome = outcome
TestListId = testListId
RelativeResultsDirectory = i.ExecutionId.ToString () // for some reason
Output =
match i.StdOut, i.StdErr, exc with
| None, None, None -> None
| stdout, stderr, exc ->
Some
{
TrxOutput.StdOut = stdout
StdErr = stderr
ErrorInfo = exc
}
}
)
let report : TrxReport =
{
Id = Guid.NewGuid ()
Name = $"@%s{hostname} %s{finishTimeHumanReadable}"
Times = times
Settings = settings
Results = results
TestDefinitions = testDefinitions
TestEntries = testEntries
TestLists = [ testList ]
ResultsSummary = resultSummary
}
let report = BuildTrxReport.build assy creationTime startTime results
match args.Trx with
| Some trxPath ->
@@ -442,7 +179,7 @@ module Program =
Console.Error.WriteLine $"Written TRX file: %s{trxPath.FullName}"
| None -> ()
match outcome with
match report.ResultsSummary.Outcome with
| TrxOutcome.Completed -> 0
| _ -> 1