Compare commits

...

14 Commits

Author SHA1 Message Date
Patrick Stevens
fb945c04ac Use DOTNET_STARTUP_HOOK to have the target run the tests (#91) 2024-06-23 17:25:24 +01:00
Patrick Stevens
85cd116d52 Delete unused file (#94) 2024-06-23 15:30:46 +01:00
Patrick Stevens
8e7c54cc83 Split out AppContext and Args to lib (#93) 2024-06-23 15:26:35 +01:00
Patrick Stevens
378a0169f8 Formatting (#92) 2024-06-23 15:13:40 +01:00
Patrick Stevens
870804d6ef Multi-framework Consumer test (#89) 2024-06-23 11:50:58 +01:00
Patrick Stevens
9f5f22c644 Pull report generation into lib (#90) 2024-06-23 11:44:53 +01:00
Patrick Stevens
296f230616 Bump deps (#88) 2024-06-17 23:51:00 +01:00
Patrick Stevens
56ac203570 Attest contents of NuGet packages (#87) 2024-06-17 23:37:53 +01:00
Patrick Stevens
e17e769d5a Direnv (#84) 2024-06-16 23:29:01 +01:00
Patrick Stevens
57c34e0c4c Permit self-contained test fixtures (#83) 2024-06-16 21:26:31 +01:00
Patrick Stevens
7f9464b826 Args (#82) 2024-06-16 19:28:16 +01:00
Patrick Stevens
3d04199c56 More runtimes (#81) 2024-06-16 16:13:58 +01:00
Patrick Stevens
9d4b893e02 Run tests in parallel (#79) 2024-06-16 15:43:07 +01:00
Patrick Stevens
55e9645316 Rewrite tests to allow being run in parallel (#80) 2024-06-16 15:20:07 +01:00
35 changed files with 1466 additions and 510 deletions

View File

@@ -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

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

14
.github/workflows/assert-contents.sh vendored Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
echo "Unzipping version from NuGet"
ls from-nuget.nupkg
mkdir from-nuget && cp from-nuget.nupkg from-nuget/zip.zip && cd from-nuget && unzip zip.zip && rm zip.zip && cd - || exit 1
echo "Unzipping version from local build"
ls packed/
mkdir from-local && cp packed/*.nupkg from-local/zip.zip && cd from-local && unzip zip.zip && rm zip.zip && cd - || exit 1
cd from-local && find . -type f -exec sha256sum {} \; | sort > ../from-local.txt && cd .. || exit 1
cd from-nuget && find . -type f -and -not -name '.signature.p7s' -exec sha256sum {} \; | sort > ../from-nuget.txt && cd .. || exit 1
diff from-local.txt from-nuget.txt

View File

@@ -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()
@@ -240,11 +282,53 @@ jobs:
steps:
- run: echo "All required checks complete."
nuget-publish:
attestation-lib:
runs-on: ubuntu-latest
needs: [all-required-checks-complete]
if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
permissions:
id-token: write
attestations: write
contents: read
steps:
- name: Download NuGet artifact
uses: actions/download-artifact@v4
with:
name: nuget-package-lib
path: packed
- name: Attest Build Provenance
uses: actions/attest-build-provenance@897ed5eab6ed058a474202017ada7f40bfa52940 # v1.0.0
with:
subject-path: "packed/*.nupkg"
attestation-tool:
runs-on: ubuntu-latest
needs: [all-required-checks-complete]
if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
permissions:
id-token: write
attestations: write
contents: read
steps:
- name: Download NuGet artifact
uses: actions/download-artifact@v4
with:
name: nuget-package-tool
path: packed
- name: Attest Build Provenance
uses: actions/attest-build-provenance@897ed5eab6ed058a474202017ada7f40bfa52940 # v1.0.0
with:
subject-path: "packed/*.nupkg"
nuget-publish-lib:
runs-on: ubuntu-latest
if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
needs: [all-required-checks-complete]
environment: main-deploy
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Nix
@@ -252,20 +336,73 @@ jobs:
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Download NuGet artifact (lib)
- name: Download NuGet artifact
uses: actions/download-artifact@v4
with:
name: nuget-package-lib
path: packed-lib
- name: Publish to NuGet (lib)
run: nix develop --command dotnet nuget push "packed-lib/WoofWare.NUnitTestRunner.Lib.*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Download NuGet artifact (tool)
path: packed
- name: Publish to NuGet
id: publish-success
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: 'nix develop --command bash ./.github/workflows/nuget-push.sh "packed/WoofWare.NUnitTestRunner.Lib.*.nupkg"'
- name: Wait for availability
if: steps.publish-success.outputs.result == 'published'
env:
PACKAGE_VERSION: ${{ steps.publish-success.outputs.version }}
run: 'echo "$PACKAGE_VERSION" && while ! curl -L --fail -o from-nuget.nupkg "https://www.nuget.org/api/v2/package/WoofWare.NUnitTestRunner.Lib/$PACKAGE_VERSION" ; do sleep 10; done'
# Astonishingly, NuGet.org considers it to be "more secure" to tamper with my package after upload (https://devblogs.microsoft.com/nuget/introducing-repository-signatures/).
# So we have to *re-attest* it after it's uploaded. Mind-blowing.
- name: Assert package contents
if: steps.publish-success.outputs.result == 'published'
run: 'bash ./.github/workflows/assert-contents.sh'
- name: Attest Build Provenance
if: steps.publish-success.outputs.result == 'published'
uses: actions/attest-build-provenance@897ed5eab6ed058a474202017ada7f40bfa52940 # v1.0.0
with:
subject-path: "from-nuget.nupkg"
nuget-publish-tool:
runs-on: ubuntu-latest
if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
needs: [all-required-checks-complete]
environment: main-deploy
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@V27
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Download NuGet artifact
uses: actions/download-artifact@v4
with:
name: nuget-package-tool
path: packed-tool
- name: Publish to NuGet (tool)
run: nix develop --command dotnet nuget push "packed-tool/WoofWare.NUnitTestRunner.*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
path: packed
- name: Publish to NuGet
id: publish-success
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: 'nix develop --command bash ./.github/workflows/nuget-push.sh "packed/WoofWare.NUnitTestRunner.*.nupkg"'
- name: Wait for availability
if: steps.publish-success.outputs.result == 'published'
env:
PACKAGE_VERSION: ${{ steps.publish-success.outputs.version }}
run: 'echo "$PACKAGE_VERSION" && while ! curl -L --fail -o from-nuget.nupkg "https://www.nuget.org/api/v2/package/WoofWare.NUnitTestRunner/$PACKAGE_VERSION" ; do sleep 10; done'
# Astonishingly, NuGet.org considers it to be "more secure" to tamper with my package after upload (https://devblogs.microsoft.com/nuget/introducing-repository-signatures/).
# So we have to *re-attest* it after it's uploaded. Mind-blowing.
- name: Assert package contents
if: steps.publish-success.outputs.result == 'published'
run: 'bash ./.github/workflows/assert-contents.sh'
- name: Attest Build Provenance
if: steps.publish-success.outputs.result == 'published'
uses: actions/attest-build-provenance@897ed5eab6ed058a474202017ada7f40bfa52940 # v1.0.0
with:
subject-path: "from-nuget.nupkg"
github-release-tool:
runs-on: ubuntu-latest

24
.github/workflows/nuget-push.sh vendored Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
SOURCE_NUPKG=$(find . -type f -name '*.nupkg')
PACKAGE_VERSION=$(basename "$SOURCE_NUPKG" | rev | cut -d '.' -f 2-4 | rev)
echo "version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
tmp=$(mktemp)
if ! dotnet nuget push "$SOURCE_NUPKG" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json > "$tmp" ; then
cat "$tmp"
if grep 'already exists and cannot be modified' "$tmp" ; then
echo "result=skipped" >> "$GITHUB_OUTPUT"
exit 0
else
echo "Unexpected failure to upload"
exit 1
fi
fi
cat "$tmp"
echo "result=published" >> "$GITHUB_OUTPUT"

View File

@@ -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="TestNonParallel.fs" />
<Compile Include="TestParallel.fs" />
<Compile Include="TestStdout.fs" />
<Compile Include="TestParameterisedFixture.fs" />
@@ -25,9 +26,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
View 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 ()

View File

@@ -0,0 +1,19 @@
namespace Consumer
open System
open System.Threading
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
[<NonParallelizable>]
module TestNonParallel =
let defaults = List.init 40 id
let lock = ref 0
[<TestCaseSource(nameof defaults)>]
let ``Default thing, but not parallel`` (i : int) =
Interlocked.Increment lock |> shouldEqual 1
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
lock.Value <- 0
i |> shouldEqual i

View File

@@ -1,5 +1,7 @@
namespace Consumer
open System
open System.Threading
open NUnit.Framework
open FsUnitTyped
@@ -7,53 +9,58 @@ open FsUnitTyped
[<Parallelizable>]
module TestParallelDefault =
let defaults = List.init 100 id
let defaults = List.init 60 id
[<TestCaseSource(nameof defaults)>]
let ``Default thing`` (i : int) =
System.Console.WriteLine i
let ``Default thing, no scope`` (i : int) =
Console.WriteLine i
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
i |> shouldEqual i
[<TestFixture>]
[<Parallelizable(ParallelScope.All)>]
module TestParallelAllScope =
let defaults = List.init 100 id
let defaults = List.init 60 id
[<TestCaseSource(nameof defaults)>]
let ``Default thing`` (i : int) =
System.Console.WriteLine i
let ``Thing, all scope`` (i : int) =
Console.WriteLine i
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
i |> shouldEqual i
[<TestFixture>]
[<Parallelizable(ParallelScope.Self)>]
module TestParallelSelfScope =
let defaults = List.init 100 id
let defaults = List.init 60 id
[<TestCaseSource(nameof defaults)>]
let ``Default thing`` (i : int) =
System.Console.WriteLine i
let ``Thing, self scope`` (i : int) =
Console.WriteLine i
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
i |> shouldEqual i
[<TestFixture>]
[<Parallelizable(ParallelScope.Children)>]
module TestParallelChildrenScope =
let defaults = List.init 100 id
let defaults = List.init 60 id
[<TestCaseSource(nameof defaults)>]
let ``Default thing`` (i : int) =
System.Console.WriteLine i
let ``Thing, children scope`` (i : int) =
Console.WriteLine i
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
i |> shouldEqual i
[<TestFixture>]
[<Parallelizable(ParallelScope.Fixtures)>]
module TestParallelFixturesScope =
let defaults = List.init 100 id
let defaults = List.init 60 id
[<TestCaseSource(nameof defaults)>]
let ``Default thing`` (i : int) =
System.Console.WriteLine i
let ``Thing, fixtures scope`` (i : int) =
Console.WriteLine i
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
i |> shouldEqual i

View File

@@ -47,10 +47,11 @@ module TestSetUp =
setUpTimesSeen
|> Seq.toList
|> List.sort
// Six tests: one for Test, two for the TestCase, three for the Repeat.
|> shouldEqual [ 1..6 ]
tearDownTimesSeen |> Seq.toList |> shouldEqual [ 1..6 ]
tearDownTimesSeen |> Seq.toList |> List.sort |> shouldEqual [ 1..6 ]
[<Test>]
let ``Test 1`` () =

View File

@@ -86,14 +86,42 @@ module TestValues =
[<OneTimeTearDown>]
let ``Values are all OK`` () =
seen1 |> Seq.toList |> shouldEqual [ true ; false ]
seen2 |> Seq.toList |> shouldEqual [ (true, false) ; (false, true) ]
seen3 |> Seq.toList |> shouldEqual [ (88, box 29) ; (31, box 0) ]
seen4 |> Seq.toList |> shouldEqual [ ("hi", box "ohh") ; ("bye", null) ]
seen5 |> Seq.toList |> shouldEqual [ (88, box 29) ; (31, box 29) ]
seen6 |> Seq.toList |> shouldEqual [ ("hi", box "ohh") ; ("bye", box "ohh") ]
seen7 |> Seq.toList |> shouldEqual [ (88, box 29) ; (31, box 29) ]
seen8 |> Seq.toList |> shouldEqual [ ("hi", box "ohh") ; ("bye", box "ohh") ]
seen1 |> Seq.toList |> List.sort |> shouldEqual [ false ; true ]
seen2
|> Seq.toList
|> List.sort
|> shouldEqual [ (false, true) ; (true, false) ]
seen3
|> Seq.toList
|> List.sortBy fst
|> shouldEqual [ (31, box 0) ; (88, box 29) ]
seen4
|> Seq.toList
|> List.sortBy fst
|> shouldEqual [ ("bye", null) ; ("hi", box "ohh") ]
seen5
|> Seq.toList
|> List.sortBy fst
|> shouldEqual [ (31, box 29) ; (88, box 29) ]
seen6
|> Seq.toList
|> List.sortBy fst
|> shouldEqual [ ("bye", box "ohh") ; ("hi", box "ohh") ]
seen7
|> Seq.toList
|> List.sortBy fst
|> shouldEqual [ (31, box 29) ; (88, box 29) ]
seen8
|> Seq.toList
|> List.sortBy fst
|> shouldEqual [ ("bye", box "ohh") ; ("hi", box "ohh") ]
seen9
|> Seq.toList

View 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

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

@@ -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

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

@@ -88,6 +88,7 @@ type SingleTestMethod =
/// A test fixture (usually represented by the [<TestFixture>]` attribute), which may contain many tests,
/// each of which may run many times.
[<NoComparison>]
type TestFixture =
{
/// The assembly which contains this TestFixture, loaded into a separate context.

View File

@@ -7,11 +7,7 @@ open WoofWare.DotnetRuntimeLocator
/// Functions for locating .NET runtimes.
[<RequireQualifiedAccess>]
module DotnetRuntime =
let private selectRuntime
(config : RuntimeOptions)
(f : DotnetEnvironmentInfo)
: Choice<DotnetEnvironmentFrameworkInfo, DotnetEnvironmentSdkInfo> option
=
let private selectRuntime (config : RuntimeOptions) (f : DotnetEnvironmentInfo) : DirectoryInfo list =
let rollForward =
match Environment.GetEnvironmentVariable "DOTNET_ROLL_FORWARD" with
| null ->
@@ -20,6 +16,14 @@ module DotnetRuntime =
|> Option.defaultValue RollForward.Minor
| s -> RollForward.Parse s
if
Option.isSome config.IncludedFramework
|| Option.isSome config.IncludedFrameworks
then
// No need for a framework that's anywhere other than the given DLL.
[]
else
let desiredVersions =
match config.Framework with
| Some f -> [ Version f.Version, f.Name ]
@@ -66,15 +70,13 @@ module DotnetRuntime =
name, data.Installed
)
// TODO: how do we select between many available frameworks?
|> Seq.tryHead
|> Seq.toList
match available with
| Some (_, f) -> Some (Choice1Of2 f)
| None ->
// TODO: maybe we can ask the SDK. But we keep on trucking: maybe we're self-contained,
// and we'll actually find all the runtime next to the DLL.
None
// TODO: maybe we can ask the SDK if we don't have any runtimes.
// But we keep on trucking: maybe we're self-contained, and we'll actually find all the runtime next to the
// DLL.
available
|> List.map (fun (_name, runtime) -> DirectoryInfo $"%s{runtime.Path}/%s{runtime.Version}")
| _ -> failwith "non-minor RollForward not supported yet; please shout if you want it"
/// Given an executable DLL, locate the .NET runtime that can best run it.
@@ -96,9 +98,4 @@ module DotnetRuntime =
let runtime = selectRuntime runtimeConfig availableRuntimes
match runtime with
| None ->
// Keep on trucking: let's be optimistic and hope that we're self-contained.
[ dll.Directory ]
| Some (Choice1Of2 runtime) -> [ dll.Directory ; DirectoryInfo $"%s{runtime.Path}/%s{runtime.Version}" ]
| Some (Choice2Of2 sdk) -> [ dll.Directory ; DirectoryInfo sdk.Path ]
dll.Directory :: runtime

View File

@@ -2,7 +2,7 @@ namespace WoofWare.NUnitTestRunner
open System
open System.IO
open PrattParser
open WoofWare.PrattParser
// Documentation:
// https://learn.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest

View File

@@ -36,66 +36,311 @@ type TestFixtureTearDownToken = private | TestFixtureTearDownToken of TestFixtur
module private TestFixtureTearDownToken =
let vouchNoTearDownRequired (TestFixtureSetupToken tf) = TestFixtureTearDownToken tf
[<RequireQualifiedAccess>]
type private MailboxMessage =
| Quit of AsyncReplyChannel<unit>
| RunTest of ThunkCrate
/// Check current state, see if we need to start more tests, etc.
| Reconcile
| RunTest of within : TestFixture * Parallelizable<unit> option * test : ThunkCrate
| BeginTestFixture of TestFixture * AsyncReplyChannel<TestFixtureRunningToken>
| EndTestFixture of TestFixtureTearDownToken * AsyncReplyChannel<unit>
type private RunningFixture =
{
Fixture : TestFixture
RunningCanParallelize : bool
Running : Task list
Waiting : ((unit -> Task) * Parallelizable<unit> option) list
}
static member Make (f : TestFixture) =
{
Fixture = f
Running = []
RunningCanParallelize = true
Waiting = []
}
type private RunningState =
{
MaxParallelism : int
// TODO: make these efficiently look-up-able
CurrentlyRunning : RunningFixture list
Waiting : (TestFixture * AsyncReplyChannel<TestFixtureRunningToken>) list
}
member this.NewTest (tf : TestFixture) (par : Parallelizable<unit> option) (test : unit -> Task) =
{
MaxParallelism = this.MaxParallelism
Waiting = this.Waiting
CurrentlyRunning =
let found = ref 0
this.CurrentlyRunning
|> List.map (fun f ->
if Object.ReferenceEquals (f.Fixture, tf) then
Interlocked.Increment found |> ignore<int>
{ f with
Waiting = (test, par) :: f.Waiting
}
else
f
)
|> fun l ->
match found.Value with
| 1 -> l
| 0 -> failwith $"Unexpectedly did not find the running test fixture '%s{tf.Name}' to add a test to"
| _ -> failwith $"Unexpectedly found the running test fixture '%s{tf.Name}' multiple times in list"
}
member this.CompleteFixture (tf : TestFixture) : RunningState =
let rec go (acc : RunningFixture list) (running : RunningFixture list) =
match running with
| [] -> failwith "Caller has somehow called EndTestFixture while we're not running that test fixture"
| runningFixture :: tail ->
if Object.ReferenceEquals (runningFixture.Fixture, tf) then
match runningFixture.Running, runningFixture.Waiting with
| [], [] -> acc @ tail
| r, [] ->
failwith $"Caller has called EndTestFixture while its tests are still running (%i{r.Length})"
| [], r ->
failwith $"Caller has called EndTestFixture while it has tests waiting to run (%i{r.Length})"
| r, s ->
failwith
$"Caller has called EndTestFixture while it has tests waiting to run (%i{s.Length}) and test running (%i{r.Length})"
else
go (runningFixture :: acc) tail
let currentlyRunning = go [] this.CurrentlyRunning
{
CurrentlyRunning = currentlyRunning
Waiting = this.Waiting
MaxParallelism = this.MaxParallelism
}
type private MailboxState =
| Idle
| Running of TestFixture * (TestFixture * AsyncReplyChannel<TestFixtureRunningToken>) list
| Running of RunningState
/// Run some things in parallel.
/// TODO: actually implement the parallelism! Right now this just runs everything serially.
/// TODO: consume the cancellation token
type ParallelQueue
(_parallelism : int option, _scope : Parallelizable<AssemblyParallelScope> option, ?ct : CancellationToken)
(parallelism : int option, _scope : Parallelizable<AssemblyParallelScope> option, ?ct : CancellationToken)
=
let parallelism =
match parallelism with
| None -> max (Environment.ProcessorCount / 2) 2
| Some p -> p
let rec processTask (state : MailboxState) (m : MailboxProcessor<MailboxMessage>) =
async {
let! message = m.Receive ()
match message with
| Quit rc -> rc.Reply ()
| BeginTestFixture (tf, rc) ->
| MailboxMessage.Quit rc -> rc.Reply ()
| MailboxMessage.Reconcile ->
match state with
| Running (current, rest) ->
let state = Running (current, (tf, rc) :: rest)
return! processTask state m
| Idle ->
let state = Running (tf, [])
rc.Reply (TestFixtureRunningToken tf)
return! processTask state m
| EndTestFixture (TestFixtureTearDownToken tf, rc) ->
match state with
| Idle ->
return failwith "Caller has somehow called EndTestFixture while we're not running a test fixture"
| Running (current, rest) ->
if not (Object.ReferenceEquals (current, tf)) then
return
failwith
"Caller has somehow called EndTestFixture while we're not running that test fixture"
| Idle -> return! processTask state m
| Running r ->
rc.Reply ()
match rest with
match r.CurrentlyRunning with
| [] ->
match r.Waiting with
| [] -> return! processTask Idle m
| (head, rc) :: tail ->
rc.Reply (TestFixtureRunningToken head)
return! processTask (Running (head, tail)) m
| RunTest message ->
// Currently we rely on the caller to only send this message when we've given them permission through
// the StartTestFixture method returning.
{ new ThunkEvaluator<_> with
member _.Eval t rc =
use ec = ExecutionContext.Capture ()
ExecutionContext.Run (ec, (fun _ -> rc.Reply (t ())), ())
FakeUnit
}
|> message.Apply
|> function
| FakeUnit -> ()
let newRunning =
{
Fixture = head
Running = []
RunningCanParallelize = true
Waiting = []
}
let state =
{
MaxParallelism = r.MaxParallelism
CurrentlyRunning = [ newRunning ]
Waiting = tail
}
// For now, we'll just run one fixture at a time. When we run multiple fixtures in parallel,
// we probably want to call Reconcile here again.
return! processTask (Running state) m
| [ currentlyRunning ] ->
let currentlyRunningTasks =
currentlyRunning.Running |> List.filter (fun t -> not t.IsCompleted)
let r =
{ r with
CurrentlyRunning =
[
{ currentlyRunning with
Running = currentlyRunningTasks
}
]
}
match currentlyRunningTasks with
| [] ->
match currentlyRunning.Waiting with
| [] ->
// Nothing to run yet
return! processTask (Running r) m
| (head, par) :: tail ->
let par =
match par with
| None -> true
| Some Parallelizable.No -> false
| Some (Parallelizable.Yes ()) -> true
let state =
{
Fixture = currentlyRunning.Fixture
RunningCanParallelize = par
Waiting = tail
Running = [ head () ]
}
m.Post MailboxMessage.Reconcile
return!
processTask
(Running
{ r with
CurrentlyRunning = [ state ]
})
m
| currentlyRunningTasks ->
if currentlyRunningTasks.Length >= parallelism then
return! processTask (Running r) m
else
match currentlyRunning.Waiting, currentlyRunning.RunningCanParallelize with
| [], _ ->
// No new candidates.
return! processTask (Running r) m
| _, false ->
// The running test(s) can't have others added.
return! processTask (Running r) m
| (head, par) :: tail, true ->
match par with
| Some Parallelizable.No -> return! processTask (Running r) m
| Some (Parallelizable.Yes ()) ->
let state =
{
RunningState.MaxParallelism = r.MaxParallelism
Waiting = r.Waiting
CurrentlyRunning =
[
{
Fixture = currentlyRunning.Fixture
RunningCanParallelize = true
Running = head () :: currentlyRunning.Running
Waiting = tail
}
]
}
m.Post MailboxMessage.Reconcile
return! processTask (Running state) m
| None ->
match currentlyRunning.Fixture.Parallelize with
| Some Parallelizable.No
| Some (Parallelizable.Yes ClassParallelScope.Self)
| Some (Parallelizable.Yes ClassParallelScope.Fixtures) ->
// Can't add this test to the parallel queue right now
return! processTask (Running r) m
| None
| Some (Parallelizable.Yes ClassParallelScope.All)
| Some (Parallelizable.Yes ClassParallelScope.Children) ->
let state =
{
Fixture = currentlyRunning.Fixture
RunningCanParallelize = true
Waiting = tail
Running = (head ()) :: currentlyRunningTasks
}
m.Post MailboxMessage.Reconcile
return!
processTask
(Running
{ r with
CurrentlyRunning = [ state ]
})
m
| _ -> failwith "Logic error: we currently only run one fixture at a time"
| MailboxMessage.BeginTestFixture (tf, rc) ->
match state with
| Running state ->
let state =
{
MaxParallelism = state.MaxParallelism
CurrentlyRunning = state.CurrentlyRunning
Waiting = (tf, rc) :: state.Waiting
}
|> Running
m.Post MailboxMessage.Reconcile
return! processTask state m
| Idle ->
let state =
{
MaxParallelism = parallelism
CurrentlyRunning = []
Waiting = [ (tf, rc) ]
}
|> Running
m.Post MailboxMessage.Reconcile
return! processTask state m
| MailboxMessage.EndTestFixture (TestFixtureTearDownToken tf, rc) ->
match state with
| Idle ->
return failwith "Caller has somehow called EndTestFixture while we're not running a test fixture"
| Running state ->
let state = state.CompleteFixture tf
rc.Reply ()
m.Post MailboxMessage.Reconcile
return! processTask (Running state) m
| MailboxMessage.RunTest (withinFixture, par, message) ->
let t () =
{ new ThunkEvaluator<_> with
member _.Eval<'b> (t : unit -> 'b) rc =
let tcs = TaskCompletionSource ()
use ec = ExecutionContext.Capture ()
fun () ->
ExecutionContext.Run (
ec,
(fun _ ->
let result = t ()
tcs.SetResult ()
m.Post MailboxMessage.Reconcile
rc.Reply result
),
()
)
|> Task.Factory.StartNew
|> ignore<Task>
tcs.Task
}
|> message.Apply
let state =
match state with
| Idle -> failwith "somehow asked the queue to run tests when there is no active fixture"
| Running state -> state.NewTest withinFixture par t |> Running
m.Post MailboxMessage.Reconcile
return! processTask state m
}
@@ -103,50 +348,75 @@ type ParallelQueue
let mb = new MailboxProcessor<_> (processTask MailboxState.Idle)
do mb.Start ()
/// Request to run the given action on its own, not in parallel with anything else.
/// The resulting Task will return when the action has completed.
member _.NonParallel<'a> (parent : TestFixtureSetupToken) (action : unit -> 'a) : 'a Task =
ThunkCrate.make action >> RunTest |> mb.PostAndAsyncReply |> Async.StartAsTask
/// Request to run the given action, freely in parallel with other running tests.
/// The resulting Task will return when the action has completed.
member _.Parallel<'a> (parent : TestFixtureSetupToken) (action : unit -> 'a) : 'a Task =
ThunkCrate.make action >> RunTest |> mb.PostAndAsyncReply |> Async.StartAsTask
/// Request to run the given action, obeying the parallelism constraints of the parent test fixture.
/// The resulting Task will return when the action has completed.
member _.ObeyParent<'a> (tf : TestFixtureSetupToken) (action : unit -> 'a) : 'a Task =
ThunkCrate.make action >> RunTest |> mb.PostAndAsyncReply |> Async.StartAsTask
member _.Run<'a>
(TestFixtureSetupToken parent)
(scope : Parallelizable<unit> option)
(action : unit -> 'a)
: 'a Task
=
(fun rc -> MailboxMessage.RunTest (parent, scope, ThunkCrate.make action rc))
|> mb.PostAndAsyncReply
|> Async.StartAsTask
/// Declare that we wish to start the given test fixture. The resulting Task will return
/// when you are allowed to start running tests from that fixture.
/// Once you've finished running tests from that fixture, call EndTestFixture.
member _.StartTestFixture (tf : TestFixture) : Task<TestFixtureRunningToken> =
fun rc -> BeginTestFixture (tf, rc)
fun rc -> MailboxMessage.BeginTestFixture (tf, rc)
|> mb.PostAndAsyncReply
|> Async.StartAsTask
/// Run the given one-time setup for the test fixture.
member _.RunTestSetup (TestFixtureRunningToken tf) (action : unit -> 'a) : ('a * TestFixtureSetupToken) Task =
member _.RunTestSetup (TestFixtureRunningToken parent) (action : unit -> 'a) : ('a * TestFixtureSetupToken) Task =
task {
let! response = ThunkCrate.make action >> RunTest |> mb.PostAndAsyncReply
return response, TestFixtureSetupToken tf
let par =
parent.Parallelize
|> Option.map (fun p ->
match p with
| Parallelizable.No -> Parallelizable.No
| Parallelizable.Yes _ -> Parallelizable.Yes ()
)
let! response =
(fun rc -> MailboxMessage.RunTest (parent, par, ThunkCrate.make action rc))
|> mb.PostAndAsyncReply
return response, TestFixtureSetupToken parent
}
/// Run the given one-time tear-down for the test fixture.
member _.RunTestTearDown (TestFixtureSetupToken tf) (action : unit -> 'a) : ('a * TestFixtureTearDownToken) Task =
member _.RunTestTearDown
(TestFixtureSetupToken parent)
(action : unit -> 'a)
: ('a * TestFixtureTearDownToken) Task
=
task {
let! response = ThunkCrate.make action >> RunTest |> mb.PostAndAsyncReply
return response, TestFixtureTearDownToken tf
let par =
parent.Parallelize
|> Option.map (fun p ->
match p with
| Parallelizable.No -> Parallelizable.No
| Parallelizable.Yes _ -> Parallelizable.Yes ()
)
let! response =
(fun rc -> MailboxMessage.RunTest (parent, par, ThunkCrate.make action rc))
|> mb.PostAndAsyncReply
return response, TestFixtureTearDownToken parent
}
/// Declare that we have finished submitting requests to run in the given test fixture.
/// You don't need to worry about when the resulting Task returns, but we provide it just in case.
member _.EndTestFixture (tf : TestFixtureTearDownToken) : Task<unit> =
(fun rc -> EndTestFixture (tf, rc)) |> mb.PostAndAsyncReply |> Async.StartAsTask
(fun rc -> MailboxMessage.EndTestFixture (tf, rc))
|> mb.PostAndAsyncReply
|> Async.StartAsTask
interface IDisposable with
member _.Dispose () =
// Still race conditions, of course: people could still be submitting after we finish the sync.
mb.PostAndReply Quit
mb.PostAndReply MailboxMessage.Quit
(mb :> IDisposable).Dispose ()

View File

@@ -16,6 +16,8 @@ type internal RuntimeOptions =
Tfm : string
Framework : FrameworkDescription option
Frameworks : FrameworkDescription list option
IncludedFramework : FrameworkDescription option
IncludedFrameworks : FrameworkDescription list option
RollForward : string option
}

View File

@@ -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
@@ -200,9 +239,7 @@ WoofWare.NUnitTestRunner.Parallelizable`1.Tag [property]: [read-only] int
WoofWare.NUnitTestRunner.ParallelQueue inherit obj, implements IDisposable
WoofWare.NUnitTestRunner.ParallelQueue..ctor [constructor]: (int option, WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option, System.Threading.CancellationToken option)
WoofWare.NUnitTestRunner.ParallelQueue.EndTestFixture [method]: WoofWare.NUnitTestRunner.TestFixtureTearDownToken -> unit System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.NonParallel [method]: WoofWare.NUnitTestRunner.TestFixtureSetupToken -> (unit -> 'a) -> 'a System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.ObeyParent [method]: WoofWare.NUnitTestRunner.TestFixtureSetupToken -> (unit -> 'a) -> 'a System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.Parallel [method]: WoofWare.NUnitTestRunner.TestFixtureSetupToken -> (unit -> 'a) -> 'a System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.Run [method]: WoofWare.NUnitTestRunner.TestFixtureSetupToken -> unit WoofWare.NUnitTestRunner.Parallelizable option -> (unit -> 'a) -> 'a System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.RunTestSetup [method]: WoofWare.NUnitTestRunner.TestFixtureRunningToken -> (unit -> 'a) -> ('a * WoofWare.NUnitTestRunner.TestFixtureSetupToken) System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.RunTestTearDown [method]: WoofWare.NUnitTestRunner.TestFixtureSetupToken -> (unit -> 'a) -> ('a * WoofWare.NUnitTestRunner.TestFixtureTearDownToken) System.Threading.Tasks.Task
WoofWare.NUnitTestRunner.ParallelQueue.StartTestFixture [method]: WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.TestFixtureRunningToken System.Threading.Tasks.Task
@@ -228,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
@@ -358,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

View File

@@ -401,11 +401,7 @@ module TestFixture =
result, meta
let! results, summary =
match test.Parallelize with
| Some Parallelizable.No -> par.NonParallel running runMe
| Some (Parallelizable.Yes _) -> par.Parallel running runMe
| None -> par.ObeyParent running runMe
let! results, summary = par.Run running test.Parallelize runMe
match results with
| Ok results -> return Ok results, summary

View File

@@ -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

View File

@@ -14,7 +14,7 @@
<PackageId>WoofWare.NUnitTestRunner.Lib</PackageId>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarnOn>FS3559</WarnOn>
<WoofWareMyriadPluginVersion>2.1.44</WoofWareMyriadPluginVersion>
<WoofWareMyriadPluginVersion>2.1.45</WoofWareMyriadPluginVersion>
</PropertyGroup>
<ItemGroup>
@@ -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.1.2" />
<PackageReference Include="WoofWare.PrattParser" Version="0.2.2" />
<PackageReference Update="FSharp.Core" Version="6.0.0" />
<PackageReference Include="WoofWare.DotnetRuntimeLocator" Version="0.1.4" />
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="3.1.6" />
<PackageReference Include="Myriad.SDK" Version="0.8.3" />
<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" 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>

View File

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

View 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);
}
}

View File

@@ -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>

View File

@@ -0,0 +1,102 @@
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 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);
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -1,42 +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)
module Program =
let main argv =
// This is actually transcribed into C# in WoofWare.NUnitTestRunner.StartupHookLogic.
let execute argv =
let startTime = DateTimeOffset.Now
let testDll, filter, trxPath =
match argv |> List.ofSeq with
| [ dll ] -> FileInfo dll, None, None
| [ dll ; "--trx" ; trxPath ] -> FileInfo dll, None, Some (FileInfo trxPath)
| [ dll ; "--filter" ; filter ] -> FileInfo dll, Some (Filter.parse filter), None
| [ dll ; "--trx" ; trxPath ; "--filter" ; filter ] ->
FileInfo dll, Some (Filter.parse filter), Some (FileInfo trxPath)
| [ dll ; "--filter" ; filter ; "--trx" ; trxPath ] ->
FileInfo dll, Some (Filter.parse filter), Some (FileInfo trxPath)
| _ ->
failwith
"provide exactly one arg, a test DLL; then optionally `--filter <filter>` and/or `--trx <output-filename>`."
let args = argv |> List.ofArray |> Args.Parse
let filter =
match filter with
| Some filter -> Filter.shouldRun filter
match args.Filter with
| Some (_, filter) -> Filter.shouldRun filter
| None -> fun _ _ -> true
let stderr =
@@ -46,55 +26,38 @@ module Program =
let progress = Progress.spectre stderr
use _ = new SetBaseDir (testDll)
let runtime = DotnetRuntime.locate args.Dll
match args.Logging with
| LogLevel.Nothing -> ()
| LogLevel.Verbose ->
for d in runtime do
stderr.WriteLine $".NET runtime directory: %s{d.FullName}"
use contexts = TestContexts.Empty ()
let ctx = LoadContext (testDll, DotnetRuntime.locate testDll, contexts)
let assy = ctx.LoadFromAssemblyPath testDll.FullName
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>
let attrs = AssemblyLevelAttributes.get assy
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 levelOfParallelism =
match args.LevelOfParallelism, attrs.Parallelism with
| None, None -> None
| Some taken, Some ignored ->
match args.Logging with
| LogLevel.Nothing -> ()
| LogLevel.Verbose ->
stderr.WriteLine
$"Taking parallelism %i{taken} from command line, ignoring value %i{ignored} from assembly"
Some taken
| Some x, None
| None, Some x -> Some x
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
@@ -103,240 +66,19 @@ module Program =
|> List.map (TestFixture.run contexts par progress filter)
|> Task.WhenAll
if not (results.Wait (TimeSpan.FromHours 2.0)) then
let timeout =
match args.Timeout with
| None -> TimeSpan.FromHours 2.0
| Some t -> t
if not (results.Wait timeout) then
failwith "Tests failed to terminate within two hours"
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 report = BuildTrxReport.build assy creationTime startTime results
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
}
match trxPath with
match args.Trx with
| Some trxPath ->
let contents = TrxReport.toXml report |> fun d -> d.OuterXml
trxPath.Directory.Create ()
@@ -344,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.

View File

@@ -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

View File

@@ -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>

View File

@@ -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
];
};
};
});
}

View File

@@ -248,22 +248,22 @@
})
(fetchNuGet {
pname = "WoofWare.DotnetRuntimeLocator";
version = "0.1.4";
sha256 = "19pp4qlyf18g704ppbcsm1rhjqjpi84py18yljj9nx70331m8bpg";
version = "0.1.9";
sha256 = "14yc3ixcn58wy0v3pbj0hjfj4iv5k1ckig0dg1n7njx30510kzyj";
})
(fetchNuGet {
pname = "WoofWare.Myriad.Plugins";
version = "2.1.44";
sha256 = "0rp9hpkah60gd9x0ba2izr9ff1g7yhzv5a4pkhi5fbrwf5rpqpwx";
version = "2.1.45";
sha256 = "1i9s9aq8dqnxyn01sa10dd24y9i7cgv2d0rshmrkvbvbjkcnz9vs";
})
(fetchNuGet {
pname = "WoofWare.Myriad.Plugins.Attributes";
version = "3.1.6";
sha256 = "0786pr1p0nq0854mqi2cddmh185j3jihwn6azz9wiy6nxawjbrd2";
version = "3.1.7";
sha256 = "1v1wsrjh7qz2khrlbcysj50yydqc9njj09vs1jglwscjhml1wl1v";
})
(fetchNuGet {
pname = "WoofWare.PrattParser";
version = "0.1.2";
sha256 = "0spypcwsbn805yrs6grjj68ccva902lhkq93mxy32rdply1xs34q";
version = "0.2.2";
sha256 = "0cgrmd1kc3k224lsjhy5npalwg6kpqd8nx78szi1yq67kyb0farq";
})
]