mirror of
https://github.com/Smaug123/unofficial-nunit-runner
synced 2025-10-05 17:38:40 +00:00
Compare commits
23 Commits
WoofWare.N
...
WoofWare.N
Author | SHA1 | Date | |
---|---|---|---|
|
81aa6832d5 | ||
|
a20f32de02 | ||
|
1e53e72d4a | ||
|
b38a3fcc02 | ||
|
73dc21e11f | ||
|
5b54bb256e | ||
|
b56e1b1542 | ||
|
ba46b1edb6 | ||
|
72674e1711 | ||
|
c4b862bdd8 | ||
|
4c629b1d64 | ||
|
e67820c56d | ||
|
31bff4cb03 | ||
|
d081cfaafb | ||
|
c237df3885 | ||
|
3b9b9eb4c8 | ||
|
12e3fc0e4f | ||
|
208b809096 | ||
|
b4e5baddcf | ||
|
5597b3f2f8 | ||
|
fcfdcef6cf | ||
|
eeada219f6 | ||
|
99e0fdff08 |
@@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"fsharp-analyzers": {
|
||||
"version": "0.32.0",
|
||||
"version": "0.32.1",
|
||||
"commands": [
|
||||
"fsharp-analyzers"
|
||||
]
|
||||
|
22
.envrc
22
.envrc
@@ -1 +1,23 @@
|
||||
use flake
|
||||
DOTNET_PATH=$(readlink "$(which dotnet)")
|
||||
SETTINGS_FILE=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
|
||||
MSBUILD=$(realpath "$(find "$(dirname "$DOTNET_PATH")/../share/dotnet/sdk" -maxdepth 2 -type f -name MSBuild.dll)")
|
||||
if [ -f "$SETTINGS_FILE" ] ; then
|
||||
xmlstarlet ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
|
||||
--value "$(realpath "$(dirname "$DOTNET_PATH")/../share/dotnet/dotnet")" \
|
||||
"$SETTINGS_FILE"
|
||||
|
||||
xmlstarlet ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
|
||||
--value "$MSBUILD" \
|
||||
"$SETTINGS_FILE"
|
||||
fi
|
||||
|
68
.github/workflows/dotnet.yaml
vendored
68
.github/workflows/dotnet.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||
- name: Install Nix
|
||||
@@ -39,12 +39,12 @@ jobs:
|
||||
run: 'nix develop --command dotnet build --no-restore --configuration ${{matrix.config}}'
|
||||
- name: Test
|
||||
run: |
|
||||
nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}} --framework net8.0 --filter 'FullyQualifiedName !~ FailingConsumer'
|
||||
nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}} --filter 'FullyQualifiedName !~ FailingConsumer'
|
||||
|
||||
selftest-intended-failures:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||
- name: Install Nix
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Build
|
||||
run: 'nix develop --command dotnet build --no-restore --configuration Release'
|
||||
- name: Test using self
|
||||
run: 'nix develop --command dotnet exec ./WoofWare.NUnitTestRunner/bin/Release/net6.0/WoofWare.NUnitTestRunner.dll ./FailingConsumer/bin/Release/net8.0/FailingConsumer.dll --trx TrxOut/out.trx || true'
|
||||
run: 'nix develop --command dotnet exec ./WoofWare.NUnitTestRunner/bin/Release/net9.0/WoofWare.NUnitTestRunner.dll ./FailingConsumer/bin/Release/net9.0/FailingConsumer.dll --trx TrxOut/out.trx || true'
|
||||
- name: Munge output
|
||||
run: 'nix develop --command xmlstarlet sel -N x="http://microsoft.com/schemas/VisualStudio/TeamTest/2010" -t -m "//x:UnitTestResult" -v "@testName" -o ": " -v ".//x:ErrorInfo/x:Message" -n TrxOut/out.trx > snapshot.txt'
|
||||
- name: Check output matches expected
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
statuses: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||
- name: Install Nix
|
||||
@@ -94,10 +94,10 @@ jobs:
|
||||
- name: Build
|
||||
run: 'nix develop --command dotnet build --no-restore --configuration Release'
|
||||
- name: Test using self
|
||||
run: 'nix develop --command dotnet exec ./WoofWare.NUnitTestRunner/bin/Release/net6.0/WoofWare.NUnitTestRunner.dll ./Consumer/bin/Release/net8.0/Consumer.dll --trx TrxOut/out.trx'
|
||||
run: 'nix develop --command dotnet exec ./WoofWare.NUnitTestRunner/bin/Release/net9.0/WoofWare.NUnitTestRunner.dll ./Consumer/bin/Release/net9.0/Consumer.dll --trx TrxOut/out.trx'
|
||||
- name: Parse Trx files
|
||||
uses: NasAmin/trx-parser@v0.6.0
|
||||
if: always()
|
||||
uses: NasAmin/trx-parser@v0.7.0
|
||||
if: always() && github.ref_name != 'main'
|
||||
id: trx-parser
|
||||
with:
|
||||
TRX_PATH: ${{ github.workspace }}/TrxOut
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||
- name: Install Nix
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
@@ -194,7 +194,7 @@ jobs:
|
||||
nuget-pack:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||
- name: Install Nix
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download NuGet artifact (lib)
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: nuget-package-lib
|
||||
path: packed-lib
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
# Verify that there is exactly one nupkg in the artifact that would be NuGet published
|
||||
run: if [[ $(find packed-lib -maxdepth 1 -name 'WoofWare.NUnitTestRunner.Lib.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
|
||||
- name: Download NuGet artifact (tool)
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: nuget-package-tool
|
||||
path: packed-tool
|
||||
@@ -259,12 +259,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download NuGet artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: nuget-package-lib
|
||||
path: packed
|
||||
- name: Attest Build Provenance
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: "packed/*.nupkg"
|
||||
|
||||
@@ -278,12 +278,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download NuGet artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: nuget-package-tool
|
||||
path: packed
|
||||
- name: Attest Build Provenance
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-path: "packed/*.nupkg"
|
||||
|
||||
@@ -297,25 +297,30 @@ jobs:
|
||||
attestations: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download NuGet artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: nuget-package-lib
|
||||
path: packed
|
||||
- name: Identify .NET
|
||||
id: identify-dotnet
|
||||
run: nix develop --command bash -c "echo dotnet=$(which dotnet) >> $GITHUB_OUTPUT"
|
||||
- name: Obtain NuGet key
|
||||
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
|
||||
id: login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_USER }}
|
||||
- name: Publish NuGet package
|
||||
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
|
||||
with:
|
||||
package-name: WoofWare.NUnitTestRunner.Lib
|
||||
nuget-key: ${{ secrets.NUGET_API_KEY }}
|
||||
nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
|
||||
nupkg-dir: packed/
|
||||
dotnet: ${{ steps.identify-dotnet.outputs.dotnet }}
|
||||
|
||||
@@ -329,25 +334,30 @@ jobs:
|
||||
attestations: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download NuGet artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: nuget-package-tool
|
||||
path: packed
|
||||
- name: Identify .NET
|
||||
id: identify-dotnet
|
||||
run: nix develop --command bash -c "echo dotnet=$(which dotnet) >> $GITHUB_OUTPUT"
|
||||
- name: Obtain NuGet key
|
||||
uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544
|
||||
id: login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_USER }}
|
||||
- name: Publish NuGet package
|
||||
uses: G-Research/common-actions/publish-nuget@2b7dc49cb14f3344fbe6019c14a31165e258c059
|
||||
with:
|
||||
package-name: WoofWare.NUnitTestRunner
|
||||
nuget-key: ${{ secrets.NUGET_API_KEY }}
|
||||
nuget-key: ${{ steps.login.outputs.NUGET_API_KEY }}
|
||||
nupkg-dir: packed/
|
||||
dotnet: ${{ steps.identify-dotnet.outputs.dotnet }}
|
||||
|
||||
@@ -360,9 +370,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [nuget-pack]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Download NuGet artifact (tool)
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
- name: Compute package path
|
||||
@@ -396,9 +406,9 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Download NuGet artifact (tool)
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
- name: Compute package path
|
||||
|
2
.github/workflows/flake_update.yaml
vendored
2
.github/workflows/flake_update.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net9.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
@@ -10,6 +10,7 @@
|
||||
<Compile Include="NoAttribute.fs" />
|
||||
<Compile Include="Inconclusive.fs" />
|
||||
<Compile Include="RunSubProcess.fs" />
|
||||
<Compile Include="TestAsync.fs" />
|
||||
<Compile Include="TestExplicit.fs" />
|
||||
<Compile Include="TestNonParallel.fs" />
|
||||
<Compile Include="TestParallel.fs" />
|
||||
|
23
Consumer/TestAsync.fs
Normal file
23
Consumer/TestAsync.fs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Consumer
|
||||
|
||||
open System
|
||||
open System.Threading.Tasks
|
||||
open FsUnitTyped
|
||||
open NUnit.Framework
|
||||
|
||||
[<TestFixture>]
|
||||
module TestAsync =
|
||||
|
||||
[<Test>]
|
||||
let ``an async test`` () =
|
||||
async {
|
||||
do! Async.Sleep (TimeSpan.FromMilliseconds 20.0)
|
||||
1 |> shouldEqual 1
|
||||
}
|
||||
|
||||
[<Test>]
|
||||
let ``an async test, task-based`` () =
|
||||
task {
|
||||
do! Task.Delay (TimeSpan.FromMilliseconds 20.0)
|
||||
1 |> shouldEqual 1
|
||||
}
|
@@ -10,7 +10,7 @@
|
||||
<WarnOn>FS3388,FS3559</WarnOn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.8.38-alpha" PrivateAssets="all" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition="'$(GITHUB_ACTION)' != ''">
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
@@ -13,3 +13,7 @@ However, we would recommend phrasing some of them differently, for maximum peace
|
||||
|
||||
WoofWare.NUnitTestRunner has *limited* support for parallelism.
|
||||
By default, we run tests in parallel, taking half the available processors; we may or may not respect the NUnit parallelism attributes to any given extent that they tell us to be *more* parallel (but we will never incorrectly run tests in parallel).
|
||||
|
||||
# Licence
|
||||
|
||||
WoofWare.NUnitTestRunner is licensed to you under the MIT licence, a copy of which can be found at [LICENSE](./LICENSE).
|
||||
|
@@ -10,20 +10,14 @@ open System.Threading
|
||||
|
||||
type internal OutputStreamId = | OutputStreamId of Guid
|
||||
|
||||
type private ThreadAwareWriter
|
||||
(
|
||||
local : AsyncLocal<OutputStreamId>,
|
||||
underlying : Dictionary<OutputStreamId, TextWriter>,
|
||||
mem : Dictionary<OutputStreamId, MemoryStream>
|
||||
)
|
||||
type private ThreadAwareWriter (local : AsyncLocal<OutputStreamId>, underlying : Dictionary<OutputStreamId, TextWriter>)
|
||||
=
|
||||
inherit TextWriter ()
|
||||
override _.get_Encoding () = Encoding.Default
|
||||
|
||||
override this.Write (v : char) : unit =
|
||||
use prev = ExecutionContext.Capture ()
|
||||
|
||||
(fun _ ->
|
||||
lock
|
||||
underlying
|
||||
(fun () ->
|
||||
match underlying.TryGetValue local.Value with
|
||||
| true, output -> output.Write v
|
||||
@@ -31,16 +25,12 @@ type private ThreadAwareWriter
|
||||
let wanted =
|
||||
underlying |> Seq.map (fun (KeyValue (a, b)) -> $"%O{a}") |> String.concat "\n"
|
||||
|
||||
failwith $"no such context: %O{local.Value}\nwanted:\n"
|
||||
failwith $"no such context: %O{local.Value}\nwanted:\n{wanted}"
|
||||
)
|
||||
|> lock underlying
|
||||
)
|
||||
|> fun action -> ExecutionContext.Run (prev, action, ())
|
||||
|
||||
override this.WriteLine (v : string) : unit =
|
||||
use prev = ExecutionContext.Capture ()
|
||||
|
||||
(fun _ ->
|
||||
lock
|
||||
underlying
|
||||
(fun () ->
|
||||
match underlying.TryGetValue local.Value with
|
||||
| true, output -> output.WriteLine v
|
||||
@@ -48,16 +38,13 @@ type private ThreadAwareWriter
|
||||
let wanted =
|
||||
underlying |> Seq.map (fun (KeyValue (a, b)) -> $"%O{a}") |> String.concat "\n"
|
||||
|
||||
failwith $"no such context: %O{local.Value}\nwanted:\n"
|
||||
failwith $"no such context: %O{local.Value}\nwanted:\n{wanted}"
|
||||
)
|
||||
|> lock underlying
|
||||
)
|
||||
|> fun action -> ExecutionContext.Run (prev, action, ())
|
||||
|
||||
/// Wraps up the necessary context to intercept global state.
|
||||
[<NoEquality ; NoComparison>]
|
||||
type TestContexts =
|
||||
private
|
||||
internal
|
||||
{
|
||||
/// Accesses to this must be locked on StdOutWriters.
|
||||
StdOuts : Dictionary<OutputStreamId, MemoryStream>
|
||||
@@ -77,8 +64,8 @@ type TestContexts =
|
||||
let stdoutWriters = Dictionary ()
|
||||
let stderrWriters = Dictionary ()
|
||||
let local = AsyncLocal ()
|
||||
let stdoutWriter = new ThreadAwareWriter (local, stdoutWriters, stdouts)
|
||||
let stderrWriter = new ThreadAwareWriter (local, stderrWriters, stderrs)
|
||||
let stdoutWriter = new ThreadAwareWriter (local, stdoutWriters)
|
||||
let stderrWriter = new ThreadAwareWriter (local, stderrWriters)
|
||||
|
||||
{
|
||||
StdOuts = stdouts
|
||||
|
10
WoofWare.NUnitTestRunner.Lib/Exception.fs
Normal file
10
WoofWare.NUnitTestRunner.Lib/Exception.fs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace WoofWare.NUnitTestRunner
|
||||
|
||||
open System.Runtime.ExceptionServices
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module internal Exception =
|
||||
let reraiseWithOriginalStackTrace<'a> (e : exn) : 'a =
|
||||
let edi = ExceptionDispatchInfo.Capture e
|
||||
edi.Throw ()
|
||||
failwith "unreachable"
|
@@ -4,16 +4,16 @@ open System
|
||||
open System.Threading
|
||||
open System.Threading.Tasks
|
||||
|
||||
type private ThunkEvaluator<'ret> =
|
||||
abstract Eval<'a> : (unit -> 'a) -> AsyncReplyChannel<'a> -> 'ret
|
||||
type private AsyncThunkEvaluator<'ret> =
|
||||
abstract Eval<'a> : (unit -> Async<'a>) -> AsyncReplyChannel<Result<'a, exn>> -> 'ret
|
||||
|
||||
type private ThunkCrate =
|
||||
abstract Apply<'ret> : ThunkEvaluator<'ret> -> 'ret
|
||||
type private AsyncThunkCrate =
|
||||
abstract Apply<'ret> : AsyncThunkEvaluator<'ret> -> 'ret
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module private ThunkCrate =
|
||||
let make<'a> (t : unit -> 'a) (rc : AsyncReplyChannel<'a>) : ThunkCrate =
|
||||
{ new ThunkCrate with
|
||||
module private AsyncThunkCrate =
|
||||
let make<'a> (t : unit -> Async<'a>) (rc : AsyncReplyChannel<Result<'a, exn>>) : AsyncThunkCrate =
|
||||
{ new AsyncThunkCrate with
|
||||
member _.Apply e = e.Eval t rc
|
||||
}
|
||||
|
||||
@@ -41,7 +41,11 @@ 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
|
||||
| RunTestAsync of
|
||||
within : TestFixture *
|
||||
Parallelizable<unit> option *
|
||||
test : AsyncThunkCrate *
|
||||
context : ExecutionContext
|
||||
| BeginTestFixture of TestFixture * AsyncReplyChannel<TestFixtureRunningToken>
|
||||
| EndTestFixture of TestFixtureTearDownToken * AsyncReplyChannel<unit>
|
||||
|
||||
@@ -310,21 +314,31 @@ type ParallelQueue
|
||||
rc.Reply ()
|
||||
m.Post MailboxMessage.Reconcile
|
||||
return! processTask (Running state) m
|
||||
| MailboxMessage.RunTest (withinFixture, par, message) ->
|
||||
| MailboxMessage.RunTestAsync (withinFixture, par, message, capturedContext) ->
|
||||
let t () =
|
||||
{ new ThunkEvaluator<_> with
|
||||
member _.Eval<'b> (t : unit -> 'b) rc =
|
||||
{ new AsyncThunkEvaluator<_> with
|
||||
member _.Eval<'b> (t : unit -> Async<'b>) rc =
|
||||
let tcs = TaskCompletionSource TaskCreationOptions.RunContinuationsAsynchronously
|
||||
use ec = ExecutionContext.Capture ()
|
||||
|
||||
fun () ->
|
||||
ExecutionContext.Run (
|
||||
ec,
|
||||
capturedContext,
|
||||
(fun _ ->
|
||||
let result = t ()
|
||||
tcs.SetResult ()
|
||||
m.Post MailboxMessage.Reconcile
|
||||
rc.Reply result
|
||||
async {
|
||||
let! result =
|
||||
async {
|
||||
try
|
||||
let! r = t ()
|
||||
return Ok r
|
||||
with e ->
|
||||
return Error e
|
||||
}
|
||||
|
||||
tcs.SetResult ()
|
||||
m.Post MailboxMessage.Reconcile
|
||||
rc.Reply result
|
||||
}
|
||||
|> Async.StartImmediate
|
||||
),
|
||||
()
|
||||
)
|
||||
@@ -348,17 +362,36 @@ type ParallelQueue
|
||||
let mb = new MailboxProcessor<_> (processTask MailboxState.Idle)
|
||||
do mb.Start ()
|
||||
|
||||
/// Request to run the given async action, freely in parallel with other running tests.
|
||||
/// The resulting Task will return when the action has completed.
|
||||
member _.RunAsync<'a>
|
||||
(TestFixtureSetupToken parent)
|
||||
(scope : Parallelizable<unit> option)
|
||||
(action : unit -> Async<'a>)
|
||||
: 'a Task
|
||||
=
|
||||
let ec = ExecutionContext.Capture ()
|
||||
|
||||
task {
|
||||
let! result =
|
||||
(fun rc -> MailboxMessage.RunTestAsync (parent, scope, AsyncThunkCrate.make action rc, ec))
|
||||
|> mb.PostAndAsyncReply
|
||||
|> Async.StartAsTask
|
||||
|
||||
match result with
|
||||
| Ok o -> return o
|
||||
| Error e -> return Exception.reraiseWithOriginalStackTrace e
|
||||
}
|
||||
|
||||
/// 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)
|
||||
member this.Run<'a>
|
||||
(parent : TestFixtureSetupToken)
|
||||
(scope : Parallelizable<unit> option)
|
||||
(action : unit -> 'a)
|
||||
: 'a Task
|
||||
=
|
||||
(fun rc -> MailboxMessage.RunTest (parent, scope, ThunkCrate.make action rc))
|
||||
|> mb.PostAndAsyncReply
|
||||
|> Async.StartAsTask
|
||||
this.RunAsync parent scope (fun () -> async.Return (action ()))
|
||||
|
||||
/// 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.
|
||||
@@ -379,11 +412,22 @@ type ParallelQueue
|
||||
| Parallelizable.Yes _ -> Parallelizable.Yes ()
|
||||
)
|
||||
|
||||
let ec = ExecutionContext.Capture ()
|
||||
|
||||
let! response =
|
||||
(fun rc -> MailboxMessage.RunTest (parent, par, ThunkCrate.make action rc))
|
||||
(fun rc ->
|
||||
MailboxMessage.RunTestAsync (
|
||||
parent,
|
||||
par,
|
||||
AsyncThunkCrate.make (fun () -> async.Return (action ())) rc,
|
||||
ec
|
||||
)
|
||||
)
|
||||
|> mb.PostAndAsyncReply
|
||||
|
||||
return response, TestFixtureSetupToken parent
|
||||
match response with
|
||||
| Ok response -> return response, TestFixtureSetupToken parent
|
||||
| Error e -> return Exception.reraiseWithOriginalStackTrace e
|
||||
}
|
||||
|
||||
/// Run the given one-time tear-down for the test fixture.
|
||||
@@ -401,11 +445,22 @@ type ParallelQueue
|
||||
| Parallelizable.Yes _ -> Parallelizable.Yes ()
|
||||
)
|
||||
|
||||
let ec = ExecutionContext.Capture ()
|
||||
|
||||
let! response =
|
||||
(fun rc -> MailboxMessage.RunTest (parent, par, ThunkCrate.make action rc))
|
||||
(fun rc ->
|
||||
MailboxMessage.RunTestAsync (
|
||||
parent,
|
||||
par,
|
||||
AsyncThunkCrate.make (fun () -> async.Return (action ())) rc,
|
||||
ec
|
||||
)
|
||||
)
|
||||
|> mb.PostAndAsyncReply
|
||||
|
||||
return response, TestFixtureTearDownToken parent
|
||||
match response with
|
||||
| Ok response -> return response, TestFixtureTearDownToken parent
|
||||
| Error e -> return Exception.reraiseWithOriginalStackTrace e
|
||||
}
|
||||
|
||||
/// Declare that we have finished submitting requests to run in the given test fixture.
|
||||
|
@@ -256,6 +256,7 @@ 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.RunAsync [method]: WoofWare.NUnitTestRunner.TestFixtureSetupToken -> unit WoofWare.NUnitTestRunner.Parallelizable option -> (unit -> 'a Microsoft.FSharp.Control.FSharpAsync) -> '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
|
||||
|
@@ -76,105 +76,168 @@ module TestFixture =
|
||||
(test : MethodInfo)
|
||||
(containingObject : obj)
|
||||
(args : obj[])
|
||||
: Result<TestMemberSuccess, TestFailure list> * IndividualTestRunMetadata
|
||||
: Async<Result<TestMemberSuccess, TestFailure list> * IndividualTestRunMetadata>
|
||||
=
|
||||
let rec runMethods
|
||||
(wrap : UserMethodFailure -> TestFailure)
|
||||
(toRun : MethodInfo list)
|
||||
(args : obj[])
|
||||
: Result<unit, _>
|
||||
: Result<unit, TestFailure> Async
|
||||
=
|
||||
match toRun with
|
||||
| [] -> Ok ()
|
||||
| [] -> async.Return (Ok ())
|
||||
| head :: rest ->
|
||||
let result =
|
||||
try
|
||||
head.Invoke (containingObject, args) |> Ok
|
||||
with
|
||||
| :? TargetInvocationException as e -> Error (UserMethodFailure.Threw (head.Name, e.InnerException))
|
||||
| :? TargetParameterCountException ->
|
||||
UserMethodFailure.BadParameters (
|
||||
head.Name,
|
||||
head.GetParameters () |> Array.map (fun pm -> pm.ParameterType),
|
||||
args
|
||||
)
|
||||
|> Error
|
||||
async {
|
||||
let result =
|
||||
try
|
||||
head.Invoke (containingObject, args) |> Ok
|
||||
with
|
||||
| :? TargetInvocationException as e ->
|
||||
Error (UserMethodFailure.Threw (head.Name, e.InnerException))
|
||||
| :? TargetParameterCountException ->
|
||||
UserMethodFailure.BadParameters (
|
||||
head.Name,
|
||||
head.GetParameters () |> Array.map (fun pm -> pm.ParameterType),
|
||||
args
|
||||
)
|
||||
|> Error
|
||||
|
||||
match result with
|
||||
| Error e -> Error (wrap e)
|
||||
| Ok result ->
|
||||
match result with
|
||||
| :? unit -> runMethods wrap rest args
|
||||
| ret -> UserMethodFailure.ReturnedNonUnit (head.Name, ret) |> wrap |> Error
|
||||
let! ct = Async.CancellationToken
|
||||
|
||||
let start = DateTimeOffset.Now
|
||||
let! result =
|
||||
match result with
|
||||
| Error e -> async.Return (Error (wrap e))
|
||||
| Ok result ->
|
||||
match result with
|
||||
| :? unit -> runMethods wrap rest args
|
||||
| :? Task as result ->
|
||||
async {
|
||||
let mutable exc = None
|
||||
|
||||
let sw = Stopwatch.StartNew ()
|
||||
try
|
||||
do! Async.AwaitTask result
|
||||
with e ->
|
||||
exc <- Some e
|
||||
|
||||
let metadata () =
|
||||
let name =
|
||||
if args.Length = 0 then
|
||||
test.Name
|
||||
else
|
||||
let argsStr = args |> Seq.map string<obj> |> String.concat ","
|
||||
$"%s{test.Name}(%s{argsStr})"
|
||||
match exc with
|
||||
| None -> return! runMethods wrap rest args
|
||||
| Some e -> return Error (UserMethodFailure.Threw (head.Name, e) |> wrap)
|
||||
}
|
||||
// We'd like to do this type-test:
|
||||
// | :? Async<unit> as result ->
|
||||
// but instead we have to do all this reflective nonsense, because FSharpAsync is not part
|
||||
// of the .NET runtime, so is instead in a different AssemblyLoadContext to us!
|
||||
// It's in the user-code context, not ours.
|
||||
| ret ->
|
||||
let ty = ret.GetType ()
|
||||
|
||||
{
|
||||
End = DateTimeOffset.Now
|
||||
Start = start
|
||||
Total = sw.Elapsed
|
||||
ComputerName = Environment.MachineName
|
||||
ExecutionId = Guid.NewGuid ()
|
||||
TestId = testId
|
||||
TestName = name
|
||||
ClassName = test.DeclaringType.FullName
|
||||
StdOut =
|
||||
match contexts.DumpStdout outputId with
|
||||
| "" -> None
|
||||
| v -> Some v
|
||||
StdErr =
|
||||
match contexts.DumpStderr outputId with
|
||||
| "" -> None
|
||||
| v -> Some v
|
||||
}
|
||||
if ty.Namespace = "Microsoft.FSharp.Control" && ty.Name = "FSharpAsync`1" then
|
||||
match ty.GenericTypeArguments |> Array.map (fun t -> t.FullName) with
|
||||
| [| "Microsoft.FSharp.Core.Unit" |] ->
|
||||
let asyncModule = ty.Assembly.GetType ("Microsoft.FSharp.Control.FSharpAsync")
|
||||
// let catch = asyncModule.GetMethod("Catch").MakeGenericMethod [| ty.GenericTypeArguments.[0] |]
|
||||
// let caught = catch.Invoke ((null: obj), [| ret |])
|
||||
let startAsTask =
|
||||
asyncModule.GetMethod("StartAsTask").MakeGenericMethod
|
||||
[| ty.GenericTypeArguments.[0] |]
|
||||
|
||||
let setUpResult = runMethods TestFailure.SetUpFailed setUp [||]
|
||||
sw.Stop ()
|
||||
let started =
|
||||
startAsTask.Invoke ((null : obj), [| ret ; (null : obj) ; (null : obj) |])
|
||||
|> unbox<Task>
|
||||
|
||||
match setUpResult with
|
||||
| Error e -> Error [ e ], metadata ()
|
||||
| Ok () ->
|
||||
async {
|
||||
let! res = Async.AwaitTask started |> Async.Catch
|
||||
|
||||
sw.Start ()
|
||||
match res with
|
||||
| Choice1Of2 () -> return! runMethods wrap rest args
|
||||
| Choice2Of2 e ->
|
||||
return
|
||||
Error (
|
||||
UserMethodFailure.Threw (head.Name, started.Exception) |> wrap
|
||||
)
|
||||
}
|
||||
| _ ->
|
||||
UserMethodFailure.ReturnedNonUnit (head.Name, ret)
|
||||
|> wrap
|
||||
|> Error
|
||||
|> async.Return
|
||||
else
|
||||
async.Return (UserMethodFailure.ReturnedNonUnit (head.Name, ret) |> wrap |> Error)
|
||||
|
||||
let result =
|
||||
let result = runMethods TestFailure.TestFailed [ test ] args
|
||||
return result
|
||||
}
|
||||
|
||||
async {
|
||||
let start = DateTimeOffset.Now
|
||||
|
||||
let sw = Stopwatch.StartNew ()
|
||||
|
||||
let metadata () =
|
||||
let name =
|
||||
if args.Length = 0 then
|
||||
test.Name
|
||||
else
|
||||
let argsStr = args |> Seq.map string<obj> |> String.concat ","
|
||||
$"%s{test.Name}(%s{argsStr})"
|
||||
|
||||
{
|
||||
End = DateTimeOffset.Now
|
||||
Start = start
|
||||
Total = sw.Elapsed
|
||||
ComputerName = Environment.MachineName
|
||||
ExecutionId = Guid.NewGuid ()
|
||||
TestId = testId
|
||||
TestName = name
|
||||
ClassName = test.DeclaringType.FullName
|
||||
StdOut =
|
||||
match contexts.DumpStdout outputId with
|
||||
| "" -> None
|
||||
| v -> Some v
|
||||
StdErr =
|
||||
match contexts.DumpStderr outputId with
|
||||
| "" -> None
|
||||
| v -> Some v
|
||||
}
|
||||
|
||||
let! setUpResult = runMethods TestFailure.SetUpFailed setUp [||]
|
||||
sw.Stop ()
|
||||
|
||||
match result with
|
||||
| Ok () -> Ok None
|
||||
| Error (TestFailure.TestFailed (UserMethodFailure.Threw (_, exc)) as orig) ->
|
||||
match exc.GetType().FullName with
|
||||
| "NUnit.Framework.SuccessException" -> Ok None
|
||||
| "NUnit.Framework.IgnoreException" -> Ok (Some (TestMemberSuccess.Ignored (Option.ofObj exc.Message)))
|
||||
| "NUnit.Framework.InconclusiveException" ->
|
||||
Ok (Some (TestMemberSuccess.Inconclusive (Option.ofObj exc.Message)))
|
||||
| _ -> Error orig
|
||||
| Error orig -> Error orig
|
||||
match setUpResult with
|
||||
| Error e -> return Error [ e ], metadata ()
|
||||
| Ok () ->
|
||||
|
||||
// Unconditionally run TearDown after tests, even if tests failed.
|
||||
sw.Start ()
|
||||
let tearDownResult = runMethods TestFailure.TearDownFailed tearDown [||]
|
||||
sw.Stop ()
|
||||
sw.Start ()
|
||||
let! result = runMethods TestFailure.TestFailed [ test ] args
|
||||
sw.Stop ()
|
||||
|
||||
let metadata = metadata ()
|
||||
let result =
|
||||
match result with
|
||||
| Ok () -> Ok None
|
||||
| Error (TestFailure.TestFailed (UserMethodFailure.Threw (_, exc)) as orig) ->
|
||||
match exc.GetType().FullName with
|
||||
| "NUnit.Framework.SuccessException" -> Ok None
|
||||
| "NUnit.Framework.IgnoreException" ->
|
||||
Ok (Some (TestMemberSuccess.Ignored (Option.ofObj exc.Message)))
|
||||
| "NUnit.Framework.InconclusiveException" ->
|
||||
Ok (Some (TestMemberSuccess.Inconclusive (Option.ofObj exc.Message)))
|
||||
| _ -> Error orig
|
||||
| Error orig -> Error orig
|
||||
|
||||
match result, tearDownResult with
|
||||
| Ok None, Ok () -> Ok TestMemberSuccess.Ok, metadata
|
||||
| Ok (Some s), Ok () -> Ok s, metadata
|
||||
| Error e, Ok ()
|
||||
| Ok _, Error e -> Error [ e ], metadata
|
||||
| Error e1, Error e2 -> Error [ e1 ; e2 ], metadata
|
||||
// Unconditionally run TearDown after tests, even if tests failed.
|
||||
sw.Start ()
|
||||
let! tearDownResult = runMethods TestFailure.TearDownFailed tearDown [||]
|
||||
sw.Stop ()
|
||||
|
||||
let metadata = metadata ()
|
||||
|
||||
return
|
||||
match result, tearDownResult with
|
||||
| Ok None, Ok () -> Ok TestMemberSuccess.Ok, metadata
|
||||
| Ok (Some s), Ok () -> Ok s, metadata
|
||||
| Error e, Ok ()
|
||||
| Ok _, Error e -> Error [ e ], metadata
|
||||
| Error e1, Error e2 -> Error [ e1 ; e2 ], metadata
|
||||
}
|
||||
|
||||
let private getValues (test : SingleTestMethod) =
|
||||
let valuesAttrs =
|
||||
@@ -395,20 +458,22 @@ module TestFixture =
|
||||
|> Seq.map (fun (testGuid, args) ->
|
||||
task {
|
||||
let runMe () =
|
||||
progress.OnTestMemberStart test.Name
|
||||
let oldValue = contexts.AsyncLocal.Value
|
||||
let outputId = contexts.NewOutputs ()
|
||||
contexts.AsyncLocal.Value <- outputId
|
||||
async {
|
||||
progress.OnTestMemberStart test.Name
|
||||
let oldValue = contexts.AsyncLocal.Value
|
||||
let outputId = contexts.NewOutputs ()
|
||||
contexts.AsyncLocal.Value <- outputId
|
||||
|
||||
let result, meta =
|
||||
runOne outputId contexts setUp tearDown testGuid test.Method containingObject args
|
||||
let! result, meta =
|
||||
runOne outputId contexts setUp tearDown testGuid test.Method containingObject args
|
||||
|
||||
contexts.AsyncLocal.Value <- oldValue
|
||||
progress.OnTestMemberFinished test.Name
|
||||
contexts.AsyncLocal.Value <- oldValue
|
||||
progress.OnTestMemberFinished test.Name
|
||||
|
||||
result, meta
|
||||
return result, meta
|
||||
}
|
||||
|
||||
let! results, summary = par.Run running test.Parallelize runMe
|
||||
let! results, summary = par.RunAsync running test.Parallelize runMe
|
||||
|
||||
match results with
|
||||
| Ok results -> return Ok results, summary
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<PackageId>WoofWare.NUnitTestRunner.Lib</PackageId>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarnOn>FS3559</WarnOn>
|
||||
<WoofWareMyriadPluginVersion>8.0.5</WoofWareMyriadPluginVersion>
|
||||
<WoofWareMyriadPluginVersion>8.1.1</WoofWareMyriadPluginVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -31,6 +31,7 @@
|
||||
<Compile Include="ParallelScope.fs" />
|
||||
<Compile Include="DotnetRuntime.fs" />
|
||||
<Compile Include="Array.fs" />
|
||||
<Compile Include="Exception.fs" />
|
||||
<Compile Include="List.fs" />
|
||||
<Compile Include="Result.fs" />
|
||||
<Compile Include="Domain.fs" />
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.21",
|
||||
"version": "0.22",
|
||||
"publicReleaseRefSpec": [
|
||||
"^refs/heads/main$"
|
||||
],
|
||||
@@ -8,4 +8,4 @@
|
||||
":/Directory.Build.props",
|
||||
":/README.md"
|
||||
]
|
||||
}
|
||||
}
|
@@ -0,0 +1,396 @@
|
||||
namespace WoofWare.NUnitTestRunner.Test
|
||||
|
||||
open System
|
||||
open System.Text
|
||||
open System.Threading
|
||||
open System.Threading.Tasks
|
||||
open NUnit.Framework
|
||||
open FsUnitTyped
|
||||
open WoofWare.NUnitTestRunner
|
||||
|
||||
[<TestFixture>]
|
||||
module TestSynchronizationContext =
|
||||
|
||||
[<Test>]
|
||||
let ``ExecutionContext flows correctly through synchronous operations`` () =
|
||||
task {
|
||||
let dummyFixture =
|
||||
TestFixture.Empty typeof<obj> (Some (Parallelizable.Yes ClassParallelScope.All)) [] []
|
||||
|
||||
use contexts = TestContexts.Empty ()
|
||||
use queue = new ParallelQueue (Some 4, None)
|
||||
|
||||
// Track which context values we see during execution
|
||||
let contextValues = System.Collections.Concurrent.ConcurrentBag<Guid * Guid> ()
|
||||
|
||||
// Start the fixture
|
||||
let! running = queue.StartTestFixture dummyFixture
|
||||
let! _, setup = queue.RunTestSetup running (fun () -> ())
|
||||
|
||||
// Create several synchronous operations with different context values
|
||||
let tasks =
|
||||
[ 1..10 ]
|
||||
|> List.map (fun _ ->
|
||||
task {
|
||||
do! Task.Yield ()
|
||||
// Set a unique context value
|
||||
let outputId = contexts.NewOutputs ()
|
||||
let (OutputStreamId expectedId) = outputId
|
||||
contexts.AsyncLocal.Value <- outputId
|
||||
|
||||
// Run a synchronous operation that checks the context
|
||||
let! actualId =
|
||||
queue.Run
|
||||
setup
|
||||
None
|
||||
(fun () ->
|
||||
// Check context immediately
|
||||
let immediate = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId immediateGuid) = immediate
|
||||
contextValues.Add (expectedId, immediateGuid)
|
||||
|
||||
// Do some work that might cause context issues
|
||||
Thread.Sleep 10
|
||||
|
||||
// Check context after work
|
||||
let afterWork = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId afterWorkGuid) = afterWork
|
||||
contextValues.Add (expectedId, afterWorkGuid)
|
||||
|
||||
// Simulate calling into framework code that might use ExecutionContext
|
||||
let mutable capturedValue = Guid.Empty
|
||||
|
||||
ExecutionContext.Run (
|
||||
ExecutionContext.Capture (),
|
||||
(fun _ ->
|
||||
let current = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId currentGuid) = current
|
||||
capturedValue <- currentGuid
|
||||
),
|
||||
()
|
||||
)
|
||||
|
||||
contextValues.Add (expectedId, capturedValue)
|
||||
|
||||
afterWorkGuid
|
||||
)
|
||||
|
||||
// Verify the returned value matches what we set
|
||||
actualId |> shouldEqual expectedId
|
||||
}
|
||||
)
|
||||
|
||||
// Wait for all tasks
|
||||
let! results = Task.WhenAll tasks
|
||||
results |> Array.iter id
|
||||
|
||||
// Verify all context values were correct
|
||||
let allValues = contextValues |> Seq.toList
|
||||
allValues |> shouldHaveLength 30 // 3 checks per operation * 10 operations
|
||||
|
||||
// Every captured value should match its expected value
|
||||
allValues
|
||||
|> List.iter (fun (expected, actual) -> actual |> shouldEqual expected)
|
||||
|
||||
// Clean up
|
||||
let! _, teardown = queue.RunTestTearDown setup (fun () -> ())
|
||||
do! queue.EndTestFixture teardown
|
||||
}
|
||||
|
||||
[<Test>]
|
||||
let ``ExecutionContext isolation between concurrent synchronous operations`` () =
|
||||
task {
|
||||
let dummyFixture =
|
||||
TestFixture.Empty typeof<obj> (Some (Parallelizable.Yes ClassParallelScope.All)) [] []
|
||||
|
||||
use contexts = TestContexts.Empty ()
|
||||
use queue = new ParallelQueue (Some 4, None)
|
||||
|
||||
let! running = queue.StartTestFixture dummyFixture
|
||||
let! _, setup = queue.RunTestSetup running (fun () -> ())
|
||||
|
||||
// Use a barrier to ensure operations run concurrently
|
||||
let barrier = new Barrier (3)
|
||||
let seenValues = System.Collections.Concurrent.ConcurrentBag<int * Guid> ()
|
||||
let outputIds = System.Collections.Concurrent.ConcurrentBag<OutputStreamId> ()
|
||||
|
||||
// Create operations that will definitely run concurrently
|
||||
let tasks =
|
||||
[ 1..3 ]
|
||||
|> List.map (fun i ->
|
||||
task {
|
||||
// Each task sets its own context value
|
||||
let outputId = contexts.NewOutputs ()
|
||||
let (OutputStreamId myId) = outputId
|
||||
contexts.AsyncLocal.Value <- outputId
|
||||
outputIds.Add outputId
|
||||
|
||||
let! result =
|
||||
queue.Run
|
||||
setup
|
||||
(Some (Parallelizable.Yes ()))
|
||||
(fun () ->
|
||||
// Wait for all tasks to reach this point
|
||||
barrier.SignalAndWait ()
|
||||
|
||||
// Now check what value we see
|
||||
let currentValue = contexts.AsyncLocal.Value
|
||||
|
||||
match currentValue with
|
||||
| OutputStreamId guid -> seenValues.Add (i, guid)
|
||||
|
||||
// Do some synchronous work
|
||||
Thread.Sleep 5
|
||||
|
||||
// Check again after work
|
||||
let afterWork = contexts.AsyncLocal.Value
|
||||
|
||||
match afterWork with
|
||||
| OutputStreamId guid ->
|
||||
// Also verify we can write to the correct streams
|
||||
contexts.Stdout.WriteLine $"Task %i{i} sees context %O{guid}"
|
||||
guid
|
||||
)
|
||||
|
||||
// Each task should see its own value
|
||||
result |> shouldEqual myId
|
||||
}
|
||||
)
|
||||
|
||||
let! results = Task.WhenAll tasks
|
||||
results |> Array.iter id
|
||||
|
||||
// Verify we saw 3 different values (one per task)
|
||||
let values = seenValues |> Seq.toList
|
||||
values |> shouldHaveLength 3
|
||||
|
||||
// All seen values should be different (no context bleeding)
|
||||
let uniqueValues = values |> List.map snd |> List.distinct
|
||||
uniqueValues |> shouldHaveLength 3
|
||||
|
||||
let! _, teardown = queue.RunTestTearDown setup (fun () -> ())
|
||||
do! queue.EndTestFixture teardown
|
||||
|
||||
// Verify stdout content for each task
|
||||
let collectedOutputs = outputIds |> Seq.toList
|
||||
collectedOutputs |> shouldHaveLength 3
|
||||
|
||||
for outputId in collectedOutputs do
|
||||
let content = contexts.DumpStdout outputId
|
||||
content |> shouldNotEqual ""
|
||||
let (OutputStreamId guid) = outputId
|
||||
content |> shouldContainText (guid.ToString ())
|
||||
}
|
||||
|
||||
[<Test>]
|
||||
let ``ExecutionContext flows correctly through nested synchronous operations`` () =
|
||||
task {
|
||||
let dummyFixture =
|
||||
TestFixture.Empty typeof<obj> (Some (Parallelizable.Yes ClassParallelScope.All)) [] []
|
||||
|
||||
use contexts = TestContexts.Empty ()
|
||||
use queue = new ParallelQueue (Some 4, None)
|
||||
|
||||
let! running = queue.StartTestFixture dummyFixture
|
||||
let! _, setup = queue.RunTestSetup running (fun () -> ())
|
||||
|
||||
// Set an initial context
|
||||
let outputId = contexts.NewOutputs ()
|
||||
let (OutputStreamId outerGuid) = outputId
|
||||
contexts.AsyncLocal.Value <- outputId
|
||||
|
||||
let! result =
|
||||
queue.Run
|
||||
setup
|
||||
None
|
||||
(fun () ->
|
||||
// Check we have the outer context
|
||||
let outer = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId outerSeen) = outer
|
||||
outerSeen |> shouldEqual outerGuid
|
||||
|
||||
// Now change the context for a nested operation
|
||||
let innerOutputId = contexts.NewOutputs ()
|
||||
let (OutputStreamId innerGuid) = innerOutputId
|
||||
contexts.AsyncLocal.Value <- innerOutputId
|
||||
|
||||
// Use Task.Run to potentially hop threads
|
||||
let innerResult =
|
||||
Task
|
||||
.Run(fun () ->
|
||||
let inner = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId innerSeen) = inner
|
||||
innerSeen |> shouldEqual innerGuid
|
||||
innerSeen
|
||||
)
|
||||
.Result
|
||||
|
||||
// After the nested operation, we should still have our inner context
|
||||
let afterNested = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId afterNestedGuid) = afterNested
|
||||
afterNestedGuid |> shouldEqual innerGuid
|
||||
|
||||
(outerSeen, innerResult, afterNestedGuid)
|
||||
)
|
||||
|
||||
// Unpack results
|
||||
let seenOuter, seenInner, seenAfter = result
|
||||
seenOuter |> shouldEqual outerGuid
|
||||
seenInner |> shouldNotEqual outerGuid
|
||||
seenAfter |> shouldEqual seenInner
|
||||
|
||||
let! _, teardown = queue.RunTestTearDown setup (fun () -> ())
|
||||
do! queue.EndTestFixture teardown
|
||||
}
|
||||
|
||||
[<Test>]
|
||||
let ``ExecutionContext flows correctly through async operations`` () =
|
||||
task {
|
||||
// Create a test fixture
|
||||
let dummyFixture =
|
||||
TestFixture.Empty typeof<obj> (Some (Parallelizable.Yes ClassParallelScope.All)) [] []
|
||||
|
||||
use contexts = TestContexts.Empty ()
|
||||
use queue = new ParallelQueue (Some 4, None)
|
||||
|
||||
// Track which context values we see during execution
|
||||
let contextValues = System.Collections.Concurrent.ConcurrentBag<Guid * Guid> ()
|
||||
|
||||
// Start the fixture
|
||||
let! running = queue.StartTestFixture dummyFixture
|
||||
let! _, setup = queue.RunTestSetup running (fun () -> ())
|
||||
|
||||
// Create several async operations with different context values
|
||||
let tasks =
|
||||
[ 1..10 ]
|
||||
|> List.map (fun i ->
|
||||
task {
|
||||
// Set a unique context value
|
||||
let expectedId = Guid.NewGuid ()
|
||||
let outputId = OutputStreamId expectedId
|
||||
contexts.AsyncLocal.Value <- outputId
|
||||
|
||||
// Run an async operation that checks the context at multiple points
|
||||
let! actualId =
|
||||
queue.RunAsync
|
||||
setup
|
||||
None
|
||||
(fun () ->
|
||||
async {
|
||||
// Check context immediately
|
||||
let immediate = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId immediateGuid) = immediate
|
||||
contextValues.Add (expectedId, immediateGuid)
|
||||
|
||||
// Yield to allow potential context loss
|
||||
do! Async.Sleep 10
|
||||
|
||||
// Check context after yield
|
||||
let afterYield = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId afterYieldGuid) = afterYield
|
||||
contextValues.Add (expectedId, afterYieldGuid)
|
||||
|
||||
// Do some actual async work
|
||||
do! Task.Delay (10) |> Async.AwaitTask
|
||||
|
||||
// Check context after task
|
||||
let afterTask = contexts.AsyncLocal.Value
|
||||
let (OutputStreamId afterTaskGuid) = afterTask
|
||||
contextValues.Add (expectedId, afterTaskGuid)
|
||||
|
||||
return afterTaskGuid
|
||||
}
|
||||
)
|
||||
|
||||
// Verify the returned value matches what we set
|
||||
actualId |> shouldEqual expectedId
|
||||
}
|
||||
)
|
||||
|
||||
// Wait for all tasks
|
||||
let! results = Task.WhenAll (tasks)
|
||||
results |> Array.iter id
|
||||
|
||||
// Verify all context values were correct
|
||||
let allValues = contextValues |> Seq.toList
|
||||
allValues |> shouldHaveLength 30 // 3 checks per operation * 10 operations
|
||||
|
||||
// Every captured value should match its expected value
|
||||
allValues
|
||||
|> List.iter (fun (expected, actual) -> actual |> shouldEqual expected)
|
||||
|
||||
// Clean up
|
||||
let! _, teardown = queue.RunTestTearDown setup (fun () -> ())
|
||||
do! queue.EndTestFixture teardown
|
||||
}
|
||||
|
||||
[<Test>]
|
||||
let ``ExecutionContext isolation between concurrent operations`` () =
|
||||
task {
|
||||
let dummyFixture =
|
||||
TestFixture.Empty typeof<obj> (Some (Parallelizable.Yes ClassParallelScope.All)) [] []
|
||||
|
||||
use contexts = TestContexts.Empty ()
|
||||
use queue = new ParallelQueue (Some 4, None)
|
||||
|
||||
let! running = queue.StartTestFixture dummyFixture
|
||||
let! _, setup = queue.RunTestSetup running (fun () -> ())
|
||||
|
||||
// Use a barrier to ensure operations run concurrently
|
||||
let barrier = new Barrier (3)
|
||||
let seenValues = System.Collections.Concurrent.ConcurrentBag<int * Guid> ()
|
||||
|
||||
// Create operations that will definitely run concurrently
|
||||
let tasks =
|
||||
[ 1..3 ]
|
||||
|> List.map (fun i ->
|
||||
task {
|
||||
// Each task sets its own context value
|
||||
let myId = Guid.NewGuid ()
|
||||
contexts.AsyncLocal.Value <- OutputStreamId myId
|
||||
|
||||
let! result =
|
||||
queue.RunAsync
|
||||
setup
|
||||
(Some (Parallelizable.Yes ()))
|
||||
(fun () ->
|
||||
async {
|
||||
// Wait for all tasks to reach this point
|
||||
barrier.SignalAndWait () |> ignore
|
||||
|
||||
// Now check what value we see
|
||||
let currentValue = contexts.AsyncLocal.Value
|
||||
|
||||
match currentValue with
|
||||
| OutputStreamId guid -> seenValues.Add (i, guid)
|
||||
|
||||
// Do some async work
|
||||
do! Async.Sleep 5
|
||||
|
||||
// Check again after async work
|
||||
let afterAsync = contexts.AsyncLocal.Value
|
||||
|
||||
match afterAsync with
|
||||
| OutputStreamId guid -> return guid
|
||||
}
|
||||
)
|
||||
|
||||
// Each task should see its own value
|
||||
result |> shouldEqual myId
|
||||
}
|
||||
)
|
||||
|
||||
let! results = Task.WhenAll (tasks)
|
||||
results |> Array.iter id
|
||||
|
||||
// Verify we saw 3 different values (one per task)
|
||||
let values = seenValues |> Seq.toList
|
||||
values |> shouldHaveLength 3
|
||||
|
||||
// All seen values should be different (no context bleeding)
|
||||
let uniqueValues = values |> List.map snd |> List.distinct
|
||||
uniqueValues |> shouldHaveLength 3
|
||||
|
||||
let! _, teardown = queue.RunTestTearDown setup (fun () -> ())
|
||||
do! queue.EndTestFixture teardown
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
@@ -11,13 +11,14 @@
|
||||
<Compile Include="TestFilter.fs" />
|
||||
<Compile Include="TestList.fs" />
|
||||
<Compile Include="TestSurface.fs" />
|
||||
<Compile Include="TestSynchronizationContext.fs" />
|
||||
<Compile Include="TestTrx.fs" />
|
||||
<EmbeddedResource Include="Example1.trx" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ApiSurface" Version="4.1.22" />
|
||||
<PackageReference Include="FsCheck" Version="3.3.0" />
|
||||
<PackageReference Include="ApiSurface" Version="5.0.2" />
|
||||
<PackageReference Include="FsCheck" Version="3.3.1" />
|
||||
<PackageReference Include="FsUnit" Version="7.1.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RollForward>Major</RollForward>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -35,7 +35,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console" Version="0.50.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.51.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"version": "0.3",
|
||||
"publicReleaseRefSpec": [
|
||||
"^refs/heads/main$"
|
||||
],
|
||||
|
6
flake.lock
generated
6
flake.lock
generated
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1753432016,
|
||||
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
|
||||
"lastModified": 1758262103,
|
||||
"narHash": "sha256-aBGl3XEOsjWw6W3AHiKibN7FeoG73dutQQEqnd/etR8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6027c30c8e9810896b92429f0092f624f7b1aace",
|
||||
"rev": "12bd230118a1901a4a5d393f9f56b6ad7e571d01",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@@ -14,8 +14,8 @@
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
pname = "unofficial-nunit-runner";
|
||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_8_0;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0;
|
||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_9_0;
|
||||
version = "0.1";
|
||||
dotnetTool = dllOverride: toolName: toolVersion: hash:
|
||||
pkgs.stdenvNoCC.mkDerivation rec {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"pname": "ApiSurface",
|
||||
"version": "4.1.22",
|
||||
"hash": "sha256-voj9m3YmyJ95FAMLV4sWzQMod3Em0mTjzf0LBUUFOso="
|
||||
"version": "5.0.2",
|
||||
"hash": "sha256-zcq1H1ccQzsZQf4kolzoOBSbyz07skihgPAvQ9Jri+E="
|
||||
},
|
||||
{
|
||||
"pname": "fantomas",
|
||||
@@ -21,13 +21,18 @@
|
||||
},
|
||||
{
|
||||
"pname": "FsCheck",
|
||||
"version": "3.3.0",
|
||||
"hash": "sha256-TFDR/uAGv4OqrMX8/reQ4faaAhH9hxTHr1T/YkNPCCU="
|
||||
"version": "3.3.1",
|
||||
"hash": "sha256-k65ksdOSOGz+meRUUND+yuqJtm5ChaKuaxmRIdKzx2Y="
|
||||
},
|
||||
{
|
||||
"pname": "fsharp-analyzers",
|
||||
"version": "0.32.0",
|
||||
"hash": "sha256-MnhsK5tOeexL6uQhsV4nTRz8CGbz2o8VyHwAK8x91pE="
|
||||
"version": "0.32.1",
|
||||
"hash": "sha256-le6rPnAF7cKGBZ2w8H2u9glK+6rT2ZjiAVnrkH2IhrM="
|
||||
},
|
||||
{
|
||||
"pname": "FSharp.Core",
|
||||
"version": "4.3.4",
|
||||
"hash": "sha256-styyo+6mJy+yxE0NZG/b1hxkAjPOnJfMgd9zWzCJ5uk="
|
||||
},
|
||||
{
|
||||
"pname": "FSharp.Core",
|
||||
@@ -36,8 +41,8 @@
|
||||
},
|
||||
{
|
||||
"pname": "FSharp.Core",
|
||||
"version": "8.0.403",
|
||||
"hash": "sha256-3XSQp7JUOU5T6gvSQXNfBF4t4gaX4J4xushH+cfM9mE="
|
||||
"version": "9.0.303",
|
||||
"hash": "sha256-AxR6wqodeU23KOTgkUfIgbavgbcSuzD4UBP+tiFydgA="
|
||||
},
|
||||
{
|
||||
"pname": "FsUnit",
|
||||
@@ -129,15 +134,20 @@
|
||||
"version": "6.0.36",
|
||||
"hash": "sha256-0xIJYFzxdMcnCj3wzkFRQZSnQcPHzPHMzePRIOA3oJs="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.NETCore.Platforms",
|
||||
"version": "1.1.0",
|
||||
"hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.NETCore.Platforms",
|
||||
"version": "1.1.1",
|
||||
"hash": "sha256-8hLiUKvy/YirCWlFwzdejD2Db3DaXhHxT7GSZx/znJg="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.NETCore.Platforms",
|
||||
"version": "2.0.0",
|
||||
"hash": "sha256-IEvBk6wUXSdyCnkj6tHahOJv290tVVT8tyemYcR0Yro="
|
||||
"pname": "Microsoft.NETCore.Targets",
|
||||
"version": "1.1.0",
|
||||
"hash": "sha256-0AqQ2gMS8iNlYkrD+BxtIg7cXMnr9xZHtKAuN4bjfaQ="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.NETCore.Targets",
|
||||
@@ -196,8 +206,8 @@
|
||||
},
|
||||
{
|
||||
"pname": "Nerdbank.GitVersioning",
|
||||
"version": "3.8.38-alpha",
|
||||
"hash": "sha256-gPMrVbjOZxXoofczF/pn6eVkLhjVSJIyQrLO2oljrDc="
|
||||
"version": "3.8.118",
|
||||
"hash": "sha256-Hmyy0ZKOmwN4zIhI4+MqoN8geZNc1sd033aZJ6APrO8="
|
||||
},
|
||||
{
|
||||
"pname": "Newtonsoft.Json",
|
||||
@@ -261,8 +271,8 @@
|
||||
},
|
||||
{
|
||||
"pname": "Spectre.Console",
|
||||
"version": "0.50.0",
|
||||
"hash": "sha256-3MNgumQSXzuXVGj7kLb5FMkTH/LoFohMvUjAZ7nyHfo="
|
||||
"version": "0.51.1",
|
||||
"hash": "sha256-FQAK07dEwEsNYVI1T3S488LHv8AXJ+ZeA9N2hPpWBoo="
|
||||
},
|
||||
{
|
||||
"pname": "System.Collections.Immutable",
|
||||
@@ -284,36 +294,16 @@
|
||||
"version": "6.0.0",
|
||||
"hash": "sha256-KaMHgIRBF7Nf3VwOo+gJS1DcD+41cJDPWFh+TDQ8ee8="
|
||||
},
|
||||
{
|
||||
"pname": "System.IO.Abstractions",
|
||||
"version": "4.2.13",
|
||||
"hash": "sha256-nkC/PiqE6+c1HJ2yTwg3x+qdBh844Z8n3ERWDW8k6Gg="
|
||||
},
|
||||
{
|
||||
"pname": "System.IO.FileSystem.AccessControl",
|
||||
"version": "4.5.0",
|
||||
"hash": "sha256-ck44YBQ0M+2Im5dw0VjBgFD1s0XuY54cujrodjjSBL8="
|
||||
},
|
||||
{
|
||||
"pname": "System.Memory",
|
||||
"version": "4.5.5",
|
||||
"hash": "sha256-EPQ9o1Kin7KzGI5O3U3PUQAZTItSbk9h/i4rViN3WiI="
|
||||
},
|
||||
{
|
||||
"pname": "System.Memory",
|
||||
"version": "4.6.3",
|
||||
"hash": "sha256-JgeK63WMmumF6L+FH5cwJgYdpqXrSDcgTQwtIgTHKVU="
|
||||
},
|
||||
{
|
||||
"pname": "System.Private.Uri",
|
||||
"version": "4.3.0",
|
||||
"hash": "sha256-fVfgcoP4AVN1E5wHZbKBIOPYZ/xBeSIdsNF+bdukIRM="
|
||||
},
|
||||
{
|
||||
"pname": "System.Reflection.Metadata",
|
||||
"version": "1.6.0",
|
||||
"hash": "sha256-JJfgaPav7UfEh4yRAQdGhLZF1brr0tUWPl6qmfNWq/E="
|
||||
},
|
||||
{
|
||||
"pname": "System.Reflection.Metadata",
|
||||
"version": "8.0.0",
|
||||
@@ -329,11 +319,6 @@
|
||||
"version": "6.0.0",
|
||||
"hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
|
||||
},
|
||||
{
|
||||
"pname": "System.Security.AccessControl",
|
||||
"version": "4.5.0",
|
||||
"hash": "sha256-AFsKPb/nTk2/mqH/PYpaoI8PLsiKKimaXf+7Mb5VfPM="
|
||||
},
|
||||
{
|
||||
"pname": "System.Security.Cryptography.Pkcs",
|
||||
"version": "6.0.4",
|
||||
@@ -344,11 +329,6 @@
|
||||
"version": "4.4.0",
|
||||
"hash": "sha256-Ri53QmFX8I8UH0x4PikQ1ZA07ZSnBUXStd5rBfGWFOE="
|
||||
},
|
||||
{
|
||||
"pname": "System.Security.Principal.Windows",
|
||||
"version": "4.5.0",
|
||||
"hash": "sha256-BkUYNguz0e4NJp1kkW7aJBn3dyH9STwB5N8XqnlCsmY="
|
||||
},
|
||||
{
|
||||
"pname": "System.Text.Json",
|
||||
"version": "8.0.5",
|
||||
@@ -366,13 +346,13 @@
|
||||
},
|
||||
{
|
||||
"pname": "WoofWare.Myriad.Plugins",
|
||||
"version": "8.0.5",
|
||||
"hash": "sha256-IfTT2GM9ktUW5BQoKQGFKK39BAKeziJJnrOIL7Vs19o="
|
||||
"version": "8.1.1",
|
||||
"hash": "sha256-Cx7/+5+qc8lhxb0On/OW58PrabadMfbcNP1TvLZGHrY="
|
||||
},
|
||||
{
|
||||
"pname": "WoofWare.Myriad.Plugins.Attributes",
|
||||
"version": "3.6.12",
|
||||
"hash": "sha256-90uiVtc5exCbkcdS8DgTmlEZZT8/AdrY0QuFzy+FHUo="
|
||||
"version": "3.7.1",
|
||||
"hash": "sha256-Mn0OVvC/AwTYg6+3MCKLkNKGGSdzLfBOaLapxHsUDCw="
|
||||
},
|
||||
{
|
||||
"pname": "WoofWare.PrattParser",
|
||||
|
Reference in New Issue
Block a user