mirror of
https://github.com/Smaug123/unofficial-nunit-runner
synced 2025-10-06 01:48:38 +00:00
Compare commits
8 Commits
WoofWare.N
...
failing-te
Author | SHA1 | Date | |
---|---|---|---|
|
71d340d33a | ||
|
c09eb93b5e | ||
|
fb945c04ac | ||
|
85cd116d52 | ||
|
8e7c54cc83 | ||
|
378a0169f8 | ||
|
870804d6ef | ||
|
9f5f22c644 |
@@ -1,40 +1,40 @@
|
||||
root=true
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
trim_trailing_whitespace=true
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# ReSharper properties
|
||||
resharper_xml_indent_size=2
|
||||
resharper_xml_max_line_length=100
|
||||
resharper_xml_tab_width=2
|
||||
resharper_xml_indent_size = 2
|
||||
resharper_xml_max_line_length = 100
|
||||
resharper_xml_tab_width = 2
|
||||
|
||||
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{fs,fsi}]
|
||||
fsharp_bar_before_discriminated_union_declaration=true
|
||||
fsharp_space_before_uppercase_invocation=true
|
||||
fsharp_space_before_class_constructor=true
|
||||
fsharp_space_before_member=true
|
||||
fsharp_space_before_colon=true
|
||||
fsharp_space_before_semicolon=true
|
||||
fsharp_multiline_bracket_style=aligned
|
||||
fsharp_newline_between_type_definition_and_members=true
|
||||
fsharp_align_function_signature_to_indentation=true
|
||||
fsharp_alternative_long_member_definitions=true
|
||||
fsharp_multi_line_lambda_closing_newline=true
|
||||
fsharp_experimental_keep_indent_in_branch=true
|
||||
fsharp_max_value_binding_width=80
|
||||
fsharp_max_record_width=0
|
||||
max_line_length=120
|
||||
end_of_line=lf
|
||||
fsharp_bar_before_discriminated_union_declaration = true
|
||||
fsharp_space_before_uppercase_invocation = true
|
||||
fsharp_space_before_class_constructor = true
|
||||
fsharp_space_before_member = true
|
||||
fsharp_space_before_colon = true
|
||||
fsharp_space_before_semicolon = true
|
||||
fsharp_multiline_bracket_style = aligned
|
||||
fsharp_newline_between_type_definition_and_members = true
|
||||
fsharp_align_function_signature_to_indentation = true
|
||||
fsharp_alternative_long_member_definitions = true
|
||||
fsharp_multi_line_lambda_closing_newline = true
|
||||
fsharp_experimental_keep_indent_in_branch = true
|
||||
fsharp_max_value_binding_width = 80
|
||||
fsharp_max_record_width = 0
|
||||
max_line_length = 120
|
||||
end_of_line = lf
|
||||
|
||||
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=2
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
46
.github/workflows/dotnet.yaml
vendored
46
.github/workflows/dotnet.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Build
|
||||
run: 'nix develop --command dotnet build --no-restore --configuration ${{matrix.config}}'
|
||||
- name: Test
|
||||
run: 'nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}'
|
||||
run: 'nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}} --framework net8.0'
|
||||
|
||||
selftest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -71,7 +71,49 @@ jobs:
|
||||
- name: Build
|
||||
run: 'nix develop --command dotnet build --no-restore --configuration Release'
|
||||
- name: Test using self
|
||||
run: 'nix develop --command dotnet exec ./WoofWare.NUnitTestRunner/bin/Release/net8.0/WoofWare.NUnitTestRunner.dll ./Consumer/bin/Release/net8.0/Consumer.dll --trx TrxOut/out.trx'
|
||||
run: 'nix develop --command dotnet exec ./WoofWare.NUnitTestRunner/bin/Release/net6.0/WoofWare.NUnitTestRunner.dll ./Consumer/bin/Release/net8.0/Consumer.dll --trx TrxOut/out.trx'
|
||||
- name: Parse Trx files
|
||||
uses: NasAmin/trx-parser@v0.6.0
|
||||
if: always()
|
||||
id: trx-parser
|
||||
with:
|
||||
TRX_PATH: ${{ github.workspace }}/TrxOut
|
||||
REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
selftest-net6:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: none
|
||||
checks: write
|
||||
contents: read
|
||||
deployments: none
|
||||
id-token: none
|
||||
issues: none
|
||||
discussions: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: read
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@V27
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Restore dependencies
|
||||
run: nix develop --command dotnet restore
|
||||
- name: Build runner
|
||||
run: 'nix develop --command dotnet build WoofWare.NUnitTestRunner --configuration Release'
|
||||
- name: Build target
|
||||
run: 'nix develop --command dotnet build Consumer --configuration Release'
|
||||
- name: Test using self
|
||||
run: 'nix develop .#net6 --command ./WoofWare.NUnitTestRunner/bin/Release/net6.0/WoofWare.NUnitTestRunner ./Consumer/bin/Release/net6.0/Consumer.dll --trx TrxOut/out.trx'
|
||||
- name: Parse Trx files
|
||||
uses: NasAmin/trx-parser@v0.6.0
|
||||
if: always()
|
||||
|
@@ -1,8 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
@@ -10,6 +9,8 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="NoAttribute.fs" />
|
||||
<Compile Include="Inconclusive.fs" />
|
||||
<Compile Include="RunSubProcess.fs" />
|
||||
<Compile Include="TestContext.fs" />
|
||||
<Compile Include="TestNonParallel.fs" />
|
||||
<Compile Include="TestParallel.fs" />
|
||||
<Compile Include="TestStdout.fs" />
|
||||
@@ -26,9 +27,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FsUnit" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.1.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.1.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
45
Consumer/RunSubProcess.fs
Normal file
45
Consumer/RunSubProcess.fs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace Consumer
|
||||
|
||||
open System
|
||||
open System.Diagnostics
|
||||
open System.IO
|
||||
open System.IO.Compression
|
||||
open System.Text
|
||||
open NUnit.Framework
|
||||
open FsUnitTyped
|
||||
|
||||
[<TestFixture>]
|
||||
module RunSubProcess =
|
||||
[<Test>]
|
||||
let ``Run a subprocess`` () =
|
||||
let exe = "/bin/bash"
|
||||
let args = [ "-c" ; "echo hi >&2 && echo bye" ]
|
||||
let workingDir = None
|
||||
|
||||
let psi =
|
||||
ProcessStartInfo (
|
||||
exe,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
WorkingDirectory = Option.toObj workingDir
|
||||
)
|
||||
|
||||
for arg in args do
|
||||
psi.ArgumentList.Add arg
|
||||
|
||||
psi.EnvironmentVariables.Add ("THING", Path.Combine (AppDomain.CurrentDomain.BaseDirectory, "hi"))
|
||||
let stderr = StringBuilder ()
|
||||
use proc = new Process (StartInfo = psi)
|
||||
proc.OutputDataReceived.Add (fun e -> printfn $"%s{e.Data}")
|
||||
|
||||
proc.ErrorDataReceived.Add (fun e ->
|
||||
eprintfn $"%s{e.Data}"
|
||||
stderr.AppendLine e.Data |> ignore
|
||||
)
|
||||
|
||||
proc.Start () |> shouldEqual true
|
||||
proc.BeginOutputReadLine ()
|
||||
proc.BeginErrorReadLine ()
|
||||
|
||||
proc.WaitForExit ()
|
27
Consumer/TestContext.fs
Normal file
27
Consumer/TestContext.fs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Consumer
|
||||
|
||||
open FsUnitTyped
|
||||
open NUnit.Framework
|
||||
|
||||
[<TestFixture>]
|
||||
module TestContext =
|
||||
|
||||
[<TestCase 3>]
|
||||
let ``Context has appropriate values`` (i : int) =
|
||||
// We explicitly cannot support this (https://github.com/dotnet/dotnet-api-docs/pull/3869/files).
|
||||
// TestContext.Progress.WriteLine "hi!"
|
||||
|
||||
TestContext.CurrentContext.Test.MethodName
|
||||
|> shouldEqual "Context has appropriate values"
|
||||
|
||||
TestContext.CurrentContext.Test.Name
|
||||
|> shouldEqual "Context has appropriate values(3)"
|
||||
|
||||
TestContext.CurrentContext.Test.Namespace |> shouldEqual "Consumer"
|
||||
TestContext.CurrentContext.Test.ClassName |> shouldEqual "Consumer.TestContext"
|
||||
|
||||
TestContext.CurrentContext.Test.FullName
|
||||
|> shouldEqual "Consumer.TestContext.Context has appropriate values(3)"
|
||||
|
||||
i |> shouldEqual 3
|
||||
TestContext.CurrentContext.Test.Arguments |> List.ofArray |> shouldEqual [ 3 ]
|
@@ -14,3 +14,8 @@ However, we would recommend phrasing some of them differently, for maximum peace
|
||||
WoofWare.NUnitTestRunner has *limited* support for parallelism.
|
||||
By default, we run tests serially; we may or may not respect the NUnit parallelism attributes to any given extent (but we will never incorrectly run tests in parallel).
|
||||
For example, as of this writing, we do not run any tests in parallel (but the internal infrastructure is set up so that we will be able to do this soon).
|
||||
|
||||
## `TestContext`
|
||||
|
||||
WoofWare.NUnitTestRunner has partial support for NUnit's `TestContext`.
|
||||
See [the test file](./Consumer/TestContext.fs) for everything we expect to work.
|
||||
|
99
WoofWare.NUnitTestRunner.Lib/Args.fs
Normal file
99
WoofWare.NUnitTestRunner.Lib/Args.fs
Normal file
@@ -0,0 +1,99 @@
|
||||
namespace WoofWare.NUnitTestRunner
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
|
||||
[<AutoOpen>]
|
||||
module internal Patterns =
|
||||
let (|Key|_|) (start : string) (s : string) : string option =
|
||||
if s.StartsWith (start + "=", StringComparison.Ordinal) then
|
||||
s.Substring (start.Length + 1) |> Some
|
||||
else
|
||||
None
|
||||
|
||||
/// Represents how verbose the test runner's logging should be.
|
||||
[<RequireQualifiedAccess>]
|
||||
type LogLevel =
|
||||
/// Don't log any information about the test run.
|
||||
| Nothing
|
||||
/// Log as much information as is available about the test run.
|
||||
| Verbose
|
||||
|
||||
/// Arguments controlling the test runner itself (not the tests therein).
|
||||
type Args =
|
||||
{
|
||||
/// The DLL containing the tests we'll reflectively discover and invoke.
|
||||
Dll : FileInfo
|
||||
/// If set, the output file into which we will write a TRX report. (We'll create parent directories as necessary.)
|
||||
Trx : FileInfo option
|
||||
/// Also contains the original string which specified the filter.
|
||||
Filter : (string * Filter) option
|
||||
/// How verbose to be with the test runner's own logging.
|
||||
Logging : LogLevel
|
||||
/// Maximum number of tests which can run concurrently. This setting overrides any LevelOfParallelism reflectively
|
||||
/// extracted from the assembly under test.
|
||||
LevelOfParallelism : int option
|
||||
/// Abort if the test runner is running for longer than this timeout.
|
||||
Timeout : TimeSpan option
|
||||
}
|
||||
|
||||
/// Parse `argv` into a structured Args.
|
||||
static member Parse (args : string list) : Args =
|
||||
match args with
|
||||
| [] -> failwith "The first arg must be a positional arg, the DLL to test."
|
||||
| dll :: args ->
|
||||
|
||||
let rec go
|
||||
(trx : FileInfo option)
|
||||
(filter : (string * Filter) option)
|
||||
(logging : LogLevel option)
|
||||
(par : int option)
|
||||
(timeout : TimeSpan option)
|
||||
(args : string list)
|
||||
=
|
||||
match args with
|
||||
| [] ->
|
||||
{
|
||||
Dll = FileInfo dll
|
||||
Trx = trx
|
||||
Filter = filter
|
||||
Logging = logging |> Option.defaultValue LogLevel.Nothing
|
||||
LevelOfParallelism = par
|
||||
Timeout = timeout
|
||||
}
|
||||
| Key "--filter" filterStr :: rest
|
||||
| "--filter" :: filterStr :: rest ->
|
||||
match filter with
|
||||
| Some _ -> failwith "Two conflicting filters; you can only specify --filter once"
|
||||
| None -> go trx (Some (filterStr, Filter.parse filterStr)) logging par timeout rest
|
||||
| Key "--trx" trxStr :: rest
|
||||
| "--trx" :: trxStr :: rest ->
|
||||
match trx with
|
||||
| Some _ -> failwith "Two conflicting TRX outputs; you can only specify --trx once"
|
||||
| None -> go (Some (FileInfo trxStr)) filter logging par timeout rest
|
||||
| Key "--verbose" verboseStr :: rest
|
||||
| "--verbose" :: verboseStr :: rest ->
|
||||
match logging with
|
||||
| Some _ -> failwith "Two conflicting --verbose outputs; you can only specify --verbose once"
|
||||
| None ->
|
||||
let verbose =
|
||||
if Boolean.Parse verboseStr then
|
||||
LogLevel.Verbose
|
||||
else
|
||||
LogLevel.Nothing
|
||||
|
||||
go trx filter (Some verbose) par timeout rest
|
||||
| Key "--parallelism" parStr :: rest
|
||||
| "--parallelism" :: parStr :: rest ->
|
||||
match par with
|
||||
| Some _ -> failwith "Two conflicting --parallelism outputs; you can only specify --parallelism once"
|
||||
| None -> go trx filter logging (Some (Int32.Parse parStr)) timeout rest
|
||||
| Key "--timeout-seconds" timeoutStr :: rest
|
||||
| "--timeout-seconds" :: timeoutStr :: rest ->
|
||||
match timeout with
|
||||
| Some _ ->
|
||||
failwith "Two conflicting --timeout-seconds outputs; you can only specify --timeout-seconds once"
|
||||
| None -> go trx filter logging par (Some (TimeSpan.FromSeconds (Int32.Parse timeoutStr |> float))) rest
|
||||
| k :: _rest -> failwith $"Unrecognised arg %s{k}"
|
||||
|
||||
go None None None None None args
|
58
WoofWare.NUnitTestRunner.Lib/AssemblyLevelAttributes.fs
Normal file
58
WoofWare.NUnitTestRunner.Lib/AssemblyLevelAttributes.fs
Normal 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
|
||||
}
|
@@ -89,8 +89,13 @@ type TestContexts =
|
||||
AsyncLocal = local
|
||||
}
|
||||
|
||||
member internal this.Stdout : TextWriter = this.StdOutWriter
|
||||
member internal this.Stderr : TextWriter = this.StdErrWriter
|
||||
/// An output stream which will identify the ExecutionContext it's being written to from,
|
||||
/// and will separate that output into its own stream internally.
|
||||
member this.Stdout : TextWriter = this.StdOutWriter
|
||||
|
||||
/// An output stream which will identify the ExecutionContext it's being written to from,
|
||||
/// and will separate that output into its own stream internally.
|
||||
member this.Stderr : TextWriter = this.StdErrWriter
|
||||
|
||||
member internal this.DumpStdout (id : OutputStreamId) : string =
|
||||
lock
|
||||
|
243
WoofWare.NUnitTestRunner.Lib/CreateTrxReport.fs
Normal file
243
WoofWare.NUnitTestRunner.Lib/CreateTrxReport.fs
Normal 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
|
||||
}
|
@@ -1,3 +1,26 @@
|
||||
WoofWare.NUnitTestRunner.Args inherit obj, implements WoofWare.NUnitTestRunner.Args System.IEquatable, System.Collections.IStructuralEquatable
|
||||
WoofWare.NUnitTestRunner.Args..ctor [constructor]: (System.IO.FileInfo, System.IO.FileInfo option, (string * WoofWare.NUnitTestRunner.Filter) option, WoofWare.NUnitTestRunner.LogLevel, int option, System.TimeSpan option)
|
||||
WoofWare.NUnitTestRunner.Args.Dll [property]: [read-only] System.IO.FileInfo
|
||||
WoofWare.NUnitTestRunner.Args.Filter [property]: [read-only] (string * WoofWare.NUnitTestRunner.Filter) option
|
||||
WoofWare.NUnitTestRunner.Args.get_Dll [method]: unit -> System.IO.FileInfo
|
||||
WoofWare.NUnitTestRunner.Args.get_Filter [method]: unit -> (string * WoofWare.NUnitTestRunner.Filter) option
|
||||
WoofWare.NUnitTestRunner.Args.get_LevelOfParallelism [method]: unit -> int option
|
||||
WoofWare.NUnitTestRunner.Args.get_Logging [method]: unit -> WoofWare.NUnitTestRunner.LogLevel
|
||||
WoofWare.NUnitTestRunner.Args.get_Timeout [method]: unit -> System.TimeSpan option
|
||||
WoofWare.NUnitTestRunner.Args.get_Trx [method]: unit -> System.IO.FileInfo option
|
||||
WoofWare.NUnitTestRunner.Args.LevelOfParallelism [property]: [read-only] int option
|
||||
WoofWare.NUnitTestRunner.Args.Logging [property]: [read-only] WoofWare.NUnitTestRunner.LogLevel
|
||||
WoofWare.NUnitTestRunner.Args.Parse [static method]: string list -> WoofWare.NUnitTestRunner.Args
|
||||
WoofWare.NUnitTestRunner.Args.Timeout [property]: [read-only] System.TimeSpan option
|
||||
WoofWare.NUnitTestRunner.Args.Trx [property]: [read-only] System.IO.FileInfo option
|
||||
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 +35,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
|
||||
@@ -145,6 +170,20 @@ WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberSkipped [method]: string -> u
|
||||
WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberStart [method]: string -> unit
|
||||
WoofWare.NUnitTestRunner.LoadContext inherit System.Runtime.Loader.AssemblyLoadContext
|
||||
WoofWare.NUnitTestRunner.LoadContext..ctor [constructor]: (System.IO.FileInfo, System.IO.DirectoryInfo list, WoofWare.NUnitTestRunner.TestContexts)
|
||||
WoofWare.NUnitTestRunner.LogLevel inherit obj, implements WoofWare.NUnitTestRunner.LogLevel System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.LogLevel System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 2 cases
|
||||
WoofWare.NUnitTestRunner.LogLevel+Tags inherit obj
|
||||
WoofWare.NUnitTestRunner.LogLevel+Tags.Nothing [static field]: int = 0
|
||||
WoofWare.NUnitTestRunner.LogLevel+Tags.Verbose [static field]: int = 1
|
||||
WoofWare.NUnitTestRunner.LogLevel.get_IsNothing [method]: unit -> bool
|
||||
WoofWare.NUnitTestRunner.LogLevel.get_IsVerbose [method]: unit -> bool
|
||||
WoofWare.NUnitTestRunner.LogLevel.get_Nothing [static method]: unit -> WoofWare.NUnitTestRunner.LogLevel
|
||||
WoofWare.NUnitTestRunner.LogLevel.get_Tag [method]: unit -> int
|
||||
WoofWare.NUnitTestRunner.LogLevel.get_Verbose [static method]: unit -> WoofWare.NUnitTestRunner.LogLevel
|
||||
WoofWare.NUnitTestRunner.LogLevel.IsNothing [property]: [read-only] bool
|
||||
WoofWare.NUnitTestRunner.LogLevel.IsVerbose [property]: [read-only] bool
|
||||
WoofWare.NUnitTestRunner.LogLevel.Nothing [static property]: [read-only] WoofWare.NUnitTestRunner.LogLevel
|
||||
WoofWare.NUnitTestRunner.LogLevel.Tag [property]: [read-only] int
|
||||
WoofWare.NUnitTestRunner.LogLevel.Verbose [static property]: [read-only] WoofWare.NUnitTestRunner.LogLevel
|
||||
WoofWare.NUnitTestRunner.Match inherit obj, implements WoofWare.NUnitTestRunner.Match System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.Match System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 2 cases
|
||||
WoofWare.NUnitTestRunner.Match+Contains inherit WoofWare.NUnitTestRunner.Match
|
||||
WoofWare.NUnitTestRunner.Match+Contains.get_Item [method]: unit -> string
|
||||
@@ -226,6 +265,10 @@ WoofWare.NUnitTestRunner.SingleTestMethodModule inherit obj
|
||||
WoofWare.NUnitTestRunner.SingleTestMethodModule.parse [static method]: string list -> System.Reflection.MethodInfo -> System.Reflection.CustomAttributeData list -> (WoofWare.NUnitTestRunner.SingleTestMethod option * System.Reflection.CustomAttributeData list)
|
||||
WoofWare.NUnitTestRunner.TestContexts inherit obj, implements WoofWare.NUnitTestRunner.TestContexts System.IEquatable, System.Collections.IStructuralEquatable, IDisposable
|
||||
WoofWare.NUnitTestRunner.TestContexts.Empty [static method]: unit -> WoofWare.NUnitTestRunner.TestContexts
|
||||
WoofWare.NUnitTestRunner.TestContexts.get_Stderr [method]: unit -> System.IO.TextWriter
|
||||
WoofWare.NUnitTestRunner.TestContexts.get_Stdout [method]: unit -> System.IO.TextWriter
|
||||
WoofWare.NUnitTestRunner.TestContexts.Stderr [property]: [read-only] System.IO.TextWriter
|
||||
WoofWare.NUnitTestRunner.TestContexts.Stdout [property]: [read-only] System.IO.TextWriter
|
||||
WoofWare.NUnitTestRunner.TestFailure inherit obj, implements WoofWare.NUnitTestRunner.TestFailure System.IEquatable, System.Collections.IStructuralEquatable - union type with 3 cases
|
||||
WoofWare.NUnitTestRunner.TestFailure+SetUpFailed inherit WoofWare.NUnitTestRunner.TestFailure
|
||||
WoofWare.NUnitTestRunner.TestFailure+SetUpFailed.get_Item [method]: unit -> WoofWare.NUnitTestRunner.UserMethodFailure
|
||||
@@ -356,6 +399,7 @@ WoofWare.NUnitTestRunner.TestMemberSuccess.Ok [static property]: [read-only] Woo
|
||||
WoofWare.NUnitTestRunner.TestMemberSuccess.Tag [property]: [read-only] int
|
||||
WoofWare.NUnitTestRunner.TestProgress inherit obj
|
||||
WoofWare.NUnitTestRunner.TestProgress.toStderr [static method]: unit -> WoofWare.NUnitTestRunner.ITestProgress
|
||||
WoofWare.NUnitTestRunner.TestProgress.toWriter [static method]: System.IO.TextWriter -> WoofWare.NUnitTestRunner.ITestProgress
|
||||
WoofWare.NUnitTestRunner.TrxCounters inherit obj, implements WoofWare.NUnitTestRunner.TrxCounters System.IEquatable, System.Collections.IStructuralEquatable
|
||||
WoofWare.NUnitTestRunner.TrxCounters..ctor [constructor]: (System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32, System.UInt32)
|
||||
WoofWare.NUnitTestRunner.TrxCounters.Aborted [property]: [read-only] System.UInt32
|
||||
|
@@ -1,6 +1,7 @@
|
||||
namespace WoofWare.NUnitTestRunner
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
|
||||
/// Represents something which knows how to report progress through a test suite.
|
||||
/// Note that we don't guarantee anything about parallelism; you must make sure
|
||||
@@ -24,22 +25,25 @@ type ITestProgress =
|
||||
/// Methods for constructing specific ITestProgress objects.
|
||||
[<RequireQualifiedAccess>]
|
||||
module TestProgress =
|
||||
/// An ITestProgress which logs to stderr.
|
||||
let toStderr () : ITestProgress =
|
||||
/// An ITestProgress which logs to the given writer.
|
||||
let toWriter (writer : TextWriter) : ITestProgress =
|
||||
{ new ITestProgress with
|
||||
member _.OnTestFixtureStart name testCount =
|
||||
let plural = if testCount = 1 then "" else "s"
|
||||
Console.Error.WriteLine $"Running test fixture: %s{name} (%i{testCount} test%s{plural} to run)"
|
||||
writer.WriteLine $"Running test fixture: %s{name} (%i{testCount} test%s{plural} to run)"
|
||||
|
||||
member _.OnTestMemberStart name =
|
||||
Console.Error.WriteLine $"Running test: %s{name}"
|
||||
writer.WriteLine $"Running test: %s{name}"
|
||||
|
||||
member _.OnTestFailed name failure =
|
||||
Console.Error.WriteLine $"Test failed: %O{failure}"
|
||||
writer.WriteLine $"Test failed: %O{failure}"
|
||||
|
||||
member _.OnTestMemberFinished name =
|
||||
Console.Error.WriteLine $"Finished test %s{name}"
|
||||
writer.WriteLine $"Finished test %s{name}"
|
||||
|
||||
member _.OnTestMemberSkipped name =
|
||||
Console.Error.WriteLine $"Skipping test due to filter: %s{name}"
|
||||
writer.WriteLine $"Skipping test due to filter: %s{name}"
|
||||
}
|
||||
|
||||
/// An ITestProgress which logs to stderr.
|
||||
let toStderr () : ITestProgress = toWriter Console.Error
|
||||
|
@@ -29,6 +29,7 @@
|
||||
<Compile Include="Result.fs" />
|
||||
<Compile Include="Domain.fs" />
|
||||
<Compile Include="Filter.fs" />
|
||||
<Compile Include="Args.fs" />
|
||||
<Compile Include="ParallelQueue.fs" />
|
||||
<Compile Include="SingleTestMethod.fs" />
|
||||
<Compile Include="TestProgress.fs" />
|
||||
@@ -36,6 +37,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>
|
||||
@@ -44,14 +47,14 @@
|
||||
<EmbeddedResource Include="version.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="WoofWare.PrattParser" Version="0.2.1" />
|
||||
<PackageReference Include="WoofWare.PrattParser" Version="0.2.2" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||
<PackageReference Include="WoofWare.DotnetRuntimeLocator" Version="0.1.9" />
|
||||
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="3.1.7" />
|
||||
<PackageReference Include="Myriad.SDK" Version="0.8.3" />
|
||||
<PackageReference Include="Myriad.SDK" Version="0.8.3" PrivateAssets="all" />
|
||||
<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>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.13",
|
||||
"version": "0.16",
|
||||
"publicReleaseRefSpec": [
|
||||
"^refs/heads/main$"
|
||||
],
|
||||
@@ -8,4 +8,4 @@
|
||||
":/Directory.Build.props",
|
||||
":/README.md"
|
||||
]
|
||||
}
|
||||
}
|
35
WoofWare.NUnitTestRunner.StartupHook/StartupHook.cs
Normal file
35
WoofWare.NUnitTestRunner.StartupHook/StartupHook.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using WoofWare.NUnitTestRunner.StartupHook;
|
||||
|
||||
namespace WoofWare.NUnitTestRunner.StartupHook
|
||||
{
|
||||
internal class StartupAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
|
||||
public StartupAssemblyLoadContext()
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(Assembly.GetExecutingAssembly().Location);
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
|
||||
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Must be internal and called `StartupHook`
|
||||
internal class StartupHook
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
var loadContext = new StartupAssemblyLoadContext();
|
||||
var assembly = loadContext.LoadFromAssemblyName(new AssemblyName("WoofWare.NUnitTestRunner.StartupHookLogic"));
|
||||
assembly.DefinedTypes.First(x => x.Name == "StartupHookLogic").GetDeclaredMethod("DoIt")!.Invoke(null, null);
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WoofWare.NUnitTestRunner.StartupHookLogic\WoofWare.NUnitTestRunner.StartupHookLogic.csproj" />
|
||||
<ProjectReference Include="..\WoofWare.NUnitTestRunner.Lib\WoofWare.NUnitTestRunner.Lib.fsproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="StartupHook.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
114
WoofWare.NUnitTestRunner.StartupHookLogic/StartupHookLogic.cs
Normal file
114
WoofWare.NUnitTestRunner.StartupHookLogic/StartupHookLogic.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.FSharp.Collections;
|
||||
using Microsoft.FSharp.Core;
|
||||
|
||||
namespace WoofWare.NUnitTestRunner.StartupHookLogic;
|
||||
|
||||
public class StartupHookLogic
|
||||
{
|
||||
private static void DoIt()
|
||||
{
|
||||
// Load test runner lib
|
||||
var nunitLib = Environment.GetEnvironmentVariable("WOOFWARE_NUNIT_LIB");
|
||||
if (string.IsNullOrEmpty(nunitLib))
|
||||
{
|
||||
throw new ArgumentException("WoofWare.NUnitTestRunner hook expects to run with WOOFWARE_NUNIT_LIB set to WoofWare.NUnitTestRunner.Lib.dll");
|
||||
}
|
||||
Assembly.LoadFrom(nunitLib);
|
||||
|
||||
var startTime = DateTimeOffset.Now;
|
||||
|
||||
// Arg parsing
|
||||
|
||||
var filterVar = Environment.GetEnvironmentVariable("WOOFWARE_NUNIT_FILTER");
|
||||
static bool NoFilter(TestFixture f, SingleTestMethod g)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
FSharpFunc<TestFixture, FSharpFunc<SingleTestMethod, bool>> filter;
|
||||
if (string.IsNullOrEmpty(filterVar))
|
||||
{
|
||||
filter = FuncConvert.FromFunc<TestFixture, SingleTestMethod, bool>(NoFilter);
|
||||
}
|
||||
else
|
||||
{
|
||||
filter = FilterModule.shouldRun(FilterModule.parse(filterVar));
|
||||
}
|
||||
|
||||
var assy = Assembly.GetEntryAssembly()!;
|
||||
|
||||
var attrs = AssemblyLevelAttributesModule.get(assy);
|
||||
|
||||
FSharpOption<int> levelOfParallelism;
|
||||
var parallelismVar = Environment.GetEnvironmentVariable("WOOFWARE_NUNIT_PARALLELISM_LEVEL");
|
||||
if (string.IsNullOrEmpty(parallelismVar))
|
||||
{
|
||||
levelOfParallelism = attrs.Parallelism;
|
||||
}
|
||||
else
|
||||
{
|
||||
levelOfParallelism = FSharpOption<int>.Some(Int32.Parse(parallelismVar));
|
||||
}
|
||||
|
||||
var testFixtures = assy.ExportedTypes.Select(TestFixtureModule.parse);
|
||||
using var par =
|
||||
new ParallelQueue(levelOfParallelism, attrs.Parallelizable, FSharpOption<CancellationToken>.None);
|
||||
var creationTime = DateTimeOffset.Now;
|
||||
|
||||
var timeoutVar = Environment.GetEnvironmentVariable("WOOFWARE_NUNIT_TIMEOUT_SECS");
|
||||
TimeSpan timeout;
|
||||
if (string.IsNullOrEmpty(timeoutVar))
|
||||
{
|
||||
timeout = TimeSpan.FromHours(2.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeout = TimeSpan.FromSeconds(Int32.Parse(timeoutVar));
|
||||
}
|
||||
|
||||
var normalErr = Console.Error;
|
||||
using var contexts = TestContexts.Empty();
|
||||
Console.SetOut(contexts.Stdout);
|
||||
Console.SetError(contexts.Stderr);
|
||||
|
||||
var nunitAssembly = Assembly.Load("NUnit.Framework");
|
||||
if (object.ReferenceEquals(nunitAssembly, null))
|
||||
{
|
||||
throw new Exception("Could not load NUnit.Framework");
|
||||
}
|
||||
|
||||
var testContext = nunitAssembly.DefinedTypes.First(t => t.FullName == "NUnit.Framework.TestContext") ?? throw new Exception("Could not find TestContext type");
|
||||
var currentContextField = testContext.GetField("CurrentContext", BindingFlags.Static | BindingFlags.Public) ?? throw new Exception("Could not find CurrentContext field on TestContext");
|
||||
var currentContext = currentContextField.GetValue(null) ?? throw new Exception("Could not obtain value of CurrentContext");
|
||||
currentContextField.SetValue(currentContext, currentContext);
|
||||
|
||||
var results =
|
||||
Task.WhenAll(testFixtures.Select(x =>
|
||||
TestFixtureModule.run(contexts, par, TestProgress.toWriter(normalErr), filter, x)));
|
||||
|
||||
if (!results.Wait(timeout))
|
||||
{
|
||||
throw new Exception($"Tests failed to terminate within timeout of {timeout}");
|
||||
}
|
||||
|
||||
var sorted =
|
||||
results.Result.SelectMany(x => x);
|
||||
var report = BuildTrxReport.build(assy, creationTime, startTime, ListModule.OfSeq(sorted));
|
||||
|
||||
var trxFile = Environment.GetEnvironmentVariable("WOOFWARE_NUNIT_GENERATE_TRX");
|
||||
if (!string.IsNullOrEmpty(trxFile))
|
||||
{
|
||||
var contents = TrxReportModule.toXml(report).OuterXml;
|
||||
var trxPath = new FileInfo(trxFile);
|
||||
trxPath.Directory!.Create();
|
||||
File.WriteAllText(trxPath.FullName, contents);
|
||||
normalErr.WriteLine($"Written TRX file: {trxPath.FullName}");
|
||||
}
|
||||
|
||||
if (report.ResultsSummary.Outcome.Equals(TrxOutcome.Completed))
|
||||
Environment.Exit(0);
|
||||
else
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WoofWare.NUnitTestRunner.Lib\WoofWare.NUnitTestRunner.Lib.fsproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="StartupHookLogic.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@@ -8,6 +8,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.NUnitTestRunner.Li
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WoofWare.NUnitTestRunner.Test", "WoofWare.NUnitTestRunner\WoofWare.NUnitTestRunner.Test\WoofWare.NUnitTestRunner.Test.fsproj", "{443B01B3-2A8C-45CF-96D6-1D890EEA0533}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WoofWare.NUnitTestRunner.StartupHook", "WoofWare.NUnitTestRunner.StartupHook\WoofWare.NUnitTestRunner.StartupHook.csproj", "{E2C73D96-570C-4E3C-B997-707AF8BB0E6A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WoofWare.NUnitTestRunner.StartupHookLogic", "WoofWare.NUnitTestRunner.StartupHookLogic\WoofWare.NUnitTestRunner.StartupHookLogic.csproj", "{A70627C8-9D19-42C2-AFEB-CFBDDDCE045D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -30,5 +34,13 @@ Global
|
||||
{443B01B3-2A8C-45CF-96D6-1D890EEA0533}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{443B01B3-2A8C-45CF-96D6-1D890EEA0533}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{443B01B3-2A8C-45CF-96D6-1D890EEA0533}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E2C73D96-570C-4E3C-B997-707AF8BB0E6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E2C73D96-570C-4E3C-B997-707AF8BB0E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E2C73D96-570C-4E3C-B997-707AF8BB0E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E2C73D96-570C-4E3C-B997-707AF8BB0E6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A70627C8-9D19-42C2-AFEB-CFBDDDCE045D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A70627C8-9D19-42C2-AFEB-CFBDDDCE045D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A70627C8-9D19-42C2-AFEB-CFBDDDCE045D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A70627C8-9D19-42C2-AFEB-CFBDDDCE045D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@@ -1,113 +1,22 @@
|
||||
namespace WoofWare.NUnitTestRunner
|
||||
|
||||
open System
|
||||
open System.Diagnostics
|
||||
open System.IO
|
||||
open System.Reflection
|
||||
open System.Threading.Tasks
|
||||
open Spectre.Console
|
||||
|
||||
// Fix for https://github.com/Smaug123/unofficial-nunit-runner/issues/8
|
||||
// Set AppContext.BaseDirectory to where the test DLL is.
|
||||
// (This tells the DLL loader to look next to the test DLL for dependencies.)
|
||||
type SetBaseDir (testDll : FileInfo) =
|
||||
let oldBaseDir = AppContext.BaseDirectory
|
||||
do AppContext.SetData ("APP_CONTEXT_BASE_DIRECTORY", testDll.Directory.FullName)
|
||||
|
||||
interface IDisposable with
|
||||
member _.Dispose () =
|
||||
AppContext.SetData ("APP_CONTEXT_BASE_DIRECTORY", oldBaseDir)
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
type LogLevel =
|
||||
| Nothing
|
||||
| Verbose
|
||||
|
||||
[<AutoOpen>]
|
||||
module Patterns =
|
||||
let (|Key|_|) (start : string) (s : string) : string option =
|
||||
if s.StartsWith (start + "=", StringComparison.Ordinal) then
|
||||
s.Substring (start.Length + 1) |> Some
|
||||
else
|
||||
None
|
||||
|
||||
type Args =
|
||||
{
|
||||
Dll : FileInfo
|
||||
Trx : FileInfo option
|
||||
Filter : Filter option
|
||||
Logging : LogLevel
|
||||
LevelOfParallelism : int option
|
||||
Timeout : TimeSpan option
|
||||
}
|
||||
|
||||
static member Parse (args : string list) : Args =
|
||||
match args with
|
||||
| [] -> failwith "The first arg must be a positional arg, the DLL to test."
|
||||
| dll :: args ->
|
||||
|
||||
let rec go
|
||||
(trx : FileInfo option)
|
||||
(filter : Filter option)
|
||||
(logging : LogLevel option)
|
||||
(par : int option)
|
||||
(timeout : TimeSpan option)
|
||||
(args : string list)
|
||||
=
|
||||
match args with
|
||||
| [] ->
|
||||
{
|
||||
Dll = FileInfo dll
|
||||
Trx = trx
|
||||
Filter = filter
|
||||
Logging = logging |> Option.defaultValue LogLevel.Nothing
|
||||
LevelOfParallelism = par
|
||||
Timeout = timeout
|
||||
}
|
||||
| Key "--filter" filterStr :: rest
|
||||
| "--filter" :: filterStr :: rest ->
|
||||
match filter with
|
||||
| Some _ -> failwith "Two conflicting filters; you can only specify --filter once"
|
||||
| None -> go trx (Some (Filter.parse filterStr)) logging par timeout rest
|
||||
| Key "--trx" trxStr :: rest
|
||||
| "--trx" :: trxStr :: rest ->
|
||||
match trx with
|
||||
| Some _ -> failwith "Two conflicting TRX outputs; you can only specify --trx once"
|
||||
| None -> go (Some (FileInfo trxStr)) filter logging par timeout rest
|
||||
| Key "--verbose" verboseStr :: rest
|
||||
| "--verbose" :: verboseStr :: rest ->
|
||||
match logging with
|
||||
| Some _ -> failwith "Two conflicting --verbose outputs; you can only specify --verbose once"
|
||||
| None ->
|
||||
let verbose =
|
||||
if Boolean.Parse verboseStr then
|
||||
LogLevel.Verbose
|
||||
else
|
||||
LogLevel.Nothing
|
||||
|
||||
go trx filter (Some verbose) par timeout rest
|
||||
| Key "--parallelism" parStr :: rest
|
||||
| "--parallelism" :: parStr :: rest ->
|
||||
match par with
|
||||
| Some _ -> failwith "Two conflicting --parallelism outputs; you can only specify --parallelism once"
|
||||
| None -> go trx filter logging (Some (Int32.Parse parStr)) timeout rest
|
||||
| Key "--timeout-seconds" timeoutStr :: rest
|
||||
| "--timeout-seconds" :: timeoutStr :: rest ->
|
||||
match timeout with
|
||||
| Some _ ->
|
||||
failwith "Two conflicting --timeout-seconds outputs; you can only specify --timeout-seconds once"
|
||||
| None -> go trx filter logging par (Some (TimeSpan.FromSeconds (Int32.Parse timeoutStr |> float))) rest
|
||||
| k :: _rest -> failwith $"Unrecognised arg %s{k}"
|
||||
|
||||
go None None None None None args
|
||||
|
||||
module Program =
|
||||
let main argv =
|
||||
// This is actually transcribed into C# in WoofWare.NUnitTestRunner.StartupHookLogic.
|
||||
let execute argv =
|
||||
let startTime = DateTimeOffset.Now
|
||||
|
||||
let args = argv |> List.ofArray |> Args.Parse
|
||||
|
||||
let filter =
|
||||
match args.Filter with
|
||||
| Some filter -> Filter.shouldRun filter
|
||||
| Some (_, filter) -> Filter.shouldRun filter
|
||||
| None -> fun _ _ -> true
|
||||
|
||||
let stderr =
|
||||
@@ -125,54 +34,15 @@ module Program =
|
||||
for d in runtime do
|
||||
stderr.WriteLine $".NET runtime directory: %s{d.FullName}"
|
||||
|
||||
use _ = new SetBaseDir (args.Dll)
|
||||
|
||||
use contexts = TestContexts.Empty ()
|
||||
|
||||
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 +57,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 +76,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,10 +86,56 @@ module Program =
|
||||
Console.Error.WriteLine $"Written TRX file: %s{trxPath.FullName}"
|
||||
| None -> ()
|
||||
|
||||
match outcome with
|
||||
match report.ResultsSummary.Outcome with
|
||||
| TrxOutcome.Completed -> 0
|
||||
| _ -> 1
|
||||
|
||||
let main argv =
|
||||
let args = argv |> List.ofArray |> Args.Parse
|
||||
|
||||
let psi = ProcessStartInfo "dotnet"
|
||||
|
||||
match args.Trx with
|
||||
| None -> ()
|
||||
| Some trx -> psi.EnvironmentVariables.Add ("WOOFWARE_NUNIT_GENERATE_TRX", trx.FullName)
|
||||
|
||||
match args.LevelOfParallelism with
|
||||
| None -> ()
|
||||
| Some par -> psi.EnvironmentVariables.Add ("WOOFWARE_NUNIT_PARALLELISM_LEVEL", string<int> par)
|
||||
|
||||
match args.Filter with
|
||||
| None -> ()
|
||||
| Some (filter, _) -> psi.EnvironmentVariables.Add ("WOOFWARE_NUNIT_FILTER", filter)
|
||||
|
||||
match args.Timeout with
|
||||
| None -> ()
|
||||
| Some timeout ->
|
||||
psi.EnvironmentVariables.Add ("WOOFWARE_NUNIT_TIMEOUT_SECS", string<int> (int<float> timeout.TotalSeconds))
|
||||
|
||||
psi.ArgumentList.Add "exec"
|
||||
psi.ArgumentList.Add args.Dll.FullName
|
||||
|
||||
let us = Assembly.GetExecutingAssembly().Location |> FileInfo
|
||||
|
||||
let startupHook =
|
||||
Path.Combine (us.Directory.FullName, "WoofWare.NUnitTestRunner.StartupHook.dll")
|
||||
|
||||
psi.EnvironmentVariables.Add ("DOTNET_STARTUP_HOOKS", startupHook)
|
||||
|
||||
psi.EnvironmentVariables.Add (
|
||||
"WOOFWARE_NUNIT_LIB",
|
||||
Path.Combine (us.Directory.FullName, "WoofWare.NUnitTestRunner.Lib.dll")
|
||||
)
|
||||
|
||||
use proc = new Process ()
|
||||
proc.StartInfo <- psi
|
||||
|
||||
if not (proc.Start ()) then
|
||||
failwith "Failed to start dotnet"
|
||||
|
||||
proc.WaitForExit ()
|
||||
proc.ExitCode
|
||||
|
||||
[<EntryPoint>]
|
||||
let reallyMain argv =
|
||||
// Hack to make sure `finally`s get run.
|
||||
|
@@ -1,22 +0,0 @@
|
||||
namespace WoofWare.NUnitTestRunner
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module internal Seq =
|
||||
|
||||
let tryMinBy (f : 'a -> 'b) (s : 'a seq) : 'a option =
|
||||
use enum = s.GetEnumerator ()
|
||||
|
||||
if not (enum.MoveNext ()) then
|
||||
None
|
||||
else
|
||||
|
||||
let mutable answer = enum.Current
|
||||
let mutable fAnswer = f enum.Current
|
||||
|
||||
while enum.MoveNext () do
|
||||
let fNext = f enum.Current
|
||||
|
||||
if fNext < fAnswer then
|
||||
answer <- enum.Current
|
||||
|
||||
Some answer
|
@@ -2,7 +2,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RollForward>Major</RollForward>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Authors>Patrick Stevens</Authors>
|
||||
@@ -19,7 +20,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Seq.fs" />
|
||||
<Compile Include="Progress.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
<None Include="..\README.md">
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WoofWare.NUnitTestRunner.Lib\WoofWare.NUnitTestRunner.Lib.fsproj" />
|
||||
<ProjectReference Include="..\WoofWare.NUnitTestRunner.StartupHookLogic\WoofWare.NUnitTestRunner.StartupHookLogic.csproj" />
|
||||
<ProjectReference Include="..\WoofWare.NUnitTestRunner.StartupHook\WoofWare.NUnitTestRunner.StartupHook.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
21
flake.nix
21
flake.nix
@@ -56,13 +56,20 @@
|
||||
doCheck = true;
|
||||
};
|
||||
};
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = [dotnet-sdk];
|
||||
packages = [
|
||||
pkgs.alejandra
|
||||
pkgs.nodePackages.markdown-link-check
|
||||
pkgs.shellcheck
|
||||
];
|
||||
devShells = {
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
dotnet-sdk
|
||||
pkgs.alejandra
|
||||
pkgs.nodePackages.markdown-link-check
|
||||
pkgs.shellcheck
|
||||
];
|
||||
};
|
||||
net6 = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.dotnetCorePackages.runtime_6_0
|
||||
];
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -263,7 +263,7 @@
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "WoofWare.PrattParser";
|
||||
version = "0.2.1";
|
||||
sha256 = "1cb9496fbbrdc40dirjmc7ax02ghr27ahqq5hpk96rdzyaang9hg";
|
||||
version = "0.2.2";
|
||||
sha256 = "0cgrmd1kc3k224lsjhy5npalwg6kpqd8nx78szi1yq67kyb0farq";
|
||||
})
|
||||
]
|
||||
|
Reference in New Issue
Block a user