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