Compare commits

...

29 Commits

Author SHA1 Message Date
Patrick Stevens
99e0fdff08 Make ParallelQueue surface its errors, correctly flow ExecutionContext (#278) 2025-07-29 22:04:45 +01:00
dependabot[bot]
fda4e7ba60 Bump WoofWare.Myriad.Plugins from 8.0.4 to 8.0.5 (#277)
* Bump WoofWare.Myriad.Plugins from 8.0.4 to 8.0.5

---
updated-dependencies:
- dependency-name: WoofWare.Myriad.Plugins
  dependency-version: 8.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-07-28 20:42:53 +01:00
patrick-conscriptus[bot]
dfdfa84733 Automated commit (#276)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-07-27 00:56:11 +00:00
patrick-conscriptus[bot]
c218110749 Automated commit (#275)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-07-20 00:55:52 +00:00
dependabot[bot]
309968721c Bump ApiSurface from 4.1.21 to 4.1.22 (#274)
* Bump ApiSurface from 4.1.21 to 4.1.22

---
updated-dependencies:
- dependency-name: ApiSurface
  dependency-version: 4.1.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-07-14 21:03:17 +00:00
patrick-conscriptus[bot]
876ca9e625 Automated commit (#273)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-07-13 00:54:57 +00:00
patrick-conscriptus[bot]
59f9789cdc Automated commit (#271)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-07-06 00:53:02 +00:00
dependabot[bot]
c8c28b9a32 Bump FsUnit to 7.1.1 (#270)
* Bump FsUnit to 7.1.1

---
updated-dependencies:
- dependency-name: FsUnit
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: FsUnit
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: FsUnit
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-06-30 20:56:14 +00:00
patrick-conscriptus[bot]
6d87610017 Automated commit (#269)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-06-29 00:53:55 +00:00
dependabot[bot]
9f28334b7f Bump WoofWare.Myriad.Plugins from 7.0.7 to 8.0.4 (#268)
* Bump WoofWare.Myriad.Plugins from 7.0.7 to 8.0.4

---
updated-dependencies:
- dependency-name: WoofWare.Myriad.Plugins
  dependency-version: 8.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-06-23 23:00:17 +00:00
patrick-conscriptus[bot]
5cf2261d7f Automated commit (#267)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-06-22 00:52:35 +00:00
dependabot[bot]
c3589820c3 Bump ApiSurface from 4.1.20 to 4.1.21 (#265)
* Bump ApiSurface from 4.1.20 to 4.1.21

---
updated-dependencies:
- dependency-name: ApiSurface
  dependency-version: 4.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-06-16 18:39:50 +00:00
dependabot[bot]
5d0c205f21 Bump actions/attest-build-provenance from 2.3.0 to 2.4.0 (#266) 2025-06-16 17:59:44 +00:00
patrick-conscriptus[bot]
16135fbd56 Automated commit (#264)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-06-15 00:52:57 +00:00
dependabot[bot]
cbc51cde14 Bump FsCheck and Microsoft.NET.Test.Sdk (#263)
* Bump FsCheck and Microsoft.NET.Test.Sdk

Bumps FsCheck from 3.2.0 to 3.3.0
Bumps Microsoft.NET.Test.Sdk to 17.14.1, 17.14.1, 17.14.1

---
updated-dependencies:
- dependency-name: FsCheck
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 17.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 17.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 17.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

* Bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Smaug123 <3138005+Smaug123@users.noreply.github.com>
2025-06-13 22:28:06 +01:00
patrick-conscriptus[bot]
6a4b900aa9 Automated commit (#262)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-06-08 00:51:50 +00:00
patrick-conscriptus[bot]
4169289ded Automated commit (#261)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-06-01 00:57:26 +00:00
patrick-conscriptus[bot]
7df58d5309 Automated commit (#260)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-05-25 00:50:30 +00:00
patrick-conscriptus[bot]
ac4c8c245e Automated commit (#259)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-05-18 00:48:51 +00:00
patrick-conscriptus[bot]
3e79e79978 Automated commit (#258)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-05-11 00:48:12 +00:00
patrick-conscriptus[bot]
c16e7dd1ee Automated commit (#257)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-05-04 00:49:25 +00:00
dependabot[bot]
cd66617ce7 Bump FSharp.Core and WoofWare.Myriad.Plugins (#256)
* Bump FSharp.Core and WoofWare.Myriad.Plugins

Bumps [FSharp.Core](https://github.com/dotnet/fsharp) and [WoofWare.Myriad.Plugins](https://github.com/Smaug123/WoofWare.Myriad). These dependencies needed to be updated together.

Updates `FSharp.Core` from 6.0.1 to 6.0.1
- [Release notes](https://github.com/dotnet/fsharp/releases)
- [Changelog](https://github.com/dotnet/fsharp/blob/main/release-notes.md)
- [Commits](https://github.com/dotnet/fsharp/commits)

Updates `WoofWare.Myriad.Plugins` from 4.0.14 to 7.0.7
- [Release notes](https://github.com/Smaug123/WoofWare.Myriad/releases)
- [Changelog](https://github.com/Smaug123/WoofWare.Myriad/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Smaug123/WoofWare.Myriad/compare/WoofWare.Myriad.Plugins.4.0.14...WoofWare.Myriad.Plugins.7.0.7)

---
updated-dependencies:
- dependency-name: FSharp.Core
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: WoofWare.Myriad.Plugins
  dependency-version: 7.0.7
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Stevens <3138005+Smaug123@users.noreply.github.com>
2025-04-29 22:05:29 +01:00
dependabot[bot]
144f71a417 Bump actions/attest-build-provenance from 2.2.3 to 2.3.0 (#255)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.2.3 to 2.3.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](c074443f1a...db473fddc0)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 23:20:49 +01:00
patrick-conscriptus[bot]
3673fc56ee Automated commit (#254)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-04-27 00:47:27 +00:00
patrick-conscriptus[bot]
5f74b41825 Automated commit (#253)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-04-20 00:47:23 +00:00
dependabot[bot]
2068007da0 Bump Spectre.Console from 0.49.1 to 0.50.0 (#251) 2025-04-14 18:56:23 +01:00
patrick-conscriptus[bot]
6fd824c065 Automated commit (#250)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-04-13 02:10:19 +00:00
dependabot[bot]
0480d5c151 Bump actions/create-github-app-token from 1 to 2 (#249)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1 to 2.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 18:50:59 +01:00
patrick-conscriptus[bot]
9107b1b502 Automated commit (#248)
Co-authored-by: patrick-conscriptus[bot] <175414948+patrick-conscriptus[bot]@users.noreply.github.com>
2025-04-06 00:45:50 +00:00
16 changed files with 538 additions and 103 deletions

View File

@@ -3,13 +3,13 @@
"isRoot": true,
"tools": {
"fantomas": {
"version": "7.0.1",
"version": "7.0.3",
"commands": [
"fantomas"
]
},
"fsharp-analyzers": {
"version": "0.30.0",
"version": "0.32.0",
"commands": [
"fsharp-analyzers"
]

View File

@@ -264,7 +264,7 @@ jobs:
name: nuget-package-lib
path: packed
- name: Attest Build Provenance
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: "packed/*.nupkg"
@@ -283,7 +283,7 @@ jobs:
name: nuget-package-tool
path: packed
- name: Attest Build Provenance
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: "packed/*.nupkg"

View File

@@ -32,7 +32,7 @@ jobs:
- name: Create token
id: generate-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v2
with:
# https://github.com/actions/create-github-app-token/issues/136
app-id: ${{ secrets.APP_ID }}

View File

@@ -27,8 +27,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsUnit" Version="7.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0"/>
<PackageReference Include="FsUnit" Version="7.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
</ItemGroup>

View File

@@ -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.38-alpha" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup Condition="'$(GITHUB_ACTION)' != ''">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@@ -11,8 +11,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsUnit" Version="7.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0"/>
<PackageReference Include="FsUnit" Version="7.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="NUnit" Version="4.3.2"/>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
</ItemGroup>

View File

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

View 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"

View File

@@ -5,14 +5,14 @@ open System.Threading
open System.Threading.Tasks
type private ThunkEvaluator<'ret> =
abstract Eval<'a> : (unit -> 'a) -> AsyncReplyChannel<'a> -> 'ret
abstract Eval<'a> : (unit -> 'a) -> AsyncReplyChannel<Result<'a, exn>> -> 'ret
type private ThunkCrate =
abstract Apply<'ret> : ThunkEvaluator<'ret> -> 'ret
[<RequireQualifiedAccess>]
module private ThunkCrate =
let make<'a> (t : unit -> 'a) (rc : AsyncReplyChannel<'a>) : ThunkCrate =
let make<'a> (t : unit -> 'a) (rc : AsyncReplyChannel<Result<'a, exn>>) : ThunkCrate =
{ new ThunkCrate with
member _.Apply e = e.Eval t rc
}
@@ -41,7 +41,7 @@ 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
| RunTest of within : TestFixture * Parallelizable<unit> option * test : ThunkCrate * context : ExecutionContext
| BeginTestFixture of TestFixture * AsyncReplyChannel<TestFixtureRunningToken>
| EndTestFixture of TestFixtureTearDownToken * AsyncReplyChannel<unit>
@@ -310,18 +310,23 @@ type ParallelQueue
rc.Reply ()
m.Post MailboxMessage.Reconcile
return! processTask (Running state) m
| MailboxMessage.RunTest (withinFixture, par, message) ->
| MailboxMessage.RunTest (withinFixture, par, message, capturedContext) ->
let t () =
{ new ThunkEvaluator<_> with
member _.Eval<'b> (t : unit -> 'b) rc =
let tcs = TaskCompletionSource TaskCreationOptions.RunContinuationsAsynchronously
use ec = ExecutionContext.Capture ()
fun () ->
ExecutionContext.Run (
ec,
capturedContext,
(fun _ ->
let result = t ()
let result =
try
let r = t ()
Ok r
with e ->
Error e
tcs.SetResult ()
m.Post MailboxMessage.Reconcile
rc.Reply result
@@ -356,9 +361,18 @@ type ParallelQueue
(action : unit -> 'a)
: 'a Task
=
(fun rc -> MailboxMessage.RunTest (parent, scope, ThunkCrate.make action rc))
|> mb.PostAndAsyncReply
|> Async.StartAsTask
let ec = ExecutionContext.Capture ()
task {
let! result =
(fun rc -> MailboxMessage.RunTest (parent, scope, ThunkCrate.make action rc, ec))
|> mb.PostAndAsyncReply
|> Async.StartAsTask
match result with
| Ok o -> return o
| Error e -> return Exception.reraiseWithOriginalStackTrace e
}
/// 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 +393,15 @@ 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.RunTest (parent, par, ThunkCrate.make 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 +419,15 @@ 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.RunTest (parent, par, ThunkCrate.make 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.

View File

@@ -14,7 +14,7 @@
<PackageId>WoofWare.NUnitTestRunner.Lib</PackageId>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarnOn>FS3559</WarnOn>
<WoofWareMyriadPluginVersion>4.0.14</WoofWareMyriadPluginVersion>
<WoofWareMyriadPluginVersion>8.0.5</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" />

View File

@@ -0,0 +1,409 @@
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
for expected, actual in allValues do
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 option>()
// 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, Some guid)
| _ -> seenValues.Add(i, None)
// 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
| _ ->
return failwith "Lost context after async"
}
)
// 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
// Each task should have seen a value
for (taskId, value) in values do
value |> shouldNotEqual None
// All seen values should be different (no context bleeding)
let uniqueValues =
values
|> List.choose snd
|> List.distinct
uniqueValues |> shouldHaveLength 3
let! _, teardown = queue.RunTestTearDown setup (fun () -> ())
do! queue.EndTestFixture teardown
}
*)

View File

@@ -11,15 +11,16 @@
<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.20" />
<PackageReference Include="FsCheck" Version="3.1.0" />
<PackageReference Include="FsUnit" Version="7.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="ApiSurface" Version="4.1.22" />
<PackageReference Include="FsCheck" Version="3.3.0" />
<PackageReference Include="FsUnit" Version="7.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
</ItemGroup>

View File

@@ -35,7 +35,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Spectre.Console" Version="0.50.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.14.0]" />
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.17.0]" />
</ItemGroup>
</Project>

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1743076231,
"narHash": "sha256-yQugdVfi316qUfqzN8JMaA2vixl+45GxNm4oUfXlbgw=",
"lastModified": 1753432016,
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6c5963357f3c1c840201eda129a99d455074db04",
"rev": "6027c30c8e9810896b92429f0092f624f7b1aace",
"type": "github"
},
"original": {

View File

@@ -1,13 +1,13 @@
[
{
"pname": "ApiSurface",
"version": "4.1.20",
"hash": "sha256-koWgO9FC9ax+Ij56ug8kxeyknl0yhLqnNLOUdxtqqo4="
"version": "4.1.22",
"hash": "sha256-voj9m3YmyJ95FAMLV4sWzQMod3Em0mTjzf0LBUUFOso="
},
{
"pname": "fantomas",
"version": "7.0.1",
"hash": "sha256-2aGD6Kjh83gmssRqqZ/Uihi7VbNqNUelX4otIfCuhTI="
"version": "7.0.3",
"hash": "sha256-0XlfV7SxXPDnk/CjkUesJSaH0cxlNHJ+Jj86zNUhkNA="
},
{
"pname": "Fantomas.Core",
@@ -21,13 +21,13 @@
},
{
"pname": "FsCheck",
"version": "3.1.0",
"hash": "sha256-u0Ha94EjClJ8evNSHu8d6+Dx9qYM3kV3sl1PEq9gF8s="
"version": "3.3.0",
"hash": "sha256-TFDR/uAGv4OqrMX8/reQ4faaAhH9hxTHr1T/YkNPCCU="
},
{
"pname": "fsharp-analyzers",
"version": "0.30.0",
"hash": "sha256-7oaSwpHAU1opzpz4szLU/gDaJC/ww9eiFkPu0nr4Mj4="
"version": "0.32.0",
"hash": "sha256-MnhsK5tOeexL6uQhsV4nTRz8CGbz2o8VyHwAK8x91pE="
},
{
"pname": "FSharp.Core",
@@ -41,8 +41,8 @@
},
{
"pname": "FsUnit",
"version": "7.0.1",
"hash": "sha256-K85CIdxMeFSHEKZk6heIXp/oFjWAn7dBILKrw49pJUY="
"version": "7.1.1",
"hash": "sha256-UMCEGKxQ4ytjmPuVpiNaAPbi3RQH9gqa61JJIUS/6hg="
},
{
"pname": "Microsoft.ApplicationInsights",
@@ -76,13 +76,13 @@
},
{
"pname": "Microsoft.CodeCoverage",
"version": "17.13.0",
"hash": "sha256-GKrIxeyQo5Az1mztfQgea1kGtJwonnNOrXK/0ULfu8o="
"version": "17.14.1",
"hash": "sha256-f8QytG8GvRoP47rO2KEmnDLxIpyesaq26TFjDdW40Gs="
},
{
"pname": "Microsoft.NET.Test.Sdk",
"version": "17.13.0",
"hash": "sha256-sc2wvyV8cGm1FrNP2GGHEI584RCvRPu15erYCsgw5QY="
"version": "17.14.1",
"hash": "sha256-mZUzDFvFp7x1nKrcnRd0hhbNu5g8EQYt8SKnRgdhT/A="
},
{
"pname": "Microsoft.NETCore.App.Host.linux-arm64",
@@ -176,13 +176,13 @@
},
{
"pname": "Microsoft.TestPlatform.ObjectModel",
"version": "17.13.0",
"hash": "sha256-6S0fjfj8vA+h6dJVNwLi6oZhYDO/I/6hBZaq2VTW+Uk="
"version": "17.14.1",
"hash": "sha256-QMf6O+w0IT+16Mrzo7wn+N20f3L1/mDhs/qjmEo1rYs="
},
{
"pname": "Microsoft.TestPlatform.TestHost",
"version": "17.13.0",
"hash": "sha256-L/CJzou7dhmShUgXq3aXL3CaLTJll17Q+JY2DBdUUpo="
"version": "17.14.1",
"hash": "sha256-1cxHWcvHRD7orQ3EEEPPxVGEkTpxom1/zoICC9SInJs="
},
{
"pname": "Myriad.Core",
@@ -199,11 +199,6 @@
"version": "3.8.38-alpha",
"hash": "sha256-gPMrVbjOZxXoofczF/pn6eVkLhjVSJIyQrLO2oljrDc="
},
{
"pname": "Newtonsoft.Json",
"version": "13.0.1",
"hash": "sha256-K2tSVW4n4beRPzPu3rlVaBEMdGvWSv/3Q1fxaDh4Mjo="
},
{
"pname": "Newtonsoft.Json",
"version": "13.0.3",
@@ -211,33 +206,33 @@
},
{
"pname": "NuGet.Common",
"version": "6.13.2",
"hash": "sha256-ASLa/Jigg5Eop0ZrXPl98RW2rxnJRC7pbbxhuV74hFw="
"version": "6.14.0",
"hash": "sha256-jDOwt3veI1GSG8CfBnf2+dJxD3E/Nmlc+vHtD4J76Ms="
},
{
"pname": "NuGet.Configuration",
"version": "6.13.2",
"hash": "sha256-z8VW1YdRDanyyRTDYRvRkSv/XPR3c/hMM1y8cNNjx0Y="
"version": "6.14.0",
"hash": "sha256-1PN9s6fhCw3wd2260U6hQ4vG3jIvcG8GIn1oQgxMXA0="
},
{
"pname": "NuGet.Frameworks",
"version": "6.13.2",
"hash": "sha256-caDyc+WgYOo43AUTjtbP0MyvYDb6JweEKDdIul61Cac="
"version": "6.14.0",
"hash": "sha256-3ViM3R1ucQMEL2hQYsivT86kI6veMQK2xDsiAcFcVQk="
},
{
"pname": "NuGet.Packaging",
"version": "6.13.2",
"hash": "sha256-lhO+SFwIYZ4aPHxIGm5ubkkE2a5Ve2xgtroRbNh7hpw="
"version": "6.14.0",
"hash": "sha256-Yafbnxs3maj55bJ1oKQiZ0QkntFUzXdhorL94YEUOhY="
},
{
"pname": "NuGet.Protocol",
"version": "6.13.2",
"hash": "sha256-5lnAHHZjy7A4vgv65AeBAs64mSNpuoUjxW3HnrMpuzY="
"version": "6.14.0",
"hash": "sha256-uLDKfs+QN1MdnuQtTES8qfNzzsmYKM6XB9pwJc4G+eo="
},
{
"pname": "NuGet.Versioning",
"version": "6.13.2",
"hash": "sha256-gmpyBpKnt+GHqgx/2uFKp+J2csbxEAy1E7WdVT117sw="
"version": "6.14.0",
"hash": "sha256-DqdOJgsphKxSvqB8b60zNPCaiLfbiu3WnUJ/90feLrY="
},
{
"pname": "NUnit",
@@ -266,8 +261,8 @@
},
{
"pname": "Spectre.Console",
"version": "0.49.1",
"hash": "sha256-tqSVojyuQjuB34lXo759NOcyLgNIw815mKXJPq5JFDo="
"version": "0.50.0",
"hash": "sha256-3MNgumQSXzuXVGj7kLb5FMkTH/LoFohMvUjAZ7nyHfo="
},
{
"pname": "System.Collections.Immutable",
@@ -286,8 +281,8 @@
},
{
"pname": "System.Formats.Asn1",
"version": "8.0.1",
"hash": "sha256-may/Wg+esmm1N14kQTG4ESMBi+GQKPp0ZrrBo/o6OXM="
"version": "6.0.0",
"hash": "sha256-KaMHgIRBF7Nf3VwOo+gJS1DcD+41cJDPWFh+TDQ8ee8="
},
{
"pname": "System.IO.Abstractions",
@@ -304,6 +299,11 @@
"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",
@@ -314,6 +314,11 @@
"version": "1.6.0",
"hash": "sha256-JJfgaPav7UfEh4yRAQdGhLZF1brr0tUWPl6qmfNWq/E="
},
{
"pname": "System.Reflection.Metadata",
"version": "8.0.0",
"hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE="
},
{
"pname": "System.Runtime",
"version": "4.3.1",
@@ -361,13 +366,13 @@
},
{
"pname": "WoofWare.Myriad.Plugins",
"version": "4.0.14",
"hash": "sha256-WxgGXH4YVyFDEyNAxFIbB5MwKc3gF5ktYvzOeMdV418="
"version": "8.0.5",
"hash": "sha256-IfTT2GM9ktUW5BQoKQGFKK39BAKeziJJnrOIL7Vs19o="
},
{
"pname": "WoofWare.Myriad.Plugins.Attributes",
"version": "3.6.8",
"hash": "sha256-k8Oum0xbv90yHmYr770w7uskMcAwZfyFI5uHaes5Yxg="
"version": "3.6.12",
"hash": "sha256-90uiVtc5exCbkcdS8DgTmlEZZT8/AdrY0QuFzy+FHUo="
},
{
"pname": "WoofWare.PrattParser",
@@ -376,7 +381,7 @@
},
{
"pname": "WoofWare.Whippet.Fantomas",
"version": "0.3.2",
"hash": "sha256-dhzp/ASz1dlROJAfLYRKOCRlmjLBoS00KYeWEkSVtMA="
"version": "0.6.3",
"hash": "sha256-FkW/HtVp8/HE2k6d7yFpnJcnP3FNNe9kGlkoIWmNgDw="
}
]