mirror of
https://github.com/Smaug123/unofficial-nunit-runner
synced 2025-10-08 18:48:40 +00:00
Compare commits
11 Commits
WoofWare.N
...
WoofWare.N
Author | SHA1 | Date | |
---|---|---|---|
|
296f230616 | ||
|
56ac203570 | ||
|
e17e769d5a | ||
|
57c34e0c4c | ||
|
7f9464b826 | ||
|
3d04199c56 | ||
|
9d4b893e02 | ||
|
55e9645316 | ||
|
e9dc768449 | ||
|
e0b2d52812 | ||
|
2ed4a04f70 |
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
|
113
.github/workflows/dotnet.yaml
vendored
113
.github/workflows/dotnet.yaml
vendored
@@ -240,11 +240,53 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: echo "All required checks complete."
|
- 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
|
runs-on: ubuntu-latest
|
||||||
if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
|
if: ${{ !github.event.repository.fork && github.ref == 'refs/heads/main' }}
|
||||||
needs: [all-required-checks-complete]
|
needs: [all-required-checks-complete]
|
||||||
environment: main-deploy
|
environment: main-deploy
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Nix
|
- name: Install Nix
|
||||||
@@ -252,20 +294,73 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
extra_nix_config: |
|
extra_nix_config: |
|
||||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Download NuGet artifact (lib)
|
- name: Download NuGet artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: nuget-package-lib
|
name: nuget-package-lib
|
||||||
path: packed-lib
|
path: packed
|
||||||
- name: Publish to NuGet (lib)
|
- name: Publish to NuGet
|
||||||
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
|
id: publish-success
|
||||||
- name: Download NuGet artifact (tool)
|
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
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: nuget-package-tool
|
name: nuget-package-tool
|
||||||
path: packed-tool
|
path: packed
|
||||||
- name: Publish to NuGet (tool)
|
- name: Publish to NuGet
|
||||||
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
|
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:
|
github-release-tool:
|
||||||
runs-on: ubuntu-latest
|
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"
|
@@ -10,7 +10,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="NoAttribute.fs" />
|
<Compile Include="NoAttribute.fs" />
|
||||||
<Compile Include="Inconclusive.fs" />
|
<Compile Include="Inconclusive.fs" />
|
||||||
|
<Compile Include="TestNonParallel.fs" />
|
||||||
<Compile Include="TestParallel.fs" />
|
<Compile Include="TestParallel.fs" />
|
||||||
|
<Compile Include="TestStdout.fs" />
|
||||||
<Compile Include="TestParameterisedFixture.fs" />
|
<Compile Include="TestParameterisedFixture.fs" />
|
||||||
<Compile Include="TestSetUp.fs" />
|
<Compile Include="TestSetUp.fs" />
|
||||||
<Compile Include="TestValues.fs" />
|
<Compile Include="TestValues.fs" />
|
||||||
|
19
Consumer/TestNonParallel.fs
Normal file
19
Consumer/TestNonParallel.fs
Normal 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
|
@@ -1,5 +1,7 @@
|
|||||||
namespace Consumer
|
namespace Consumer
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Threading
|
||||||
open NUnit.Framework
|
open NUnit.Framework
|
||||||
open FsUnitTyped
|
open FsUnitTyped
|
||||||
|
|
||||||
@@ -7,43 +9,58 @@ open FsUnitTyped
|
|||||||
[<Parallelizable>]
|
[<Parallelizable>]
|
||||||
module TestParallelDefault =
|
module TestParallelDefault =
|
||||||
|
|
||||||
let defaults = List.init 100 id
|
let defaults = List.init 60 id
|
||||||
|
|
||||||
[<TestCaseSource(nameof defaults)>]
|
[<TestCaseSource(nameof defaults)>]
|
||||||
let ``Default thing`` (i : int) = i |> shouldEqual i
|
let ``Default thing, no scope`` (i : int) =
|
||||||
|
Console.WriteLine i
|
||||||
|
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
|
||||||
|
i |> shouldEqual i
|
||||||
|
|
||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
[<Parallelizable(ParallelScope.All)>]
|
[<Parallelizable(ParallelScope.All)>]
|
||||||
module TestParallelAllScope =
|
module TestParallelAllScope =
|
||||||
|
|
||||||
let defaults = List.init 100 id
|
let defaults = List.init 60 id
|
||||||
|
|
||||||
[<TestCaseSource(nameof defaults)>]
|
[<TestCaseSource(nameof defaults)>]
|
||||||
let ``Default thing`` (i : int) = i |> shouldEqual i
|
let ``Thing, all scope`` (i : int) =
|
||||||
|
Console.WriteLine i
|
||||||
|
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
|
||||||
|
i |> shouldEqual i
|
||||||
|
|
||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
[<Parallelizable(ParallelScope.Self)>]
|
[<Parallelizable(ParallelScope.Self)>]
|
||||||
module TestParallelSelfScope =
|
module TestParallelSelfScope =
|
||||||
|
|
||||||
let defaults = List.init 100 id
|
let defaults = List.init 60 id
|
||||||
|
|
||||||
[<TestCaseSource(nameof defaults)>]
|
[<TestCaseSource(nameof defaults)>]
|
||||||
let ``Default thing`` (i : int) = i |> shouldEqual i
|
let ``Thing, self scope`` (i : int) =
|
||||||
|
Console.WriteLine i
|
||||||
|
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
|
||||||
|
i |> shouldEqual i
|
||||||
|
|
||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
[<Parallelizable(ParallelScope.Children)>]
|
[<Parallelizable(ParallelScope.Children)>]
|
||||||
module TestParallelChildrenScope =
|
module TestParallelChildrenScope =
|
||||||
|
|
||||||
let defaults = List.init 100 id
|
let defaults = List.init 60 id
|
||||||
|
|
||||||
[<TestCaseSource(nameof defaults)>]
|
[<TestCaseSource(nameof defaults)>]
|
||||||
let ``Default thing`` (i : int) = i |> shouldEqual i
|
let ``Thing, children scope`` (i : int) =
|
||||||
|
Console.WriteLine i
|
||||||
|
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
|
||||||
|
i |> shouldEqual i
|
||||||
|
|
||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
[<Parallelizable(ParallelScope.Fixtures)>]
|
[<Parallelizable(ParallelScope.Fixtures)>]
|
||||||
module TestParallelFixturesScope =
|
module TestParallelFixturesScope =
|
||||||
|
|
||||||
let defaults = List.init 100 id
|
let defaults = List.init 60 id
|
||||||
|
|
||||||
[<TestCaseSource(nameof defaults)>]
|
[<TestCaseSource(nameof defaults)>]
|
||||||
let ``Default thing`` (i : int) = i |> shouldEqual i
|
let ``Thing, fixtures scope`` (i : int) =
|
||||||
|
Console.WriteLine i
|
||||||
|
Thread.Sleep (TimeSpan.FromMilliseconds (float i))
|
||||||
|
i |> shouldEqual i
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
namespace Consumer
|
namespace Consumer
|
||||||
|
|
||||||
|
open System
|
||||||
open FsUnitTyped
|
open FsUnitTyped
|
||||||
open System.Threading
|
open System.Threading
|
||||||
open NUnit.Framework
|
open NUnit.Framework
|
||||||
@@ -11,6 +12,8 @@ module TestSetUp =
|
|||||||
|
|
||||||
[<OneTimeSetUp>]
|
[<OneTimeSetUp>]
|
||||||
let oneTimeSetUp () =
|
let oneTimeSetUp () =
|
||||||
|
Console.WriteLine "I'm being set up for the first time!"
|
||||||
|
|
||||||
if Interlocked.Increment haveOneTimeSetUp <> 1 then
|
if Interlocked.Increment haveOneTimeSetUp <> 1 then
|
||||||
failwith "one time setup happened more than once"
|
failwith "one time setup happened more than once"
|
||||||
|
|
||||||
@@ -22,12 +25,14 @@ module TestSetUp =
|
|||||||
|
|
||||||
[<SetUp>]
|
[<SetUp>]
|
||||||
let setUp () =
|
let setUp () =
|
||||||
|
Console.WriteLine "It's a set-up!"
|
||||||
haveOneTimeSetUp.Value |> shouldEqual 1
|
haveOneTimeSetUp.Value |> shouldEqual 1
|
||||||
let newId = Interlocked.Increment setUpTimes
|
let newId = Interlocked.Increment setUpTimes
|
||||||
lock setUpTimesSeen (fun () -> setUpTimesSeen.Add newId)
|
lock setUpTimesSeen (fun () -> setUpTimesSeen.Add newId)
|
||||||
|
|
||||||
[<TearDown>]
|
[<TearDown>]
|
||||||
let tearDown () =
|
let tearDown () =
|
||||||
|
Console.WriteLine "I'm a tear-down!"
|
||||||
let newId = Interlocked.Increment tearDownTimes
|
let newId = Interlocked.Increment tearDownTimes
|
||||||
lock tearDownTimesSeen (fun () -> tearDownTimesSeen.Add newId)
|
lock tearDownTimesSeen (fun () -> tearDownTimesSeen.Add newId)
|
||||||
|
|
||||||
@@ -35,19 +40,23 @@ module TestSetUp =
|
|||||||
|
|
||||||
[<OneTimeTearDown>]
|
[<OneTimeTearDown>]
|
||||||
let oneTimeTearDown () =
|
let oneTimeTearDown () =
|
||||||
|
Console.WriteLine "I'm being torn down, finally!"
|
||||||
|
|
||||||
if Interlocked.Increment haveOneTimeTearDown <> 1 then
|
if Interlocked.Increment haveOneTimeTearDown <> 1 then
|
||||||
failwith "one time tear down happened more than once"
|
failwith "one time tear down happened more than once"
|
||||||
|
|
||||||
setUpTimesSeen
|
setUpTimesSeen
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|
|> List.sort
|
||||||
// Six tests: one for Test, two for the TestCase, three for the Repeat.
|
// Six tests: one for Test, two for the TestCase, three for the Repeat.
|
||||||
|> shouldEqual [ 1..6 ]
|
|> shouldEqual [ 1..6 ]
|
||||||
|
|
||||||
tearDownTimesSeen |> Seq.toList |> shouldEqual [ 1..6 ]
|
tearDownTimesSeen |> Seq.toList |> List.sort |> shouldEqual [ 1..6 ]
|
||||||
|
|
||||||
[<Test>]
|
[<Test>]
|
||||||
let ``Test 1`` () =
|
let ``Test 1`` () =
|
||||||
haveOneTimeTearDown.Value |> shouldEqual 0
|
haveOneTimeTearDown.Value |> shouldEqual 0
|
||||||
|
Console.WriteLine "By the way, I'm test 1"
|
||||||
1 |> shouldEqual 1
|
1 |> shouldEqual 1
|
||||||
|
|
||||||
[<TestCase "h">]
|
[<TestCase "h">]
|
||||||
|
13
Consumer/TestStdout.fs
Normal file
13
Consumer/TestStdout.fs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Consumer
|
||||||
|
|
||||||
|
open System
|
||||||
|
open NUnit.Framework
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestStdout =
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Stdout is redirected`` () =
|
||||||
|
Console.Out.WriteLine "Hi!"
|
||||||
|
Console.WriteLine "Hi! part 2"
|
||||||
|
Console.Error.WriteLine "Bye!"
|
@@ -86,14 +86,42 @@ module TestValues =
|
|||||||
|
|
||||||
[<OneTimeTearDown>]
|
[<OneTimeTearDown>]
|
||||||
let ``Values are all OK`` () =
|
let ``Values are all OK`` () =
|
||||||
seen1 |> Seq.toList |> shouldEqual [ true ; false ]
|
seen1 |> Seq.toList |> List.sort |> shouldEqual [ false ; true ]
|
||||||
seen2 |> Seq.toList |> shouldEqual [ (true, false) ; (false, true) ]
|
|
||||||
seen3 |> Seq.toList |> shouldEqual [ (88, box 29) ; (31, box 0) ]
|
seen2
|
||||||
seen4 |> Seq.toList |> shouldEqual [ ("hi", box "ohh") ; ("bye", null) ]
|
|> Seq.toList
|
||||||
seen5 |> Seq.toList |> shouldEqual [ (88, box 29) ; (31, box 29) ]
|
|> List.sort
|
||||||
seen6 |> Seq.toList |> shouldEqual [ ("hi", box "ohh") ; ("bye", box "ohh") ]
|
|> shouldEqual [ (false, true) ; (true, false) ]
|
||||||
seen7 |> Seq.toList |> shouldEqual [ (88, box 29) ; (31, box 29) ]
|
|
||||||
seen8 |> Seq.toList |> shouldEqual [ ("hi", box "ohh") ; ("bye", box "ohh") ]
|
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
|
seen9
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|
180
WoofWare.NUnitTestRunner.Lib/Context.fs
Normal file
180
WoofWare.NUnitTestRunner.Lib/Context.fs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
namespace WoofWare.NUnitTestRunner
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Collections.Generic
|
||||||
|
open System.IO
|
||||||
|
open System.Reflection
|
||||||
|
open System.Runtime.Loader
|
||||||
|
open System.Text
|
||||||
|
open System.Threading
|
||||||
|
|
||||||
|
type internal OutputStreamId = | OutputStreamId of Guid
|
||||||
|
|
||||||
|
type private ThreadAwareWriter
|
||||||
|
(
|
||||||
|
local : AsyncLocal<OutputStreamId>,
|
||||||
|
underlying : Dictionary<OutputStreamId, TextWriter>,
|
||||||
|
mem : Dictionary<OutputStreamId, MemoryStream>
|
||||||
|
)
|
||||||
|
=
|
||||||
|
inherit TextWriter ()
|
||||||
|
override _.get_Encoding () = Encoding.Default
|
||||||
|
|
||||||
|
override this.Write (v : char) : unit =
|
||||||
|
use prev = ExecutionContext.Capture ()
|
||||||
|
|
||||||
|
(fun _ ->
|
||||||
|
(fun () ->
|
||||||
|
match underlying.TryGetValue local.Value with
|
||||||
|
| true, output -> output.Write v
|
||||||
|
| false, _ ->
|
||||||
|
let wanted =
|
||||||
|
underlying |> Seq.map (fun (KeyValue (a, b)) -> $"%O{a}") |> String.concat "\n"
|
||||||
|
|
||||||
|
failwith $"no such context: %O{local.Value}\nwanted:\n"
|
||||||
|
)
|
||||||
|
|> lock underlying
|
||||||
|
)
|
||||||
|
|> fun action -> ExecutionContext.Run (prev, action, ())
|
||||||
|
|
||||||
|
override this.WriteLine (v : string) : unit =
|
||||||
|
use prev = ExecutionContext.Capture ()
|
||||||
|
|
||||||
|
(fun _ ->
|
||||||
|
(fun () ->
|
||||||
|
match underlying.TryGetValue local.Value with
|
||||||
|
| true, output -> output.WriteLine v
|
||||||
|
| false, _ ->
|
||||||
|
let wanted =
|
||||||
|
underlying |> Seq.map (fun (KeyValue (a, b)) -> $"%O{a}") |> String.concat "\n"
|
||||||
|
|
||||||
|
failwith $"no such context: %O{local.Value}\nwanted:\n"
|
||||||
|
)
|
||||||
|
|> lock underlying
|
||||||
|
)
|
||||||
|
|> fun action -> ExecutionContext.Run (prev, action, ())
|
||||||
|
|
||||||
|
/// Wraps up the necessary context to intercept global state.
|
||||||
|
type TestContexts =
|
||||||
|
private
|
||||||
|
{
|
||||||
|
/// Accesses to this must be locked on StdOutWriters.
|
||||||
|
StdOuts : Dictionary<OutputStreamId, MemoryStream>
|
||||||
|
/// Accesses to this must be locked on StdErrWriters.
|
||||||
|
StdErrs : Dictionary<OutputStreamId, MemoryStream>
|
||||||
|
StdOutWriters : Dictionary<OutputStreamId, TextWriter>
|
||||||
|
StdErrWriters : Dictionary<OutputStreamId, TextWriter>
|
||||||
|
StdOutWriter : TextWriter
|
||||||
|
StdErrWriter : TextWriter
|
||||||
|
AsyncLocal : AsyncLocal<OutputStreamId>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this exactly once.
|
||||||
|
static member Empty () =
|
||||||
|
let stdouts = Dictionary ()
|
||||||
|
let stderrs = Dictionary ()
|
||||||
|
let stdoutWriters = Dictionary ()
|
||||||
|
let stderrWriters = Dictionary ()
|
||||||
|
let local = AsyncLocal ()
|
||||||
|
let stdoutWriter = new ThreadAwareWriter (local, stdoutWriters, stdouts)
|
||||||
|
let stderrWriter = new ThreadAwareWriter (local, stderrWriters, stderrs)
|
||||||
|
|
||||||
|
{
|
||||||
|
StdOuts = stdouts
|
||||||
|
StdErrs = stderrs
|
||||||
|
StdOutWriter = stdoutWriter
|
||||||
|
StdErrWriter = stderrWriter
|
||||||
|
StdOutWriters = stdoutWriters
|
||||||
|
StdErrWriters = stderrWriters
|
||||||
|
AsyncLocal = local
|
||||||
|
}
|
||||||
|
|
||||||
|
member internal this.Stdout : TextWriter = this.StdOutWriter
|
||||||
|
member internal this.Stderr : TextWriter = this.StdErrWriter
|
||||||
|
|
||||||
|
member internal this.DumpStdout (id : OutputStreamId) : string =
|
||||||
|
lock
|
||||||
|
this.StdOutWriters
|
||||||
|
(fun () ->
|
||||||
|
this.StdOutWriters.[id].Flush ()
|
||||||
|
this.StdOuts.[id].ToArray ()
|
||||||
|
)
|
||||||
|
|> Encoding.Default.GetString
|
||||||
|
|
||||||
|
member internal this.DumpStderr (id : OutputStreamId) : string =
|
||||||
|
lock
|
||||||
|
this.StdErrWriters
|
||||||
|
(fun () ->
|
||||||
|
this.StdErrWriters.[id].Flush ()
|
||||||
|
this.StdErrs.[id].ToArray ()
|
||||||
|
)
|
||||||
|
|> Encoding.Default.GetString
|
||||||
|
|
||||||
|
member internal this.NewOutputs () =
|
||||||
|
let id = Guid.NewGuid () |> OutputStreamId
|
||||||
|
let msOut = new MemoryStream ()
|
||||||
|
let wrOut = new StreamWriter (msOut)
|
||||||
|
let msErr = new MemoryStream ()
|
||||||
|
let wrErr = new StreamWriter (msErr)
|
||||||
|
|
||||||
|
lock
|
||||||
|
this.StdOutWriters
|
||||||
|
(fun () ->
|
||||||
|
this.StdOutWriters.Add (id, wrOut)
|
||||||
|
this.StdOuts.Add (id, msOut)
|
||||||
|
)
|
||||||
|
|
||||||
|
lock
|
||||||
|
this.StdErrWriters
|
||||||
|
(fun () ->
|
||||||
|
this.StdErrWriters.Add (id, wrErr)
|
||||||
|
this.StdErrs.Add (id, msErr)
|
||||||
|
)
|
||||||
|
|
||||||
|
id
|
||||||
|
|
||||||
|
interface IDisposable with
|
||||||
|
member this.Dispose () =
|
||||||
|
// TODO: dispose the streams
|
||||||
|
()
|
||||||
|
|
||||||
|
/// A separate AssemblyLoadContext within which you can run the tests in the given DLL.
|
||||||
|
/// Supply places to find the .NET runtimes.
|
||||||
|
type LoadContext (dll : FileInfo, runtimes : DirectoryInfo list, contexts : TestContexts) =
|
||||||
|
inherit AssemblyLoadContext ()
|
||||||
|
|
||||||
|
/// Load the assembly with the given name into this assembly context.
|
||||||
|
/// This additionally monkey-patches System.Console: it performs SetOut and SetError on them
|
||||||
|
/// so that they redirect their outputs into the given `TestContexts`.
|
||||||
|
override this.Load (target : AssemblyName) : Assembly =
|
||||||
|
let path = Path.Combine (dll.Directory.FullName, $"%s{target.Name}.dll")
|
||||||
|
|
||||||
|
let assy =
|
||||||
|
if File.Exists path then
|
||||||
|
this.LoadFromAssemblyPath path
|
||||||
|
else
|
||||||
|
|
||||||
|
runtimes
|
||||||
|
|> List.tryPick (fun di ->
|
||||||
|
let path = Path.Combine (di.FullName, $"%s{target.Name}.dll")
|
||||||
|
|
||||||
|
if File.Exists path then
|
||||||
|
this.LoadFromAssemblyPath path |> Some
|
||||||
|
else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|> Option.defaultValue null
|
||||||
|
|
||||||
|
if target.Name = "System.Console" then
|
||||||
|
if isNull assy then
|
||||||
|
failwith "could not monkey-patch System.Console"
|
||||||
|
else
|
||||||
|
let consoleType = assy.GetType "System.Console"
|
||||||
|
let setOut = consoleType.GetMethod "SetOut"
|
||||||
|
setOut.Invoke ((null : obj), [| contexts.Stdout |]) |> unbox<unit>
|
||||||
|
let setErr = consoleType.GetMethod "SetError"
|
||||||
|
setErr.Invoke ((null : obj), [| contexts.Stderr |]) |> unbox<unit>
|
||||||
|
|
||||||
|
assy
|
||||||
|
else
|
||||||
|
assy
|
@@ -88,6 +88,7 @@ type SingleTestMethod =
|
|||||||
|
|
||||||
/// A test fixture (usually represented by the [<TestFixture>]` attribute), which may contain many tests,
|
/// A test fixture (usually represented by the [<TestFixture>]` attribute), which may contain many tests,
|
||||||
/// each of which may run many times.
|
/// each of which may run many times.
|
||||||
|
[<NoComparison>]
|
||||||
type TestFixture =
|
type TestFixture =
|
||||||
{
|
{
|
||||||
/// The assembly which contains this TestFixture, loaded into a separate context.
|
/// The assembly which contains this TestFixture, loaded into a separate context.
|
||||||
|
@@ -7,11 +7,7 @@ open WoofWare.DotnetRuntimeLocator
|
|||||||
/// Functions for locating .NET runtimes.
|
/// Functions for locating .NET runtimes.
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module DotnetRuntime =
|
module DotnetRuntime =
|
||||||
let private selectRuntime
|
let private selectRuntime (config : RuntimeOptions) (f : DotnetEnvironmentInfo) : DirectoryInfo list =
|
||||||
(config : RuntimeOptions)
|
|
||||||
(f : DotnetEnvironmentInfo)
|
|
||||||
: Choice<DotnetEnvironmentFrameworkInfo, DotnetEnvironmentSdkInfo> option
|
|
||||||
=
|
|
||||||
let rollForward =
|
let rollForward =
|
||||||
match Environment.GetEnvironmentVariable "DOTNET_ROLL_FORWARD" with
|
match Environment.GetEnvironmentVariable "DOTNET_ROLL_FORWARD" with
|
||||||
| null ->
|
| null ->
|
||||||
@@ -20,6 +16,14 @@ module DotnetRuntime =
|
|||||||
|> Option.defaultValue RollForward.Minor
|
|> Option.defaultValue RollForward.Minor
|
||||||
| s -> RollForward.Parse s
|
| 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 =
|
let desiredVersions =
|
||||||
match config.Framework with
|
match config.Framework with
|
||||||
| Some f -> [ Version f.Version, f.Name ]
|
| Some f -> [ Version f.Version, f.Name ]
|
||||||
@@ -66,15 +70,13 @@ module DotnetRuntime =
|
|||||||
|
|
||||||
name, data.Installed
|
name, data.Installed
|
||||||
)
|
)
|
||||||
// TODO: how do we select between many available frameworks?
|
|> Seq.toList
|
||||||
|> Seq.tryHead
|
|
||||||
|
|
||||||
match available with
|
// TODO: maybe we can ask the SDK if we don't have any runtimes.
|
||||||
| Some (_, f) -> Some (Choice1Of2 f)
|
// But we keep on trucking: maybe we're self-contained, and we'll actually find all the runtime next to the
|
||||||
| None ->
|
// DLL.
|
||||||
// TODO: maybe we can ask the SDK. But we keep on trucking: maybe we're self-contained,
|
available
|
||||||
// and we'll actually find all the runtime next to the DLL.
|
|> List.map (fun (_name, runtime) -> DirectoryInfo $"%s{runtime.Path}/%s{runtime.Version}")
|
||||||
None
|
|
||||||
| _ -> failwith "non-minor RollForward not supported yet; please shout if you want it"
|
| _ -> 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.
|
/// Given an executable DLL, locate the .NET runtime that can best run it.
|
||||||
@@ -96,9 +98,4 @@ module DotnetRuntime =
|
|||||||
|
|
||||||
let runtime = selectRuntime runtimeConfig availableRuntimes
|
let runtime = selectRuntime runtimeConfig availableRuntimes
|
||||||
|
|
||||||
match runtime with
|
dll.Directory :: runtime
|
||||||
| 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 ]
|
|
||||||
|
@@ -2,7 +2,7 @@ namespace WoofWare.NUnitTestRunner
|
|||||||
|
|
||||||
open System
|
open System
|
||||||
open System.IO
|
open System.IO
|
||||||
open PrattParser
|
open WoofWare.PrattParser
|
||||||
|
|
||||||
// Documentation:
|
// Documentation:
|
||||||
// https://learn.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest
|
// https://learn.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest
|
||||||
|
422
WoofWare.NUnitTestRunner.Lib/ParallelQueue.fs
Normal file
422
WoofWare.NUnitTestRunner.Lib/ParallelQueue.fs
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
namespace WoofWare.NUnitTestRunner
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Threading
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
type private ThunkEvaluator<'ret> =
|
||||||
|
abstract Eval<'a> : (unit -> 'a) -> AsyncReplyChannel<'a> -> 'ret
|
||||||
|
|
||||||
|
type private ThunkCrate =
|
||||||
|
abstract Apply<'ret> : ThunkEvaluator<'ret> -> 'ret
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module private ThunkCrate =
|
||||||
|
let make<'a> (t : unit -> 'a) (rc : AsyncReplyChannel<'a>) : ThunkCrate =
|
||||||
|
{ new ThunkCrate with
|
||||||
|
member _.Apply e = e.Eval t rc
|
||||||
|
}
|
||||||
|
|
||||||
|
type private FakeUnit = FakeUnit
|
||||||
|
|
||||||
|
/// A handle to a running test fixture.
|
||||||
|
type TestFixtureRunningToken = private | TestFixtureRunningToken of TestFixture
|
||||||
|
|
||||||
|
/// A handle to a test fixture whose setup method has been called.
|
||||||
|
type TestFixtureSetupToken = private | TestFixtureSetupToken of TestFixture
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module private TestFixtureSetupToken =
|
||||||
|
let vouchNoSetupRequired (TestFixtureRunningToken tf) = TestFixtureSetupToken tf
|
||||||
|
|
||||||
|
/// A handle to a test fixture whose setup method has been called.
|
||||||
|
type TestFixtureTearDownToken = private | TestFixtureTearDownToken of TestFixture
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module private TestFixtureTearDownToken =
|
||||||
|
let vouchNoTearDownRequired (TestFixtureSetupToken tf) = TestFixtureTearDownToken tf
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type private MailboxMessage =
|
||||||
|
| Quit of AsyncReplyChannel<unit>
|
||||||
|
/// 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 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)
|
||||||
|
=
|
||||||
|
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
|
||||||
|
| MailboxMessage.Quit rc -> rc.Reply ()
|
||||||
|
| MailboxMessage.Reconcile ->
|
||||||
|
match state with
|
||||||
|
| Idle -> return! processTask state m
|
||||||
|
| Running r ->
|
||||||
|
|
||||||
|
match r.CurrentlyRunning with
|
||||||
|
| [] ->
|
||||||
|
match r.Waiting with
|
||||||
|
| [] -> return! processTask Idle m
|
||||||
|
| (head, rc) :: tail ->
|
||||||
|
rc.Reply (TestFixtureRunningToken head)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
let mb = new MailboxProcessor<_> (processTask MailboxState.Idle)
|
||||||
|
do mb.Start ()
|
||||||
|
|
||||||
|
/// Request to run the given action, freely in parallel with other running tests.
|
||||||
|
/// The resulting Task will return when the action has completed.
|
||||||
|
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 -> MailboxMessage.BeginTestFixture (tf, rc)
|
||||||
|
|> mb.PostAndAsyncReply
|
||||||
|
|> Async.StartAsTask
|
||||||
|
|
||||||
|
/// Run the given one-time setup for the test fixture.
|
||||||
|
member _.RunTestSetup (TestFixtureRunningToken parent) (action : unit -> 'a) : ('a * TestFixtureSetupToken) Task =
|
||||||
|
task {
|
||||||
|
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 parent)
|
||||||
|
(action : unit -> 'a)
|
||||||
|
: ('a * TestFixtureTearDownToken) Task
|
||||||
|
=
|
||||||
|
task {
|
||||||
|
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 -> 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 MailboxMessage.Quit
|
||||||
|
(mb :> IDisposable).Dispose ()
|
@@ -16,6 +16,8 @@ type internal RuntimeOptions =
|
|||||||
Tfm : string
|
Tfm : string
|
||||||
Framework : FrameworkDescription option
|
Framework : FrameworkDescription option
|
||||||
Frameworks : FrameworkDescription list option
|
Frameworks : FrameworkDescription list option
|
||||||
|
IncludedFramework : FrameworkDescription option
|
||||||
|
IncludedFrameworks : FrameworkDescription list option
|
||||||
RollForward : string option
|
RollForward : string option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -143,6 +143,8 @@ WoofWare.NUnitTestRunner.ITestProgress.OnTestFixtureStart [method]: string -> in
|
|||||||
WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberFinished [method]: string -> unit
|
WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberFinished [method]: string -> unit
|
||||||
WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberSkipped [method]: string -> unit
|
WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberSkipped [method]: string -> unit
|
||||||
WoofWare.NUnitTestRunner.ITestProgress.OnTestMemberStart [method]: string -> unit
|
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.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 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 inherit WoofWare.NUnitTestRunner.Match
|
||||||
WoofWare.NUnitTestRunner.Match+Contains.get_Item [method]: unit -> string
|
WoofWare.NUnitTestRunner.Match+Contains.get_Item [method]: unit -> string
|
||||||
@@ -195,6 +197,13 @@ WoofWare.NUnitTestRunner.Parallelizable`1.IsYes [property]: [read-only] bool
|
|||||||
WoofWare.NUnitTestRunner.Parallelizable`1.NewYes [static method]: 'scope -> 'scope WoofWare.NUnitTestRunner.Parallelizable
|
WoofWare.NUnitTestRunner.Parallelizable`1.NewYes [static method]: 'scope -> 'scope WoofWare.NUnitTestRunner.Parallelizable
|
||||||
WoofWare.NUnitTestRunner.Parallelizable`1.No [static property]: [read-only] 'scope WoofWare.NUnitTestRunner.Parallelizable
|
WoofWare.NUnitTestRunner.Parallelizable`1.No [static property]: [read-only] 'scope WoofWare.NUnitTestRunner.Parallelizable
|
||||||
WoofWare.NUnitTestRunner.Parallelizable`1.Tag [property]: [read-only] int
|
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.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
|
||||||
WoofWare.NUnitTestRunner.SingleTestMethod inherit obj, implements WoofWare.NUnitTestRunner.SingleTestMethod System.IEquatable, System.Collections.IStructuralEquatable
|
WoofWare.NUnitTestRunner.SingleTestMethod inherit obj, implements WoofWare.NUnitTestRunner.SingleTestMethod System.IEquatable, System.Collections.IStructuralEquatable
|
||||||
WoofWare.NUnitTestRunner.SingleTestMethod..ctor [constructor]: (System.Reflection.MethodInfo, WoofWare.NUnitTestRunner.TestKind, WoofWare.NUnitTestRunner.Modifier list, string list, int option, WoofWare.NUnitTestRunner.Combinatorial option, unit WoofWare.NUnitTestRunner.Parallelizable option)
|
WoofWare.NUnitTestRunner.SingleTestMethod..ctor [constructor]: (System.Reflection.MethodInfo, WoofWare.NUnitTestRunner.TestKind, WoofWare.NUnitTestRunner.Modifier list, string list, int option, WoofWare.NUnitTestRunner.Combinatorial option, unit WoofWare.NUnitTestRunner.Parallelizable option)
|
||||||
WoofWare.NUnitTestRunner.SingleTestMethod.Categories [property]: [read-only] string list
|
WoofWare.NUnitTestRunner.SingleTestMethod.Categories [property]: [read-only] string list
|
||||||
@@ -215,6 +224,8 @@ WoofWare.NUnitTestRunner.SingleTestMethod.Parallelize [property]: [read-only] un
|
|||||||
WoofWare.NUnitTestRunner.SingleTestMethod.Repeat [property]: [read-only] int option
|
WoofWare.NUnitTestRunner.SingleTestMethod.Repeat [property]: [read-only] int option
|
||||||
WoofWare.NUnitTestRunner.SingleTestMethodModule inherit obj
|
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.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.TestFailure inherit obj, implements WoofWare.NUnitTestRunner.TestFailure System.IEquatable, System.Collections.IStructuralEquatable - union type with 3 cases
|
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 inherit WoofWare.NUnitTestRunner.TestFailure
|
||||||
WoofWare.NUnitTestRunner.TestFailure+SetUpFailed.get_Item [method]: unit -> WoofWare.NUnitTestRunner.UserMethodFailure
|
WoofWare.NUnitTestRunner.TestFailure+SetUpFailed.get_Item [method]: unit -> WoofWare.NUnitTestRunner.UserMethodFailure
|
||||||
@@ -267,7 +278,11 @@ WoofWare.NUnitTestRunner.TestFixture.Tests [property]: [read-only] WoofWare.NUni
|
|||||||
WoofWare.NUnitTestRunner.TestFixture.Type [property]: [read-only] System.Type
|
WoofWare.NUnitTestRunner.TestFixture.Type [property]: [read-only] System.Type
|
||||||
WoofWare.NUnitTestRunner.TestFixtureModule inherit obj
|
WoofWare.NUnitTestRunner.TestFixtureModule inherit obj
|
||||||
WoofWare.NUnitTestRunner.TestFixtureModule.parse [static method]: System.Type -> WoofWare.NUnitTestRunner.TestFixture
|
WoofWare.NUnitTestRunner.TestFixtureModule.parse [static method]: System.Type -> WoofWare.NUnitTestRunner.TestFixture
|
||||||
WoofWare.NUnitTestRunner.TestFixtureModule.run [static method]: WoofWare.NUnitTestRunner.ITestProgress -> (WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.SingleTestMethod -> bool) -> WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.FixtureRunResults list
|
WoofWare.NUnitTestRunner.TestFixtureModule.run [static method]: WoofWare.NUnitTestRunner.TestContexts -> WoofWare.NUnitTestRunner.ParallelQueue -> WoofWare.NUnitTestRunner.ITestProgress -> (WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.SingleTestMethod -> bool) -> WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.FixtureRunResults list System.Threading.Tasks.Task
|
||||||
|
WoofWare.NUnitTestRunner.TestFixtureModule.runOneFixture [static method]: WoofWare.NUnitTestRunner.TestContexts -> WoofWare.NUnitTestRunner.ParallelQueue -> WoofWare.NUnitTestRunner.ITestProgress -> (WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.SingleTestMethod -> bool) -> string -> obj -> WoofWare.NUnitTestRunner.TestFixture -> WoofWare.NUnitTestRunner.FixtureRunResults System.Threading.Tasks.Task
|
||||||
|
WoofWare.NUnitTestRunner.TestFixtureRunningToken inherit obj, implements WoofWare.NUnitTestRunner.TestFixtureRunningToken System.IEquatable, System.Collections.IStructuralEquatable
|
||||||
|
WoofWare.NUnitTestRunner.TestFixtureSetupToken inherit obj, implements WoofWare.NUnitTestRunner.TestFixtureSetupToken System.IEquatable, System.Collections.IStructuralEquatable
|
||||||
|
WoofWare.NUnitTestRunner.TestFixtureTearDownToken inherit obj, implements WoofWare.NUnitTestRunner.TestFixtureTearDownToken System.IEquatable, System.Collections.IStructuralEquatable
|
||||||
WoofWare.NUnitTestRunner.TestKind inherit obj, implements WoofWare.NUnitTestRunner.TestKind System.IEquatable, System.Collections.IStructuralEquatable - union type with 3 cases
|
WoofWare.NUnitTestRunner.TestKind inherit obj, implements WoofWare.NUnitTestRunner.TestKind System.IEquatable, System.Collections.IStructuralEquatable - union type with 3 cases
|
||||||
WoofWare.NUnitTestRunner.TestKind+Data inherit WoofWare.NUnitTestRunner.TestKind
|
WoofWare.NUnitTestRunner.TestKind+Data inherit WoofWare.NUnitTestRunner.TestKind
|
||||||
WoofWare.NUnitTestRunner.TestKind+Data.get_Item [method]: unit -> obj list list
|
WoofWare.NUnitTestRunner.TestKind+Data.get_Item [method]: unit -> obj list list
|
||||||
|
@@ -6,21 +6,9 @@ open System.Diagnostics
|
|||||||
open System.IO
|
open System.IO
|
||||||
open System.Reflection
|
open System.Reflection
|
||||||
open System.Threading
|
open System.Threading
|
||||||
|
open System.Threading.Tasks
|
||||||
open Microsoft.FSharp.Core
|
open Microsoft.FSharp.Core
|
||||||
|
|
||||||
type private StdoutSetter (newStdout : StreamWriter, newStderr : StreamWriter) =
|
|
||||||
let oldStdout = Console.Out
|
|
||||||
let oldStderr = Console.Error
|
|
||||||
|
|
||||||
do
|
|
||||||
Console.SetOut newStdout
|
|
||||||
Console.SetError newStderr
|
|
||||||
|
|
||||||
interface IDisposable with
|
|
||||||
member _.Dispose () =
|
|
||||||
Console.SetOut oldStdout
|
|
||||||
Console.SetError oldStderr
|
|
||||||
|
|
||||||
/// Information about the circumstances of a run of a single test.
|
/// Information about the circumstances of a run of a single test.
|
||||||
type IndividualTestRunMetadata =
|
type IndividualTestRunMetadata =
|
||||||
{
|
{
|
||||||
@@ -80,6 +68,8 @@ module TestFixture =
|
|||||||
///
|
///
|
||||||
/// This function does not throw.
|
/// This function does not throw.
|
||||||
let private runOne
|
let private runOne
|
||||||
|
(outputId : OutputStreamId)
|
||||||
|
(contexts : TestContexts)
|
||||||
(setUp : MethodInfo list)
|
(setUp : MethodInfo list)
|
||||||
(tearDown : MethodInfo list)
|
(tearDown : MethodInfo list)
|
||||||
(testId : Guid)
|
(testId : Guid)
|
||||||
@@ -112,13 +102,6 @@ module TestFixture =
|
|||||||
|
|
||||||
let start = DateTimeOffset.Now
|
let start = DateTimeOffset.Now
|
||||||
|
|
||||||
use stdOutStream = new MemoryStream ()
|
|
||||||
use stdErrStream = new MemoryStream ()
|
|
||||||
use stdOut = new StreamWriter (stdOutStream)
|
|
||||||
use stdErr = new StreamWriter (stdErrStream)
|
|
||||||
|
|
||||||
use _ = new StdoutSetter (stdOut, stdErr)
|
|
||||||
|
|
||||||
let sw = Stopwatch.StartNew ()
|
let sw = Stopwatch.StartNew ()
|
||||||
|
|
||||||
let metadata () =
|
let metadata () =
|
||||||
@@ -139,13 +122,13 @@ module TestFixture =
|
|||||||
TestName = name
|
TestName = name
|
||||||
ClassName = test.DeclaringType.FullName
|
ClassName = test.DeclaringType.FullName
|
||||||
StdOut =
|
StdOut =
|
||||||
match stdOutStream.ToArray () with
|
match contexts.DumpStdout outputId with
|
||||||
| [||] -> None
|
| "" -> None
|
||||||
| arr -> Console.OutputEncoding.GetString arr |> Some
|
| v -> Some v
|
||||||
StdErr =
|
StdErr =
|
||||||
match stdErrStream.ToArray () with
|
match contexts.DumpStderr outputId with
|
||||||
| [||] -> None
|
| "" -> None
|
||||||
| arr -> Console.OutputEncoding.GetString arr |> Some
|
| v -> Some v
|
||||||
}
|
}
|
||||||
|
|
||||||
let setUpResult = runMethods TestFailure.SetUpFailed setUp [||]
|
let setUpResult = runMethods TestFailure.SetUpFailed setUp [||]
|
||||||
@@ -223,11 +206,15 @@ module TestFixture =
|
|||||||
/// This method should never throw: it only throws if there's a critical logic error in the runner.
|
/// This method should never throw: it only throws if there's a critical logic error in the runner.
|
||||||
/// Exceptions from the units under test are wrapped up and passed out.
|
/// Exceptions from the units under test are wrapped up and passed out.
|
||||||
let private runTestsFromMember
|
let private runTestsFromMember
|
||||||
|
(contexts : TestContexts)
|
||||||
|
(par : ParallelQueue)
|
||||||
|
(running : TestFixtureSetupToken)
|
||||||
|
(progress : ITestProgress)
|
||||||
(setUp : MethodInfo list)
|
(setUp : MethodInfo list)
|
||||||
(tearDown : MethodInfo list)
|
(tearDown : MethodInfo list)
|
||||||
(containingObject : obj)
|
(containingObject : obj)
|
||||||
(test : SingleTestMethod)
|
(test : SingleTestMethod)
|
||||||
: (Result<TestMemberSuccess, TestMemberFailure> * IndividualTestRunMetadata) list
|
: (Result<TestMemberSuccess, TestMemberFailure> * IndividualTestRunMetadata) Task list
|
||||||
=
|
=
|
||||||
if test.Method.ContainsGenericParameters then
|
if test.Method.ContainsGenericParameters then
|
||||||
let failureMetadata =
|
let failureMetadata =
|
||||||
@@ -247,7 +234,7 @@ module TestFixture =
|
|||||||
let error =
|
let error =
|
||||||
TestMemberFailure.Malformed [ "Test contained generic parameters; generics are not supported." ]
|
TestMemberFailure.Malformed [ "Test contained generic parameters; generics are not supported." ]
|
||||||
|
|
||||||
(Error error, failureMetadata) |> List.singleton
|
(Error error, failureMetadata) |> Task.FromResult |> List.singleton
|
||||||
else
|
else
|
||||||
|
|
||||||
let resultPreRun =
|
let resultPreRun =
|
||||||
@@ -263,17 +250,12 @@ module TestFixture =
|
|||||||
| Modifier.Ignored reason -> Some (TestMemberSuccess.Ignored reason)
|
| Modifier.Ignored reason -> Some (TestMemberSuccess.Ignored reason)
|
||||||
)
|
)
|
||||||
|
|
||||||
let sw = Stopwatch.StartNew ()
|
|
||||||
let startTime = DateTimeOffset.Now
|
|
||||||
|
|
||||||
match resultPreRun with
|
match resultPreRun with
|
||||||
| Some result ->
|
| Some result ->
|
||||||
sw.Stop ()
|
|
||||||
|
|
||||||
let failureMetadata =
|
let failureMetadata =
|
||||||
{
|
{
|
||||||
Total = sw.Elapsed
|
Total = TimeSpan.Zero
|
||||||
Start = startTime
|
Start = DateTimeOffset.Now
|
||||||
End = DateTimeOffset.Now
|
End = DateTimeOffset.Now
|
||||||
ComputerName = Environment.MachineName
|
ComputerName = Environment.MachineName
|
||||||
ExecutionId = Guid.NewGuid ()
|
ExecutionId = Guid.NewGuid ()
|
||||||
@@ -285,7 +267,7 @@ module TestFixture =
|
|||||||
StdOut = None
|
StdOut = None
|
||||||
}
|
}
|
||||||
|
|
||||||
[ Ok result, failureMetadata ]
|
(Ok result, failureMetadata) |> Task.FromResult |> List.singleton
|
||||||
| None ->
|
| None ->
|
||||||
|
|
||||||
let individualTests =
|
let individualTests =
|
||||||
@@ -348,7 +330,7 @@ module TestFixture =
|
|||||||
|
|
||||||
// Might not be an IEnumerable of a reference type.
|
// Might not be an IEnumerable of a reference type.
|
||||||
// Concretely, `FSharpList<HttpStatusCode> :> IEnumerable<obj>` fails.
|
// Concretely, `FSharpList<HttpStatusCode> :> IEnumerable<obj>` fails.
|
||||||
for arg in args.GetValue (null : obj) :?> System.Collections.IEnumerable do
|
for arg in args.GetValue (null : obj) :?> IEnumerable do
|
||||||
yield
|
yield
|
||||||
Guid.NewGuid (),
|
Guid.NewGuid (),
|
||||||
match arg with
|
match arg with
|
||||||
@@ -371,20 +353,19 @@ module TestFixture =
|
|||||||
if isNull argsMem then
|
if isNull argsMem then
|
||||||
failwith "Unexpectedly could not call `.Arguments` on TestCaseData"
|
failwith "Unexpectedly could not call `.Arguments` on TestCaseData"
|
||||||
|
|
||||||
|
// TODO: need to capture this stdout/stderr
|
||||||
(argsMem.Invoke (arg, [||]) |> unbox<obj[]>)
|
(argsMem.Invoke (arg, [||]) |> unbox<obj[]>)
|
||||||
else
|
else
|
||||||
[| arg |]
|
[| arg |]
|
||||||
]
|
]
|
||||||
|> Ok
|
|> Ok
|
||||||
|
|
||||||
sw.Stop ()
|
|
||||||
|
|
||||||
match individualTests with
|
match individualTests with
|
||||||
| Error e ->
|
| Error e ->
|
||||||
let failureMetadata =
|
let failureMetadata =
|
||||||
{
|
{
|
||||||
Total = sw.Elapsed
|
Total = TimeSpan.Zero
|
||||||
Start = startTime
|
Start = DateTimeOffset.Now
|
||||||
End = DateTimeOffset.Now
|
End = DateTimeOffset.Now
|
||||||
ComputerName = Environment.MachineName
|
ComputerName = Environment.MachineName
|
||||||
ExecutionId = Guid.NewGuid ()
|
ExecutionId = Guid.NewGuid ()
|
||||||
@@ -397,7 +378,7 @@ module TestFixture =
|
|||||||
StdOut = None
|
StdOut = None
|
||||||
}
|
}
|
||||||
|
|
||||||
[ Error e, failureMetadata ]
|
(Error e, failureMetadata) |> Task.FromResult |> List.singleton
|
||||||
| Ok individualTests ->
|
| Ok individualTests ->
|
||||||
|
|
||||||
let count = test.Repeat |> Option.defaultValue 1
|
let count = test.Repeat |> Option.defaultValue 1
|
||||||
@@ -405,116 +386,205 @@ module TestFixture =
|
|||||||
Seq.init count (fun _ -> individualTests)
|
Seq.init count (fun _ -> individualTests)
|
||||||
|> Seq.concat
|
|> Seq.concat
|
||||||
|> Seq.map (fun (testGuid, args) ->
|
|> Seq.map (fun (testGuid, args) ->
|
||||||
let results, summary =
|
task {
|
||||||
runOne setUp tearDown testGuid test.Method containingObject args
|
let runMe () =
|
||||||
|
progress.OnTestMemberStart test.Name
|
||||||
|
let oldValue = contexts.AsyncLocal.Value
|
||||||
|
let outputId = contexts.NewOutputs ()
|
||||||
|
contexts.AsyncLocal.Value <- outputId
|
||||||
|
|
||||||
match results with
|
let result, meta =
|
||||||
| Ok results -> Ok results, summary
|
runOne outputId contexts setUp tearDown testGuid test.Method containingObject args
|
||||||
| Error e -> Error (TestMemberFailure.Failed e), summary
|
|
||||||
|
contexts.AsyncLocal.Value <- oldValue
|
||||||
|
progress.OnTestMemberFinished test.Name
|
||||||
|
|
||||||
|
result, meta
|
||||||
|
|
||||||
|
let! results, summary = par.Run running test.Parallelize runMe
|
||||||
|
|
||||||
|
match results with
|
||||||
|
| Ok results -> return Ok results, summary
|
||||||
|
| Error e -> return Error (TestMemberFailure.Failed e), summary
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|
|
||||||
/// Run every test (except those which fail the `filter`) in this test fixture, as well as the
|
/// Run every test (except those which fail the `filter`) in this test fixture, as well as the
|
||||||
/// appropriate setup and tear-down logic.
|
/// appropriate setup and tear-down logic.
|
||||||
let private runOneFixture
|
let runOneFixture
|
||||||
|
(contexts : TestContexts)
|
||||||
|
(par : ParallelQueue)
|
||||||
(progress : ITestProgress)
|
(progress : ITestProgress)
|
||||||
(filter : TestFixture -> SingleTestMethod -> bool)
|
(filter : TestFixture -> SingleTestMethod -> bool)
|
||||||
(name : string)
|
(name : string)
|
||||||
(containingObject : obj)
|
(containingObject : obj)
|
||||||
(tests : TestFixture)
|
(tests : TestFixture)
|
||||||
: FixtureRunResults
|
: FixtureRunResults Task
|
||||||
=
|
=
|
||||||
progress.OnTestFixtureStart name tests.Tests.Length
|
task {
|
||||||
|
let! running = par.StartTestFixture tests
|
||||||
|
progress.OnTestFixtureStart name tests.Tests.Length
|
||||||
|
|
||||||
let oldWorkDir = Environment.CurrentDirectory
|
let oldWorkDir = Environment.CurrentDirectory
|
||||||
Environment.CurrentDirectory <- FileInfo(tests.ContainingAssembly.Location).Directory.FullName
|
Environment.CurrentDirectory <- FileInfo(tests.ContainingAssembly.Location).Directory.FullName
|
||||||
|
|
||||||
let sw = Stopwatch.StartNew ()
|
let sw = Stopwatch.StartNew ()
|
||||||
let startTime = DateTimeOffset.UtcNow
|
let startTime = DateTimeOffset.UtcNow
|
||||||
|
|
||||||
use stdOutStream = new MemoryStream ()
|
let endMetadata (outputId : OutputStreamId) =
|
||||||
use stdOut = new StreamWriter (stdOutStream)
|
let stdOut = contexts.DumpStdout outputId
|
||||||
use stdErrStream = new MemoryStream ()
|
let stdErr = contexts.DumpStderr outputId
|
||||||
use stdErr = new StreamWriter (stdErrStream)
|
|
||||||
use _ = new StdoutSetter (stdOut, stdErr)
|
|
||||||
|
|
||||||
let endMetadata () =
|
{
|
||||||
let stdOut = stdOutStream.ToArray () |> Console.OutputEncoding.GetString
|
Total = sw.Elapsed
|
||||||
let stdErr = stdErrStream.ToArray () |> Console.OutputEncoding.GetString
|
Start = startTime
|
||||||
|
End = DateTimeOffset.UtcNow
|
||||||
|
ComputerName = Environment.MachineName
|
||||||
|
ExecutionId = Guid.NewGuid ()
|
||||||
|
TestId = Guid.NewGuid ()
|
||||||
|
// This one is a bit dubious, because we don't actually have a test name at all
|
||||||
|
TestName = name
|
||||||
|
ClassName = tests.Name
|
||||||
|
StdOut = if String.IsNullOrEmpty stdOut then None else Some stdOut
|
||||||
|
StdErr = if String.IsNullOrEmpty stdErr then None else Some stdErr
|
||||||
|
}
|
||||||
|
|
||||||
{
|
let! setupResult, running =
|
||||||
Total = sw.Elapsed
|
match tests.OneTimeSetUp with
|
||||||
Start = startTime
|
| Some su ->
|
||||||
End = DateTimeOffset.UtcNow
|
par.RunTestSetup
|
||||||
ComputerName = Environment.MachineName
|
running
|
||||||
ExecutionId = Guid.NewGuid ()
|
(fun () ->
|
||||||
TestId = Guid.NewGuid ()
|
let oldValue = contexts.AsyncLocal.Value
|
||||||
// This one is a bit dubious, because we don't actually have a test name at all
|
let newOutputs = contexts.NewOutputs ()
|
||||||
TestName = name
|
contexts.AsyncLocal.Value <- newOutputs
|
||||||
ClassName = tests.Name
|
|
||||||
StdOut = if String.IsNullOrEmpty stdOut then None else Some stdOut
|
|
||||||
StdErr = if String.IsNullOrEmpty stdErr then None else Some stdErr
|
|
||||||
}
|
|
||||||
|
|
||||||
let setupResult =
|
let result =
|
||||||
match tests.OneTimeSetUp with
|
try
|
||||||
| Some su ->
|
match su.Invoke (containingObject, [||]) with
|
||||||
try
|
| :? unit -> None
|
||||||
match su.Invoke (containingObject, [||]) with
|
| ret ->
|
||||||
| :? unit -> None
|
Some (UserMethodFailure.ReturnedNonUnit (su.Name, ret), endMetadata newOutputs)
|
||||||
| ret -> Some (UserMethodFailure.ReturnedNonUnit (su.Name, ret), endMetadata ())
|
with :? TargetInvocationException as e ->
|
||||||
with :? TargetInvocationException as e ->
|
Some (UserMethodFailure.Threw (su.Name, e.InnerException), endMetadata newOutputs)
|
||||||
Some (UserMethodFailure.Threw (su.Name, e.InnerException), endMetadata ())
|
|
||||||
| _ -> None
|
|
||||||
|
|
||||||
let testFailures = ResizeArray<TestMemberFailure * IndividualTestRunMetadata> ()
|
contexts.AsyncLocal.Value <- oldValue
|
||||||
|
|
||||||
let successes =
|
match result with
|
||||||
ResizeArray<SingleTestMethod * TestMemberSuccess * IndividualTestRunMetadata> ()
|
| None -> Ok (Some newOutputs)
|
||||||
|
| Some err -> Error (err, newOutputs)
|
||||||
|
)
|
||||||
|
| _ -> Task.FromResult (Ok None, TestFixtureSetupToken.vouchNoSetupRequired running)
|
||||||
|
|
||||||
match setupResult with
|
let testFailures = ResizeArray<TestMemberFailure * IndividualTestRunMetadata> ()
|
||||||
| Some _ ->
|
|
||||||
// Don't run any tests if setup failed.
|
|
||||||
()
|
|
||||||
| None ->
|
|
||||||
for test in tests.Tests do
|
|
||||||
if filter tests test then
|
|
||||||
progress.OnTestMemberStart test.Name
|
|
||||||
let testSuccess = ref 0
|
|
||||||
|
|
||||||
let results = runTestsFromMember tests.SetUp tests.TearDown containingObject test
|
let successes =
|
||||||
|
ResizeArray<SingleTestMethod * TestMemberSuccess * IndividualTestRunMetadata> ()
|
||||||
|
|
||||||
for result, report in results do
|
let testsRun =
|
||||||
match result with
|
match setupResult with
|
||||||
| Error failure ->
|
| Error _ ->
|
||||||
testFailures.Add (failure, report)
|
// Don't run any tests if setup failed.
|
||||||
progress.OnTestFailed test.Name failure
|
Task.FromResult ()
|
||||||
| Ok result ->
|
| Ok _ ->
|
||||||
Interlocked.Increment testSuccess |> ignore<int>
|
tests.Tests
|
||||||
lock successes (fun () -> successes.Add (test, result, report))
|
|> Seq.filter (fun test ->
|
||||||
|
if filter tests test then
|
||||||
|
true
|
||||||
|
else
|
||||||
|
progress.OnTestMemberSkipped test.Name
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|> Seq.map (fun test ->
|
||||||
|
task {
|
||||||
|
let testSuccess = ref 0
|
||||||
|
|
||||||
progress.OnTestMemberFinished test.Name
|
let results =
|
||||||
else
|
runTestsFromMember
|
||||||
progress.OnTestMemberSkipped test.Name
|
contexts
|
||||||
|
par
|
||||||
|
running
|
||||||
|
progress
|
||||||
|
tests.SetUp
|
||||||
|
tests.TearDown
|
||||||
|
containingObject
|
||||||
|
test
|
||||||
|
|
||||||
// Unconditionally run OneTimeTearDown if it exists.
|
let! result =
|
||||||
let tearDownError =
|
results
|
||||||
match tests.OneTimeTearDown with
|
|> List.map (fun t ->
|
||||||
| Some td ->
|
task {
|
||||||
try
|
let! result, report = t
|
||||||
match td.Invoke (containingObject, [||]) with
|
|
||||||
| null -> None
|
|
||||||
| ret -> Some (UserMethodFailure.ReturnedNonUnit (td.Name, ret), endMetadata ())
|
|
||||||
with :? TargetInvocationException as e ->
|
|
||||||
Some (UserMethodFailure.Threw (td.Name, e), endMetadata ())
|
|
||||||
| _ -> None
|
|
||||||
|
|
||||||
Environment.CurrentDirectory <- oldWorkDir
|
match result with
|
||||||
|
| Error failure ->
|
||||||
|
testFailures.Add (failure, report)
|
||||||
|
progress.OnTestFailed test.Name failure
|
||||||
|
| Ok result ->
|
||||||
|
Interlocked.Increment testSuccess |> ignore<int>
|
||||||
|
lock successes (fun () -> successes.Add (test, result, report))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> Task.WhenAll
|
||||||
|
|
||||||
{
|
result |> Array.iter id
|
||||||
Failed = testFailures |> Seq.toList
|
}
|
||||||
Success = successes |> Seq.toList
|
)
|
||||||
OtherFailures = [ tearDownError ; setupResult ] |> List.choose id
|
|> Task.WhenAll
|
||||||
|
|> fun t ->
|
||||||
|
task {
|
||||||
|
let! t = t
|
||||||
|
return t |> Array.iter id
|
||||||
|
}
|
||||||
|
|
||||||
|
do! testsRun
|
||||||
|
|
||||||
|
// Unconditionally run OneTimeTearDown if it exists.
|
||||||
|
let! tearDownError, tornDown =
|
||||||
|
match tests.OneTimeTearDown with
|
||||||
|
| Some td ->
|
||||||
|
par.RunTestTearDown
|
||||||
|
running
|
||||||
|
(fun () ->
|
||||||
|
let oldValue = contexts.AsyncLocal.Value
|
||||||
|
let outputs = contexts.NewOutputs ()
|
||||||
|
contexts.AsyncLocal.Value <- outputs
|
||||||
|
|
||||||
|
let result =
|
||||||
|
try
|
||||||
|
match td.Invoke (containingObject, [||]) with
|
||||||
|
| :? unit -> None
|
||||||
|
| ret ->
|
||||||
|
Some (UserMethodFailure.ReturnedNonUnit (td.Name, ret), endMetadata outputs)
|
||||||
|
with :? TargetInvocationException as e ->
|
||||||
|
Some (UserMethodFailure.Threw (td.Name, e.InnerException), endMetadata outputs)
|
||||||
|
|
||||||
|
contexts.AsyncLocal.Value <- oldValue
|
||||||
|
|
||||||
|
match result with
|
||||||
|
| None -> Ok (Some outputs)
|
||||||
|
| Some err -> Error (err, outputs)
|
||||||
|
)
|
||||||
|
| _ -> Task.FromResult (Ok None, TestFixtureTearDownToken.vouchNoTearDownRequired running)
|
||||||
|
|
||||||
|
Environment.CurrentDirectory <- oldWorkDir
|
||||||
|
|
||||||
|
do! par.EndTestFixture tornDown
|
||||||
|
|
||||||
|
// TODO: we have access to stdout/err of OneTimeSetUp and OneTimeTearDown here, but we throw them away.
|
||||||
|
return
|
||||||
|
{
|
||||||
|
Failed = testFailures |> Seq.toList
|
||||||
|
Success = successes |> Seq.toList
|
||||||
|
OtherFailures =
|
||||||
|
[ tearDownError ; setupResult ]
|
||||||
|
|> List.choose (
|
||||||
|
function
|
||||||
|
| Error (e, _) -> Some e
|
||||||
|
| Ok _ -> None
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interpret this type as a [<TestFixture>], extracting the test members from it and annotating them with all
|
/// Interpret this type as a [<TestFixture>], extracting the test members from it and annotating them with all
|
||||||
@@ -639,10 +709,12 @@ module TestFixture =
|
|||||||
/// Run every test (except those which fail the `filter`) in this test fixture, as well as the
|
/// Run every test (except those which fail the `filter`) in this test fixture, as well as the
|
||||||
/// appropriate setup and tear-down logic.
|
/// appropriate setup and tear-down logic.
|
||||||
let run
|
let run
|
||||||
|
(contexts : TestContexts)
|
||||||
|
(par : ParallelQueue)
|
||||||
(progress : ITestProgress)
|
(progress : ITestProgress)
|
||||||
(filter : TestFixture -> SingleTestMethod -> bool)
|
(filter : TestFixture -> SingleTestMethod -> bool)
|
||||||
(tests : TestFixture)
|
(tests : TestFixture)
|
||||||
: FixtureRunResults list
|
: FixtureRunResults list Task
|
||||||
=
|
=
|
||||||
match tests.Parameters with
|
match tests.Parameters with
|
||||||
| [] -> [ null ]
|
| [] -> [ null ]
|
||||||
@@ -678,5 +750,11 @@ module TestFixture =
|
|||||||
let args = args |> Seq.map (fun o -> o.ToString ()) |> String.concat ","
|
let args = args |> Seq.map (fun o -> o.ToString ()) |> String.concat ","
|
||||||
$"%s{tests.Name}(%s{args})"
|
$"%s{tests.Name}(%s{args})"
|
||||||
|
|
||||||
runOneFixture progress filter name containingObject tests
|
runOneFixture contexts par progress filter name containingObject tests
|
||||||
)
|
)
|
||||||
|
|> Task.WhenAll
|
||||||
|
|> fun t ->
|
||||||
|
task {
|
||||||
|
let! t = t
|
||||||
|
return Array.toList t
|
||||||
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
<PackageId>WoofWare.NUnitTestRunner.Lib</PackageId>
|
<PackageId>WoofWare.NUnitTestRunner.Lib</PackageId>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<WarnOn>FS3559</WarnOn>
|
<WarnOn>FS3559</WarnOn>
|
||||||
<WoofWareMyriadPluginVersion>2.1.44</WoofWareMyriadPluginVersion>
|
<WoofWareMyriadPluginVersion>2.1.45</WoofWareMyriadPluginVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -29,8 +29,10 @@
|
|||||||
<Compile Include="Result.fs" />
|
<Compile Include="Result.fs" />
|
||||||
<Compile Include="Domain.fs" />
|
<Compile Include="Domain.fs" />
|
||||||
<Compile Include="Filter.fs" />
|
<Compile Include="Filter.fs" />
|
||||||
|
<Compile Include="ParallelQueue.fs" />
|
||||||
<Compile Include="SingleTestMethod.fs" />
|
<Compile Include="SingleTestMethod.fs" />
|
||||||
<Compile Include="TestProgress.fs" />
|
<Compile Include="TestProgress.fs" />
|
||||||
|
<Compile Include="Context.fs" />
|
||||||
<Compile Include="TestFixture.fs" />
|
<Compile Include="TestFixture.fs" />
|
||||||
<Compile Include="Xml.fs" />
|
<Compile Include="Xml.fs" />
|
||||||
<Compile Include="TrxReport.fs" />
|
<Compile Include="TrxReport.fs" />
|
||||||
@@ -42,10 +44,10 @@
|
|||||||
<EmbeddedResource Include="version.json" />
|
<EmbeddedResource Include="version.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="WoofWare.PrattParser" Version="0.1.2" />
|
<PackageReference Include="WoofWare.PrattParser" Version="0.2.1" />
|
||||||
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||||
<PackageReference Include="WoofWare.DotnetRuntimeLocator" Version="0.1.4" />
|
<PackageReference Include="WoofWare.DotnetRuntimeLocator" Version="0.1.9" />
|
||||||
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="3.1.6" />
|
<PackageReference Include="WoofWare.Myriad.Plugins.Attributes" Version="3.1.7" />
|
||||||
<PackageReference Include="Myriad.SDK" Version="0.8.3" />
|
<PackageReference Include="Myriad.SDK" Version="0.8.3" />
|
||||||
<PackageReference Include="WoofWare.Myriad.Plugins" Version="$(WoofWareMyriadPluginVersion)" PrivateAssets="all" />
|
<PackageReference Include="WoofWare.Myriad.Plugins" Version="$(WoofWareMyriadPluginVersion)" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.12",
|
"version": "0.13",
|
||||||
"publicReleaseRefSpec": [
|
"publicReleaseRefSpec": [
|
||||||
"^refs/heads/main$"
|
"^refs/heads/main$"
|
||||||
],
|
],
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
open System
|
open System
|
||||||
open System.IO
|
open System.IO
|
||||||
open System.Reflection
|
open System.Threading.Tasks
|
||||||
open System.Runtime.Loader
|
open Spectre.Console
|
||||||
|
|
||||||
// Fix for https://github.com/Smaug123/unofficial-nunit-runner/issues/8
|
// Fix for https://github.com/Smaug123/unofficial-nunit-runner/issues/8
|
||||||
// Set AppContext.BaseDirectory to where the test DLL is.
|
// Set AppContext.BaseDirectory to where the test DLL is.
|
||||||
@@ -16,57 +16,121 @@ type SetBaseDir (testDll : FileInfo) =
|
|||||||
member _.Dispose () =
|
member _.Dispose () =
|
||||||
AppContext.SetData ("APP_CONTEXT_BASE_DIRECTORY", oldBaseDir)
|
AppContext.SetData ("APP_CONTEXT_BASE_DIRECTORY", oldBaseDir)
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type LogLevel =
|
||||||
|
| Nothing
|
||||||
|
| Verbose
|
||||||
|
|
||||||
type Ctx (dll : FileInfo, runtimes : DirectoryInfo list) =
|
[<AutoOpen>]
|
||||||
inherit AssemblyLoadContext ()
|
module Patterns =
|
||||||
|
let (|Key|_|) (start : string) (s : string) : string option =
|
||||||
override this.Load (target : AssemblyName) : Assembly =
|
if s.StartsWith (start + "=", StringComparison.Ordinal) then
|
||||||
let path = Path.Combine (dll.Directory.FullName, $"%s{target.Name}.dll")
|
s.Substring (start.Length + 1) |> Some
|
||||||
|
|
||||||
if File.Exists path then
|
|
||||||
this.LoadFromAssemblyPath path
|
|
||||||
else
|
else
|
||||||
|
None
|
||||||
|
|
||||||
runtimes
|
type Args =
|
||||||
|> List.tryPick (fun di ->
|
{
|
||||||
let path = Path.Combine (di.FullName, $"%s{target.Name}.dll")
|
Dll : FileInfo
|
||||||
|
Trx : FileInfo option
|
||||||
|
Filter : Filter option
|
||||||
|
Logging : LogLevel
|
||||||
|
LevelOfParallelism : int option
|
||||||
|
Timeout : TimeSpan option
|
||||||
|
}
|
||||||
|
|
||||||
if File.Exists path then
|
static member Parse (args : string list) : Args =
|
||||||
this.LoadFromAssemblyPath path |> Some
|
match args with
|
||||||
else
|
| [] -> failwith "The first arg must be a positional arg, the DLL to test."
|
||||||
None
|
| dll :: args ->
|
||||||
)
|
|
||||||
|> Option.defaultValue null
|
|
||||||
|
|
||||||
|
let rec go
|
||||||
|
(trx : FileInfo option)
|
||||||
|
(filter : Filter option)
|
||||||
|
(logging : LogLevel option)
|
||||||
|
(par : int option)
|
||||||
|
(timeout : TimeSpan option)
|
||||||
|
(args : string list)
|
||||||
|
=
|
||||||
|
match args with
|
||||||
|
| [] ->
|
||||||
|
{
|
||||||
|
Dll = FileInfo dll
|
||||||
|
Trx = trx
|
||||||
|
Filter = filter
|
||||||
|
Logging = logging |> Option.defaultValue LogLevel.Nothing
|
||||||
|
LevelOfParallelism = par
|
||||||
|
Timeout = timeout
|
||||||
|
}
|
||||||
|
| Key "--filter" filterStr :: rest
|
||||||
|
| "--filter" :: filterStr :: rest ->
|
||||||
|
match filter with
|
||||||
|
| Some _ -> failwith "Two conflicting filters; you can only specify --filter once"
|
||||||
|
| None -> go trx (Some (Filter.parse filterStr)) logging par timeout rest
|
||||||
|
| Key "--trx" trxStr :: rest
|
||||||
|
| "--trx" :: trxStr :: rest ->
|
||||||
|
match trx with
|
||||||
|
| Some _ -> failwith "Two conflicting TRX outputs; you can only specify --trx once"
|
||||||
|
| None -> go (Some (FileInfo trxStr)) filter logging par timeout rest
|
||||||
|
| Key "--verbose" verboseStr :: rest
|
||||||
|
| "--verbose" :: verboseStr :: rest ->
|
||||||
|
match logging with
|
||||||
|
| Some _ -> failwith "Two conflicting --verbose outputs; you can only specify --verbose once"
|
||||||
|
| None ->
|
||||||
|
let verbose =
|
||||||
|
if Boolean.Parse verboseStr then
|
||||||
|
LogLevel.Verbose
|
||||||
|
else
|
||||||
|
LogLevel.Nothing
|
||||||
|
|
||||||
|
go trx filter (Some verbose) par timeout rest
|
||||||
|
| Key "--parallelism" parStr :: rest
|
||||||
|
| "--parallelism" :: parStr :: rest ->
|
||||||
|
match par with
|
||||||
|
| Some _ -> failwith "Two conflicting --parallelism outputs; you can only specify --parallelism once"
|
||||||
|
| None -> go trx filter logging (Some (Int32.Parse parStr)) timeout rest
|
||||||
|
| Key "--timeout-seconds" timeoutStr :: rest
|
||||||
|
| "--timeout-seconds" :: timeoutStr :: rest ->
|
||||||
|
match timeout with
|
||||||
|
| Some _ ->
|
||||||
|
failwith "Two conflicting --timeout-seconds outputs; you can only specify --timeout-seconds once"
|
||||||
|
| None -> go trx filter logging par (Some (TimeSpan.FromSeconds (Int32.Parse timeoutStr |> float))) rest
|
||||||
|
| k :: _rest -> failwith $"Unrecognised arg %s{k}"
|
||||||
|
|
||||||
|
go None None None None None args
|
||||||
|
|
||||||
module Program =
|
module Program =
|
||||||
let main argv =
|
let main argv =
|
||||||
let startTime = DateTimeOffset.Now
|
let startTime = DateTimeOffset.Now
|
||||||
|
|
||||||
let testDll, filter, trxPath =
|
let args = argv |> List.ofArray |> Args.Parse
|
||||||
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 filter =
|
let filter =
|
||||||
match filter with
|
match args.Filter with
|
||||||
| Some filter -> Filter.shouldRun filter
|
| Some filter -> Filter.shouldRun filter
|
||||||
| None -> fun _ _ -> true
|
| None -> fun _ _ -> true
|
||||||
|
|
||||||
let progress = Progress.spectre ()
|
let stderr =
|
||||||
|
let consoleSettings = AnsiConsoleSettings ()
|
||||||
|
consoleSettings.Out <- AnsiConsoleOutput Console.Error
|
||||||
|
AnsiConsole.Create consoleSettings
|
||||||
|
|
||||||
use _ = new SetBaseDir (testDll)
|
let progress = Progress.spectre stderr
|
||||||
|
|
||||||
let ctx = Ctx (testDll, DotnetRuntime.locate testDll)
|
let runtime = DotnetRuntime.locate args.Dll
|
||||||
let assy = ctx.LoadFromAssemblyPath testDll.FullName
|
|
||||||
|
match args.Logging with
|
||||||
|
| LogLevel.Nothing -> ()
|
||||||
|
| LogLevel.Verbose ->
|
||||||
|
for d in runtime do
|
||||||
|
stderr.WriteLine $".NET runtime directory: %s{d.FullName}"
|
||||||
|
|
||||||
|
use _ = new SetBaseDir (args.Dll)
|
||||||
|
|
||||||
|
use contexts = TestContexts.Empty ()
|
||||||
|
|
||||||
|
let ctx = LoadContext (args.Dll, runtime, contexts)
|
||||||
|
let assy = ctx.LoadFromAssemblyPath args.Dll.FullName
|
||||||
|
|
||||||
let levelOfParallelism, par =
|
let levelOfParallelism, par =
|
||||||
((None, None), assy.CustomAttributes)
|
((None, None), assy.CustomAttributes)
|
||||||
@@ -107,10 +171,40 @@ module Program =
|
|||||||
| _ -> levelPar, par
|
| _ -> levelPar, par
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let levelOfParallelism =
|
||||||
|
match args.LevelOfParallelism, levelOfParallelism 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
|
let testFixtures = assy.ExportedTypes |> Seq.map TestFixture.parse |> Seq.toList
|
||||||
|
|
||||||
|
use par = new ParallelQueue (levelOfParallelism, par)
|
||||||
|
|
||||||
let creationTime = DateTimeOffset.Now
|
let creationTime = DateTimeOffset.Now
|
||||||
let results = testFixtures |> List.collect (TestFixture.run progress filter)
|
|
||||||
|
let results =
|
||||||
|
testFixtures
|
||||||
|
|> List.map (TestFixture.run contexts par progress filter)
|
||||||
|
|> Task.WhenAll
|
||||||
|
|
||||||
|
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 finishTime = DateTimeOffset.Now
|
||||||
let finishTimeHumanReadable = finishTime.ToString @"yyyy-MM-dd HH:mm:ss"
|
let finishTimeHumanReadable = finishTime.ToString @"yyyy-MM-dd HH:mm:ss"
|
||||||
@@ -340,7 +434,7 @@ module Program =
|
|||||||
ResultsSummary = resultSummary
|
ResultsSummary = resultSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
match trxPath with
|
match args.Trx with
|
||||||
| Some trxPath ->
|
| Some trxPath ->
|
||||||
let contents = TrxReport.toXml report |> fun d -> d.OuterXml
|
let contents = TrxReport.toXml report |> fun d -> d.OuterXml
|
||||||
trxPath.Directory.Create ()
|
trxPath.Directory.Create ()
|
||||||
|
@@ -4,21 +4,21 @@ open Spectre.Console
|
|||||||
|
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Progress =
|
module Progress =
|
||||||
let spectre () : ITestProgress =
|
let spectre (console : IAnsiConsole) : ITestProgress =
|
||||||
{ new ITestProgress with
|
{ new ITestProgress with
|
||||||
member _.OnTestFailed name failure =
|
member _.OnTestFailed name failure =
|
||||||
AnsiConsole.Console.MarkupLine
|
console.MarkupLine
|
||||||
$"[red]Test '%s{Markup.Escape name}' failed: %s{Markup.Escape (failure.ToString ())}[/]"
|
$"[red]Test '%s{Markup.Escape name}' failed: %s{Markup.Escape (failure.ToString ())}[/]"
|
||||||
|
|
||||||
member _.OnTestFixtureStart name testCount =
|
member _.OnTestFixtureStart name testCount =
|
||||||
AnsiConsole.Console.MarkupLine $"[white]Running tests: %s{Markup.Escape name}[/]"
|
console.MarkupLine $"[white]Running tests: %s{Markup.Escape name}[/]"
|
||||||
|
|
||||||
member _.OnTestMemberFinished name =
|
member _.OnTestMemberFinished name =
|
||||||
AnsiConsole.Console.MarkupLine $"[gray]Finished test: %s{Markup.Escape name}[/]"
|
console.MarkupLine $"[gray]Finished test: %s{Markup.Escape name}[/]"
|
||||||
|
|
||||||
member _.OnTestMemberSkipped name =
|
member _.OnTestMemberSkipped name =
|
||||||
AnsiConsole.Console.MarkupLine $"[yellow]Skipping test due to filter: %s{Markup.Escape name}[/]"
|
console.MarkupLine $"[yellow]Skipping test due to filter: %s{Markup.Escape name}[/]"
|
||||||
|
|
||||||
member _.OnTestMemberStart name =
|
member _.OnTestMemberStart name =
|
||||||
AnsiConsole.Console.MarkupLine $"[white]Running test: %s{Markup.Escape name}[/]"
|
console.MarkupLine $"[white]Running test: %s{Markup.Escape name}[/]"
|
||||||
}
|
}
|
||||||
|
16
nix/deps.nix
16
nix/deps.nix
@@ -248,22 +248,22 @@
|
|||||||
})
|
})
|
||||||
(fetchNuGet {
|
(fetchNuGet {
|
||||||
pname = "WoofWare.DotnetRuntimeLocator";
|
pname = "WoofWare.DotnetRuntimeLocator";
|
||||||
version = "0.1.4";
|
version = "0.1.9";
|
||||||
sha256 = "19pp4qlyf18g704ppbcsm1rhjqjpi84py18yljj9nx70331m8bpg";
|
sha256 = "14yc3ixcn58wy0v3pbj0hjfj4iv5k1ckig0dg1n7njx30510kzyj";
|
||||||
})
|
})
|
||||||
(fetchNuGet {
|
(fetchNuGet {
|
||||||
pname = "WoofWare.Myriad.Plugins";
|
pname = "WoofWare.Myriad.Plugins";
|
||||||
version = "2.1.44";
|
version = "2.1.45";
|
||||||
sha256 = "0rp9hpkah60gd9x0ba2izr9ff1g7yhzv5a4pkhi5fbrwf5rpqpwx";
|
sha256 = "1i9s9aq8dqnxyn01sa10dd24y9i7cgv2d0rshmrkvbvbjkcnz9vs";
|
||||||
})
|
})
|
||||||
(fetchNuGet {
|
(fetchNuGet {
|
||||||
pname = "WoofWare.Myriad.Plugins.Attributes";
|
pname = "WoofWare.Myriad.Plugins.Attributes";
|
||||||
version = "3.1.6";
|
version = "3.1.7";
|
||||||
sha256 = "0786pr1p0nq0854mqi2cddmh185j3jihwn6azz9wiy6nxawjbrd2";
|
sha256 = "1v1wsrjh7qz2khrlbcysj50yydqc9njj09vs1jglwscjhml1wl1v";
|
||||||
})
|
})
|
||||||
(fetchNuGet {
|
(fetchNuGet {
|
||||||
pname = "WoofWare.PrattParser";
|
pname = "WoofWare.PrattParser";
|
||||||
version = "0.1.2";
|
version = "0.2.1";
|
||||||
sha256 = "0spypcwsbn805yrs6grjj68ccva902lhkq93mxy32rdply1xs34q";
|
sha256 = "1cb9496fbbrdc40dirjmc7ax02ghr27ahqq5hpk96rdzyaang9hg";
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
Reference in New Issue
Block a user