diff --git a/Directory.Packages.props b/Directory.Packages.props
index cba68949..6eeced38 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,6 @@
-
diff --git a/src/FsCheck.Xunit/CheckExtensions.fs b/src/FsCheck.Xunit/CheckExtensions.fs
index e3c48b33..20632106 100644
--- a/src/FsCheck.Xunit/CheckExtensions.fs
+++ b/src/FsCheck.Xunit/CheckExtensions.fs
@@ -9,26 +9,26 @@ module private Helper =
let private runner (testOutputHelper: ITestOutputHelper) =
{ new IRunner with
member __.OnStartFixture t =
- Runner.onStartFixtureToString t |> testOutputHelper.WriteLine
+ Runner.onStartFixtureToString t |> Helpers.safeWriteLine testOutputHelper
member __.OnArguments (ntest,args, every) =
- every ntest args |> testOutputHelper.WriteLine
+ every ntest args |> Helpers.safeWriteLine testOutputHelper
member __.OnShrink(args, everyShrink) =
- everyShrink args |> testOutputHelper.WriteLine
+ everyShrink args |> Helpers.safeWriteLine testOutputHelper
member __.OnFinished(name,testResult) =
- Runner.onFinishedToString name testResult |> testOutputHelper.WriteLine
+ Runner.onFinishedToString name testResult |> Helpers.safeWriteLine testOutputHelper
}
let private throwingRunner (testOutputHelper: ITestOutputHelper) =
{ new IRunner with
member __.OnStartFixture t =
- testOutputHelper.WriteLine (Runner.onStartFixtureToString t)
+ Helpers.safeWriteLine testOutputHelper (Runner.onStartFixtureToString t)
member __.OnArguments (ntest,args, every) =
- testOutputHelper.WriteLine (every ntest args)
+ Helpers.safeWriteLine testOutputHelper (every ntest args)
member __.OnShrink(args, everyShrink) =
- testOutputHelper.WriteLine (everyShrink args)
+ Helpers.safeWriteLine testOutputHelper (everyShrink args)
member __.OnFinished(name,testResult) =
match testResult with
- | TestResult.Passed _ -> testOutputHelper.WriteLine (Runner.onFinishedToString name testResult)
+ | TestResult.Passed _ -> Helpers.safeWriteLine testOutputHelper (Runner.onFinishedToString name testResult)
| _ -> failwithf "%s" (Runner.onFinishedToString name testResult)
}
diff --git a/src/FsCheck.Xunit/FsCheck.Xunit.fsproj b/src/FsCheck.Xunit/FsCheck.Xunit.fsproj
index 9f41ecf0..d3fe51a7 100644
--- a/src/FsCheck.Xunit/FsCheck.Xunit.fsproj
+++ b/src/FsCheck.Xunit/FsCheck.Xunit.fsproj
@@ -14,6 +14,7 @@
+
diff --git a/src/FsCheck.Xunit/Helpers.fs b/src/FsCheck.Xunit/Helpers.fs
new file mode 100644
index 00000000..6bf444da
--- /dev/null
+++ b/src/FsCheck.Xunit/Helpers.fs
@@ -0,0 +1,19 @@
+namespace FsCheck.Xunit
+
+open System
+open Xunit.Abstractions
+
+module internal Helpers =
+ ///
+ /// Safely writes to a TestOutputHelper, handling cases where the test may have completed
+ /// and the helper is no longer active. This prevents InvalidOperationException when
+ /// closures that capture the TestOutputHelper are called after the test lifetime ends.
+ ///
+ let safeWriteLine (output: ITestOutputHelper) (message: string) =
+ try
+ output.WriteLine(message)
+ with
+ | :? InvalidOperationException ->
+ // Test has completed, TestOutputHelper is no longer active
+ // Silently ignore as this is expected when closures outlive test lifetime
+ ()
diff --git a/src/FsCheck.Xunit/PropertyAttribute.fs b/src/FsCheck.Xunit/PropertyAttribute.fs
index 9bf94049..19d141cf 100644
--- a/src/FsCheck.Xunit/PropertyAttribute.fs
+++ b/src/FsCheck.Xunit/PropertyAttribute.fs
@@ -100,13 +100,13 @@ module internal PropertyConfig =
.WithRunner(XunitRunner())
.WithEvery(
if propertyConfig.Verbose |> Option.exists id then
- fun n args -> output.WriteLine (Config.Verbose.Every n args); ""
+ fun n args -> Helpers.safeWriteLine output (Config.Verbose.Every n args); ""
else
Config.Quick.Every
)
.WithEveryShrink(
if propertyConfig.Verbose |> Option.exists id then
- fun args -> output.WriteLine (Config.Verbose.EveryShrink args); ""
+ fun args -> Helpers.safeWriteLine output (Config.Verbose.EveryShrink args); ""
else
Config.Quick.EveryShrink
)
diff --git a/src/FsCheck.Xunit/Runner.fs b/src/FsCheck.Xunit/Runner.fs
index ab682dc3..1f9c6f50 100644
--- a/src/FsCheck.Xunit/Runner.fs
+++ b/src/FsCheck.Xunit/Runner.fs
@@ -1,6 +1,7 @@
namespace FsCheck.Xunit
open FsCheck
+open System
/// A runner for FsCheck (i.e. that you can use as Config.Runner) which outputs
/// to Xunit's given ITestOutputHelper.
@@ -8,13 +9,13 @@ open FsCheck
type TestOutputRunner(output: Xunit.Abstractions.ITestOutputHelper) =
interface IRunner with
member _.OnStartFixture t =
- output.WriteLine (Runner.onStartFixtureToString t)
+ Helpers.safeWriteLine output (Runner.onStartFixtureToString t)
member _.OnArguments (ntest, args, every) =
- output.WriteLine (every ntest args)
+ Helpers.safeWriteLine output (every ntest args)
member _.OnShrink(args, everyShrink) =
- output.WriteLine (everyShrink args)
+ Helpers.safeWriteLine output (everyShrink args)
member _.OnFinished(name,testResult) =
let resultText = Runner.onFinishedToString name testResult
match testResult with
- | TestResult.Passed _ -> resultText |> output.WriteLine
+ | TestResult.Passed _ -> resultText |> Helpers.safeWriteLine output
| _ -> failwithf "%s" resultText
\ No newline at end of file
diff --git a/tests/FsCheck.Test/Fscheck.XUnit/PropertyAttributeTests.fs b/tests/FsCheck.Test/Fscheck.XUnit/PropertyAttributeTests.fs
index ca998d2f..50dca47d 100644
--- a/tests/FsCheck.Test/Fscheck.XUnit/PropertyAttributeTests.fs
+++ b/tests/FsCheck.Test/Fscheck.XUnit/PropertyAttributeTests.fs
@@ -91,3 +91,36 @@ module ``when type implements IAsyncLifetime`` =
[]
member this.``then InitializeAsync() is invoked``() =
executed = true
+
+/// Reproduction test for GitHub issue: Lifetime problem with Xunit: InvalidOperationException: There is no currently active test.
+/// This test class verifies that mixing Property and Fact tests with ITestOutputHelper doesn't cause lifetime issues.
+module ``when mixing Property and Fact tests with ITestOutputHelper`` =
+ open Xunit.Abstractions
+
+ type TestOutputHelperLifetimeTests(output: ITestOutputHelper) =
+
+ []
+ member _.``Property test with parameter writes to output`` (x: int) =
+ output.WriteLine($"Property test with parameter: {x}")
+ true
+
+ []
+ member _.``Fact test writes to output`` () =
+ output.WriteLine("Fact test")
+
+ []
+ member _.``Property test with string parameter writes to output`` (s: string) =
+ let str = if isNull s then "null" else s
+ output.WriteLine($"Property test with string: {str}")
+ true
+
+ []
+ member _.``Another fact test writes to output`` () =
+ output.WriteLine("Another fact test")
+
+ /// This test specifically exercises the Every and EveryShrink callbacks by enabling Verbose mode.
+ /// These callbacks capture the TestOutputHelper in closures, which was the root cause of the lifetime issue.
+ []
+ member _.``Verbose property test exercises Every and EveryShrink callbacks`` (x: int) (y: int) =
+ output.WriteLine($"Verbose mode test: x={x}, y={y}")
+ true