mirror of
https://github.com/Smaug123/unofficial-nunit-runner
synced 2025-10-06 01:48:38 +00:00
Compare commits
15 Commits
WoofWare.N
...
WoofWare.N
Author | SHA1 | Date | |
---|---|---|---|
|
1522e3cc9c | ||
|
81c6b584a4 | ||
|
40824e06e7 | ||
|
fb945c04ac | ||
|
85cd116d52 | ||
|
8e7c54cc83 | ||
|
378a0169f8 | ||
|
870804d6ef | ||
|
9f5f22c644 | ||
|
296f230616 | ||
|
56ac203570 | ||
|
e17e769d5a | ||
|
57c34e0c4c | ||
|
7f9464b826 | ||
|
3d04199c56 |
@@ -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
|
||||
|
14
.github/workflows/assert-contents.sh
vendored
Normal file
14
.github/workflows/assert-contents.sh
vendored
Normal 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
|
159
.github/workflows/dotnet.yaml
vendored
159
.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()
|
||||
@@ -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@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2
|
||||
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@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2
|
||||
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@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2
|
||||
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@bdd51370e0416ac948727f861e03c2f05d32d78e # v1.3.2
|
||||
with:
|
||||
subject-path: "from-nuget.nupkg"
|
||||
|
||||
github-release-tool:
|
||||
runs-on: ubuntu-latest
|
||||
|
24
.github/workflows/nuget-push.sh
vendored
Normal file
24
.github/workflows/nuget-push.sh
vendored
Normal 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"
|
@@ -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,7 @@
|
||||
<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" />
|
||||
@@ -26,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
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 ()
|
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
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<PackageId>WoofWare.NUnitTestRunner.Lib</PackageId>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarnOn>FS3559</WarnOn>
|
||||
<WoofWareMyriadPluginVersion>2.1.44</WoofWareMyriadPluginVersion>
|
||||
<WoofWareMyriadPluginVersion>2.1.52</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.8" />
|
||||
<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>
|
102
WoofWare.NUnitTestRunner.StartupHookLogic/StartupHookLogic.cs
Normal file
102
WoofWare.NUnitTestRunner.StartupHookLogic/StartupHookLogic.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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,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.
|
||||
|
@@ -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
|
@@ -16,7 +16,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ApiSurface" Version="4.0.41" />
|
||||
<PackageReference Include="ApiSurface" Version="4.0.42" />
|
||||
<PackageReference Include="FsCheck" Version="3.0.0-rc3" />
|
||||
<PackageReference Include="FsUnit" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
|
@@ -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
|
||||
];
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
40
nix/deps.nix
40
nix/deps.nix
@@ -3,8 +3,8 @@
|
||||
{fetchNuGet}: [
|
||||
(fetchNuGet {
|
||||
pname = "ApiSurface";
|
||||
version = "4.0.41";
|
||||
sha256 = "03kfa5ngmgkik9lc58sp8s9rrh9g40hhgjnrv662ks0d0y2i9i89";
|
||||
version = "4.0.42";
|
||||
sha256 = "0azjv64bbbhc4rndbjhcmqxxg1bkf1v3ym3x34zmsbz0lr1hy6pv";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "fantomas";
|
||||
@@ -153,23 +153,23 @@
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NuGet.Common";
|
||||
version = "6.10.0";
|
||||
sha256 = "0nizrnilmlcqbm945293h8q3wfqfchb4xi8g50x4kjn0rbpd1kbh";
|
||||
version = "6.10.1";
|
||||
sha256 = "1z69k0j727jcwrxzmvnixdac84lb9706iabqs8mrns8j7kbmw1ns";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NuGet.Configuration";
|
||||
version = "6.10.0";
|
||||
sha256 = "1aqaknaawnqx4mnvx9qw73wvj48jjzv0d78dzwl7m9zjlrl9myhz";
|
||||
version = "6.10.1";
|
||||
sha256 = "0qy2bdi3dz6fdw7qbv77fg956idm9d9733j8b1pcrcj9pfayys26";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NuGet.Frameworks";
|
||||
version = "6.10.0";
|
||||
sha256 = "0hrd8y31zx9a0wps49czw0qgbrakb49zn3abfgylc9xrq990zkqk";
|
||||
version = "6.10.1";
|
||||
sha256 = "1p8d701fhbqv2r8vqmj948af9xvz2fd3273803cdrjy3a2wykmq1";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NuGet.Packaging";
|
||||
version = "6.10.0";
|
||||
sha256 = "18s53cvrf51lihmaqqdf48p2qi6ky1l48jv0hvbp76cxwdg7rba4";
|
||||
version = "6.10.1";
|
||||
sha256 = "0zl8xfzvd1yij2ln6iwy6cz8qfwlbyyqlin872ab5y58ws61a2x2";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NuGet.Protocol";
|
||||
@@ -178,8 +178,8 @@
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NuGet.Versioning";
|
||||
version = "6.10.0";
|
||||
sha256 = "1x19njx4x0sw9fz8y5fibi15xfsrw5avir0cx0599yd7p3ykik5g";
|
||||
version = "6.10.1";
|
||||
sha256 = "0lji7g6abnpmhzlgvni8wlb7l62n4180v3sphp4494wi0gn7ds4c";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "NUnit";
|
||||
@@ -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.52";
|
||||
sha256 = "03cx16c9zhc2yjldrks4kfh8xx3x757zjhjk98sd7i8q7s6agqyn";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "WoofWare.Myriad.Plugins.Attributes";
|
||||
version = "3.1.6";
|
||||
sha256 = "0786pr1p0nq0854mqi2cddmh185j3jihwn6azz9wiy6nxawjbrd2";
|
||||
version = "3.1.8";
|
||||
sha256 = "0xhgl5kkw5hn15ghry3k0w7h4v2d6aww9dmpb7j49aims1r6c76c";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "WoofWare.PrattParser";
|
||||
version = "0.1.2";
|
||||
sha256 = "0spypcwsbn805yrs6grjj68ccva902lhkq93mxy32rdply1xs34q";
|
||||
version = "0.2.2";
|
||||
sha256 = "0cgrmd1kc3k224lsjhy5npalwg6kpqd8nx78szi1yq67kyb0farq";
|
||||
})
|
||||
]
|
||||
|
Reference in New Issue
Block a user