Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="5.0.2" />
<PackageVersion Include="Microsoft.NETCore.Targets" Version="5.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="16.11" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.7.0" />
<PackageVersion Include="MSTest.TestFramework" Version="3.7.0" />
Expand Down
26 changes: 18 additions & 8 deletions src/FsCheck.Xunit/CheckExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,39 @@ open FsCheck
open Xunit.Abstractions

module private Helper =
// Helper to safely write to output, handling case where test may have completed
let private safeWriteLine (testOutputHelper: ITestOutputHelper) (message: string) =
try
testOutputHelper.WriteLine(message)
with
| :? InvalidOperationException ->
// Test has completed, TestOutputHelper is no longer active
// Silently ignore as this is expected when runners outlive test lifetime
()

let private runner (testOutputHelper: ITestOutputHelper) =
{ new IRunner with
member __.OnStartFixture t =
Runner.onStartFixtureToString t |> testOutputHelper.WriteLine
Runner.onStartFixtureToString t |> safeWriteLine testOutputHelper
member __.OnArguments (ntest,args, every) =
every ntest args |> testOutputHelper.WriteLine
every ntest args |> safeWriteLine testOutputHelper
member __.OnShrink(args, everyShrink) =
everyShrink args |> testOutputHelper.WriteLine
everyShrink args |> safeWriteLine testOutputHelper
member __.OnFinished(name,testResult) =
Runner.onFinishedToString name testResult |> testOutputHelper.WriteLine
Runner.onFinishedToString name testResult |> safeWriteLine testOutputHelper
}

let private throwingRunner (testOutputHelper: ITestOutputHelper) =
{ new IRunner with
member __.OnStartFixture t =
testOutputHelper.WriteLine (Runner.onStartFixtureToString t)
safeWriteLine testOutputHelper (Runner.onStartFixtureToString t)
member __.OnArguments (ntest,args, every) =
testOutputHelper.WriteLine (every ntest args)
safeWriteLine testOutputHelper (every ntest args)
member __.OnShrink(args, everyShrink) =
testOutputHelper.WriteLine (everyShrink args)
safeWriteLine testOutputHelper (everyShrink args)
member __.OnFinished(name,testResult) =
match testResult with
| TestResult.Passed _ -> testOutputHelper.WriteLine (Runner.onFinishedToString name testResult)
| TestResult.Passed _ -> safeWriteLine testOutputHelper (Runner.onFinishedToString name testResult)
| _ -> failwithf "%s" (Runner.onFinishedToString name testResult)
}

Expand Down
14 changes: 12 additions & 2 deletions src/FsCheck.Xunit/PropertyAttribute.fs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ module internal PropertyConfig =
{ Rnd = Rnd (seed,gamma); Size = size }

let toConfig (output : TestOutputHelper) propertyConfig =
// Helper to safely write to output, handling case where test may have completed
let safeWriteLine (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
()

Config.Default
.WithReplay(
propertyConfig.Replay
Expand All @@ -100,13 +110,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 -> safeWriteLine (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 -> safeWriteLine (Config.Verbose.EveryShrink args); ""
else
Config.Quick.EveryShrink
)
Expand Down
19 changes: 15 additions & 4 deletions src/FsCheck.Xunit/Runner.fs
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
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.
/// For example, { Config.QuickThrowOnFailure with Runner = TestOutputRunner(output) }
type TestOutputRunner(output: Xunit.Abstractions.ITestOutputHelper) =
// Helper to safely write to output, handling case where test may have completed
let safeWriteLine (message: string) =
try
output.WriteLine(message)
with
| :? InvalidOperationException ->
// Test has completed, TestOutputHelper is no longer active
// Silently ignore as this is expected when runner outlives test lifetime
()

interface IRunner with
member _.OnStartFixture t =
output.WriteLine (Runner.onStartFixtureToString t)
safeWriteLine (Runner.onStartFixtureToString t)
member _.OnArguments (ntest, args, every) =
output.WriteLine (every ntest args)
safeWriteLine (every ntest args)
member _.OnShrink(args, everyShrink) =
output.WriteLine (everyShrink args)
safeWriteLine (everyShrink args)
member _.OnFinished(name,testResult) =
let resultText = Runner.onFinishedToString name testResult
match testResult with
| TestResult.Passed _ -> resultText |> output.WriteLine
| TestResult.Passed _ -> resultText |> safeWriteLine
| _ -> failwithf "%s" resultText
13 changes: 13 additions & 0 deletions tests/FsCheck.Test.CSharp/FsCheck.Test.CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,17 @@
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../../src/FsCheck.Xunit/FsCheck.Xunit.fsproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="xunit.core" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

</Project>
54 changes: 54 additions & 0 deletions tests/FsCheck.Test.CSharp/TestOutputHelperLifetimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Xunit;
using Xunit.Abstractions;

namespace FsCheck.Test.CSharp
{
/// <summary>
/// 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.
/// </summary>
public class TestOutputHelperLifetimeTests
{
private readonly ITestOutputHelper _testOutputHelper;

public TestOutputHelperLifetimeTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}

[FsCheck.Xunit.Property]
public void Test1(int x)
{
_testOutputHelper.WriteLine($"{nameof(Test1)}: {x}");
}

[Fact]
public void Test2()
{
_testOutputHelper.WriteLine($"{nameof(Test2)}");
}

[FsCheck.Xunit.Property]
public void Test3(string s)
{
_testOutputHelper.WriteLine($"{nameof(Test3)}: {s ?? "null"}");
}

[Fact]
public void Test4()
{
_testOutputHelper.WriteLine($"{nameof(Test4)}");
}

/// <summary>
/// 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.
/// </summary>
[FsCheck.Xunit.Property(Verbose = true, MaxTest = 5)]
public bool Test5_VerboseMode(int x, int y)
{
_testOutputHelper.WriteLine($"{nameof(Test5_VerboseMode)}: x={x}, y={y}");
return true; // Always pass
}
}
}