diff --git a/Directory.Packages.props b/Directory.Packages.props index 543f6d23..75a04975 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,5 +38,6 @@ <PackageVersion Include="xunit.extensibility.core" Version="[2.4.1, 3.0.0)" /> <PackageVersion Include="xunit.extensibility.execution" Version="[2.4.1, 3.0.0)" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" /> + <PackageVersion Include="xunit.v3.extensibility.core" Version="2.0.0" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/FsCheck.sln b/FsCheck.sln index 97d79f45..4b667080 100644 --- a/FsCheck.sln +++ b/FsCheck.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34616.47 MinimumVisualStudioVersion = 10.0.40219.1 @@ -59,6 +59,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharp.DocSnippets", "examp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsCheck.Test.CSharp", "tests\FsCheck.Test.CSharp\FsCheck.Test.CSharp.csproj", "{86955D54-4D54-4D4C-A7DC-F98F4CCFE498}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCheck.Xunit.v3", "src\FsCheck.Xunit.v3\FsCheck.Xunit.v3.fsproj", "{CADBF086-2B3D-42D5-8A66-F9ACC6119ED0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +115,10 @@ Global {86955D54-4D54-4D4C-A7DC-F98F4CCFE498}.Debug|Any CPU.Build.0 = Debug|Any CPU {86955D54-4D54-4D4C-A7DC-F98F4CCFE498}.Release|Any CPU.ActiveCfg = Release|Any CPU {86955D54-4D54-4D4C-A7DC-F98F4CCFE498}.Release|Any CPU.Build.0 = Release|Any CPU + {CADBF086-2B3D-42D5-8A66-F9ACC6119ED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CADBF086-2B3D-42D5-8A66-F9ACC6119ED0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CADBF086-2B3D-42D5-8A66-F9ACC6119ED0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CADBF086-2B3D-42D5-8A66-F9ACC6119ED0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/FsCheck.Xunit.v3/AssemblyInfo.fs b/src/FsCheck.Xunit.v3/AssemblyInfo.fs new file mode 100644 index 00000000..e658dfdc --- /dev/null +++ b/src/FsCheck.Xunit.v3/AssemblyInfo.fs @@ -0,0 +1,23 @@ + +// Auto-generated; edits may be deleted at any time +namespace System +open System.Reflection +open System.Runtime.CompilerServices + +[<assembly: AssemblyTitle("FsCheck.Xunit.v3")>] +[<assembly: AssemblyProduct("FsCheck.Xunit.v3")>] +[<assembly: AssemblyDescription("Integrates FsCheck with xUnit.v3.NET")>] +[<assembly: AssemblyVersion("3.1.0")>] +[<assembly: AssemblyFileVersion("3.1.0")>] +[<assembly: AssemblyKeyFile("../../FsCheckKey.snk")>] +[<assembly: InternalsVisibleTo("FsCheck.Test")>] +do () + +module internal AssemblyVersionInformation = + let [<Literal>] AssemblyTitle = "FsCheck.Xunit.v3" + let [<Literal>] AssemblyProduct = "FsCheck.Xunit.v3" + let [<Literal>] AssemblyDescription = "Integrates FsCheck with xUnit.v3.NET" + let [<Literal>] AssemblyVersion = "3.1.0" + let [<Literal>] AssemblyFileVersion = "3.1.0" + let [<Literal>] AssemblyKeyFile = "../../FsCheckKey.snk" + let [<Literal>] InternalsVisibleTo = "FsCheck.Test" diff --git a/src/FsCheck.Xunit.v3/CheckExtensions.fs b/src/FsCheck.Xunit.v3/CheckExtensions.fs new file mode 100644 index 00000000..811d46f2 --- /dev/null +++ b/src/FsCheck.Xunit.v3/CheckExtensions.fs @@ -0,0 +1,60 @@ +namespace FsCheck.Xunit + +open System +open System.Runtime.CompilerServices +open FsCheck +open global.Xunit + +module private Helper = + let private runner (testOutputHelper: ITestOutputHelper) = + { new IRunner with + member __.OnStartFixture t = + Runner.onStartFixtureToString t |> testOutputHelper.WriteLine + member __.OnArguments (ntest,args, every) = + every ntest args |> testOutputHelper.WriteLine + member __.OnShrink(args, everyShrink) = + everyShrink args |> testOutputHelper.WriteLine + member __.OnFinished(name,testResult) = + Runner.onFinishedToString name testResult |> testOutputHelper.WriteLine + } + + let private throwingRunner (testOutputHelper: ITestOutputHelper) = + { new IRunner with + member __.OnStartFixture t = + testOutputHelper.WriteLine (Runner.onStartFixtureToString t) + member __.OnArguments (ntest,args, every) = + testOutputHelper.WriteLine (every ntest args) + member __.OnShrink(args, everyShrink) = + testOutputHelper.WriteLine (everyShrink args) + member __.OnFinished(name,testResult) = + match testResult with + | FsCheck.TestResult.Passed _ -> testOutputHelper.WriteLine (Runner.onFinishedToString name testResult) + | _ -> failwithf "%s" (Runner.onFinishedToString name testResult) + } + + let writeToXunit (config:Config) (testOutputHelper: ITestOutputHelper) = + config.WithRunner(runner testOutputHelper) + + let writeToXunitThrow (config:Config) (testOutputHelper: ITestOutputHelper) = + config.WithRunner(throwingRunner testOutputHelper) + +[<AbstractClass;Sealed;Extension>] +type CheckExtensions = + [<Extension>] + static member QuickCheck(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunit Config.Quick testOutputHelper,property) + [<Extension>] + static member QuickCheck(property:Property, testName:string, testOutputHelper: ITestOutputHelper) = + Check.One(testName,Helper.writeToXunit Config.Quick testOutputHelper,property) + [<Extension>] + static member QuickCheckThrowOnFailure(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunitThrow Config.QuickThrowOnFailure testOutputHelper,property) + [<Extension>] + static member VerboseCheck(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunit Config.Verbose testOutputHelper, property) + [<Extension>] + static member VerboseCheck(property:Property, testName:string, testOutputHelper: ITestOutputHelper) = + Check.One(testName, Helper.writeToXunit Config.Verbose testOutputHelper, property) + [<Extension>] + static member VerboseCheckThrowOnFailure(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunitThrow Config.VerboseThrowOnFailure testOutputHelper,property) \ No newline at end of file diff --git a/src/FsCheck.Xunit.v3/FsCheck.Xunit.v3.fsproj b/src/FsCheck.Xunit.v3/FsCheck.Xunit.v3.fsproj new file mode 100644 index 00000000..f3f619b7 --- /dev/null +++ b/src/FsCheck.Xunit.v3/FsCheck.Xunit.v3.fsproj @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <AssemblyName>FsCheck.Xunit</AssemblyName> + <TargetFrameworks>netstandard2.0;net462</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <PackageDescription> + FsCheck.Xunit integrates FsCheck with xUnit.v3.NET by adding a PropertyAttribute that runs FsCheck tests, similar to xUnit.v3.NET's FactAttribute. + + All the options normally available in vanilla FsCheck via configuration can be controlled via the PropertyAttribute. + </PackageDescription> + <PackageTags>$(PackageTags);xunit;xunit.net</PackageTags> + </PropertyGroup> + <ItemGroup> + <Compile Include="AssemblyInfo.fs"/> + <Compile Include="Runner.fs"/> + <Compile Include="PropertyAttribute.fs"/> + <Compile Include="CheckExtensions.fs"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="FSharp.Core"/> + <PackageReference Include="xunit.v3.extensibility.core" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="../FsCheck/FsCheck.fsproj"/> + </ItemGroup> +</Project> diff --git a/src/FsCheck.Xunit.v3/PropertyAttribute.fs b/src/FsCheck.Xunit.v3/PropertyAttribute.fs new file mode 100644 index 00000000..c78f6188 --- /dev/null +++ b/src/FsCheck.Xunit.v3/PropertyAttribute.fs @@ -0,0 +1,314 @@ +namespace FsCheck.Xunit.v3 + +open System +open System.Reflection +open System.Threading.Tasks + +open Xunit +open Xunit.Sdk + +open FsCheck +open global.Xunit.v3 + +type PropertyFailedException = + inherit Exception + new (testResult:FsCheck.TestResult) = { + inherit Exception(sprintf "%s%s" Environment.NewLine (Runner.onFinishedToString "" testResult)) } + new (userMessage, innerException : exn) = { + inherit Exception(userMessage, innerException) } + +//can not be an anonymous type because of let mutable. +type XunitRunner() = + let mutable result = None + member __.Result = result.Value + interface IRunner with + override __.OnStartFixture _ = () + override __.OnArguments (ntest,args, every) = + every ntest args |> ignore + override __.OnShrink(args, everyShrink) = + everyShrink args |> ignore + override __.OnFinished(_ ,testResult) = + result <- Some testResult + +type internal PropertyConfig = + { MaxTest : Option<int> + MaxRejected : Option<int> + Replay : Option<string> + Parallelism : Option<int> + StartSize : Option<int> + EndSize : Option<int> + Verbose : Option<bool> + QuietOnSuccess : Option<bool> + Arbitrary : Type[] } + +[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] +module internal PropertyConfig = + let orElse y = function + | Some x -> Some x + | None -> y + + let orDefault x y = defaultArg y x + + let zero = + { MaxTest = None + MaxRejected = None + Replay = None + Parallelism = None + StartSize = None + EndSize = None + Verbose = None + QuietOnSuccess = None + Arbitrary = [||] } + + let combine extra original = + { MaxTest = extra.MaxTest |> orElse original.MaxTest + MaxRejected = extra.MaxRejected |> orElse original.MaxRejected + Replay = extra.Replay |> orElse original.Replay + Parallelism = extra.Parallelism |> orElse original.Parallelism + StartSize = extra.StartSize |> orElse original.StartSize + EndSize = extra.EndSize |> orElse original.EndSize + Verbose = extra.Verbose |> orElse original.Verbose + QuietOnSuccess = extra.QuietOnSuccess |> orElse original.QuietOnSuccess + Arbitrary = Array.append extra.Arbitrary original.Arbitrary } + + let parseReplay (str: string) = + //if someone sets this, we want it to throw if it fails + let split = str.Trim('(',')').Split([|","|], StringSplitOptions.RemoveEmptyEntries) + let seed = UInt64.Parse(split.[0]) + let gamma = UInt64.Parse(split.[1]) + let size = if split.Length = 3 then Some <| Convert.ToInt32(UInt32.Parse(split.[2])) else None + { Rnd = Rnd (seed,gamma); Size = size } + + let toConfig (output : TestOutputHelper) propertyConfig = + Config.Default + .WithReplay( + propertyConfig.Replay + |> Option.map parseReplay + |> orElse Config.Default.Replay + ) + .WithParallelRunConfig( + propertyConfig.Parallelism + |> Option.map (fun i -> { MaxDegreeOfParallelism = i }) + |> orElse Config.Default.ParallelRunConfig + ) + .WithMaxTest(propertyConfig.MaxTest |> orDefault Config.Default.MaxTest) + .WithMaxRejected(propertyConfig.MaxRejected |> orDefault Config.Default.MaxRejected) + .WithStartSize(propertyConfig.StartSize |> orDefault Config.Default.StartSize) + .WithEndSize(propertyConfig.EndSize |> orDefault Config.Default.EndSize) + .WithQuietOnSuccess(propertyConfig.QuietOnSuccess |> orDefault Config.Default.QuietOnSuccess) + .WithArbitrary(Seq.toList propertyConfig.Arbitrary) + .WithRunner(XunitRunner()) + .WithEvery( + if propertyConfig.Verbose |> Option.exists id then + fun n args -> output.WriteLine (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); "" + else + Config.Quick.EveryShrink + ) + +///Run this method as an FsCheck test. +[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property, AllowMultiple = false)>] +[<XunitTestCaseDiscoverer(typeof<PropertyDiscoverer>)>] +type public PropertyAttribute() = + inherit FactAttribute() + let mutable config = PropertyConfig.zero + let mutable replay = null + let mutable parallelism = -1 + let mutable maxTest = -1 + let mutable maxRejected = -1 + let mutable startSize = -1 + let mutable endSize = -1 + let mutable verbose = false + let mutable arbitrary = [||] + let mutable quietOnSuccess = false + + ///If set, the seed to use to start testing. Allows reproduction of previous runs. You can just paste + ///the tuple from the output window, e.g. 12344,12312 or (123,123). + ///Additionally, you can also specify a start size as the third parameter, e.g. 12344,12312,10 or (123,123,10). + member __.Replay with get() = replay and set(v) = replay <- v; config <- {config with Replay = if String.IsNullOrEmpty v then None else Some v} + ///If set, run tests in parallel. Useful for Task/async related work and heavy number crunching + ///Environment.ProcessorCount have been found to be useful default. + member __.Parallelism with get() = parallelism and set(v) = parallelism <- v; config <- {config with Parallelism = Some v} + ///The maximum number of tests that are run. + member __.MaxTest with get() = maxTest and set(v) = maxTest <- v; config <- {config with MaxTest = Some v} + ///The maximum number of tests where values are rejected, e.g. as the result of ==> + member __.MaxRejected with get() = maxRejected and set(v) = maxRejected <- v; config <- {config with MaxRejected = Some v} + ///The size to use for the first test. + member __.StartSize with get() = startSize and set(v) = startSize <- v; config <- {config with StartSize = Some v} + ///The size to use for the last test, when all the tests are passing. The size increases linearly between Start- and EndSize. + member __.EndSize with get() = endSize and set(v) = endSize <- v; config <- {config with EndSize = Some v} + ///Output all generated arguments. + member __.Verbose with get() = verbose and set(v) = verbose <- v; config <- {config with Verbose = Some v} + ///The Arbitrary instances to use for this test method. The Arbitrary instances + ///are merged in back to front order i.e. instances for the same generated type + ///at the front of the array will override those at the back. + member __.Arbitrary with get() = arbitrary and set(v) = arbitrary <- v; config <- {config with Arbitrary = v} + ///If set, suppresses the output from the test if the test is successful. This can be useful when running tests + ///with TestDriven.net, because TestDriven.net pops up the Output window in Visual Studio if a test fails; thus, + ///when conditioned to that behaviour, it's always a bit jarring to receive output from passing tests. + ///The default is false, which means that FsCheck will also output test results on success, but if set to true, + ///FsCheck will suppress output in the case of a passing test. This setting doesn't affect the behaviour in case of + ///test failures. + member __.QuietOnSuccess with get() = quietOnSuccess and set(v) = quietOnSuccess <- v; config <- {config with QuietOnSuccess = Some v} + + member internal __.Config = config +and + ///Set common configuration for all properties within this class or module + [<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Assembly, AllowMultiple = false)>] + public PropertiesAttribute() = inherit PropertyAttribute() +and +/// The xUnit2 test runner for the PropertyAttribute that executes the test via FsCheck + PropertyTestCase(diagnosticMessageSink:IMessageSink, defaultMethodDisplay:TestMethodDisplay, defaultMethodDisplayOptions:TestMethodDisplayOptions, testMethod:ITestMethod, ?testMethodArguments:obj []) = + inherit XunitTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, (match testMethodArguments with | None -> null | Some v -> v)) + + let combineAttributes (configs: (PropertyConfig option) list) = + configs + |> List.choose id + |> List.reduce(fun higherLevelAttribute lowerLevelAttribute -> + PropertyConfig.combine lowerLevelAttribute higherLevelAttribute) + + new() = new PropertyTestCase(null, TestMethodDisplay.ClassAndMethod, TestMethodDisplayOptions.None, null) + + member this.Init(output:TestOutputHelper) = + let getPropertiesOnDeclaringClasses (testClass: ITestClass) = + [ let mutable current: Type = testClass.Class.ToRuntimeType() + while not (isNull current) do + yield current.GetTypeInfo().GetCustomAttributes<PropertiesAttribute>() + |> Seq.tryHead + |> Option.map (fun attr -> attr.Config) + current <- current.DeclaringType] + |> List.rev + + let getConfig (attr: IAttributeInfo) = + attr.GetNamedArgument<PropertyConfig> "Config" + + let config = combineAttributes [ + yield this.TestMethod.TestClass.Class.Assembly.GetCustomAttributes(typeof<PropertiesAttribute>) |> Seq.tryHead |> Option.map getConfig + yield! getPropertiesOnDeclaringClasses this.TestMethod.TestClass + yield this.TestMethod.Method.GetCustomAttributes(typeof<PropertyAttribute>) |> Seq.head |> getConfig |> Some] + + { config with Arbitrary = config.Arbitrary } + |> PropertyConfig.toConfig output + + override this.RunAsync(diagnosticMessageSink:IMessageSink, messageBus:IMessageBus, constructorArguments:obj [], aggregator:ExceptionAggregator, cancellationTokenSource:Threading.CancellationTokenSource) = + let test = XunitTest(this, this.DisplayName) + let summary = RunSummary(Total = 1); + + // 1. We always need an initialized TestOutputHelper so we can write output. + // 2. xunit supports test classes that have a constructor that takes a TestOutputHelper, which we need to pass along. + // xunit has two different ways of passing us the TestOutputHelper. + // pre-2.9.0: as a TestOutputHelper constructor argument + // post-2.9.0: as a Func<TestOutputHelper> constructor argument https://github.com/xunit/xunit/issues/2996#issuecomment-2271764192 + // The below code handles both cases. + let mutable outputHelper = TestOutputHelper() + for i in 0..constructorArguments.Length-1 do + match constructorArguments[i] with + | :? TestOutputHelper as foundHelper -> + outputHelper <- foundHelper + | :? Func<TestOutputHelper> as foundHelperFunc -> + outputHelper <- foundHelperFunc.Invoke() + constructorArguments.[i] <- outputHelper + | _ -> () + outputHelper.Initialize(messageBus, test) + + let dispose testClass = + match testClass with + | None -> () + | Some obj -> + match box obj with + | :? IDisposable as d -> d.Dispose() + | _ -> () + + let testExec() = + let config = this.Init(outputHelper) + let timer = ExecutionTimer() + let result = + try + let xunitRunner = if config.Runner :? XunitRunner then (config.Runner :?> XunitRunner) else XunitRunner() + let runMethod = this.TestMethod.Method.ToRuntimeMethod() + let target = + let testClass = this.TestMethod.TestClass.Class.ToRuntimeType() + if (not (isNull this.TestMethod.TestClass)) && not this.TestMethod.Method.IsStatic then + + let testInstance = test.CreateTestClass(testClass, constructorArguments, messageBus, timer, cancellationTokenSource) + + match testInstance with + | :? IAsyncLifetime as asyncLifetime -> asyncLifetime.InitializeAsync() + | _ -> Task.CompletedTask + |> Async.AwaitTask + |> Async.StartAsTask + |> Task.WaitAll + + Some testInstance + else None + + timer.Aggregate(fun () -> Check.Method(config, runMethod, ?target=target)) + + dispose target + + match xunitRunner.Result with + | TestResult.Passed _ -> + let output = Runner.onFinishedToString "" xunitRunner.Result + outputHelper.WriteLine(output) + TestPassed(test, timer.Total, outputHelper.Output) :> TestResultMessage + | TestResult.Exhausted _ -> + summary.Failed <- summary.Failed + 1 + upcast TestFailed(test, timer.Total, outputHelper.Output, PropertyFailedException(xunitRunner.Result)) + | TestResult.Failed (testdata, originalArgs, shrunkArgs, Outcome.Failed e, originalSeed, lastSeed, lastSize) -> + summary.Failed <- summary.Failed + 1 + let message = sprintf "%s%s" Environment.NewLine (Runner.onFailureToString "" testdata originalArgs shrunkArgs originalSeed lastSeed lastSize) + upcast TestFailed(test, timer.Total, outputHelper.Output, PropertyFailedException(message, e)) + | TestResult.Failed _ -> + summary.Failed <- summary.Failed + 1 + upcast TestFailed(test, timer.Total, outputHelper.Output, PropertyFailedException(xunitRunner.Result)) + with + | ex -> + summary.Failed <- summary.Failed + 1 + outputHelper.WriteLine("Exception during test") + upcast TestFailed(test, timer.Total, outputHelper.Output, ex) + + + outputHelper.Uninitialize() + + messageBus.QueueMessage(result) |> ignore + summary.Time <- summary.Time + result.ExecutionTime + if not (messageBus.QueueMessage(TestFinished(test, summary.Time, result.Output))) then + cancellationTokenSource.Cancel() |> ignore + if not (messageBus.QueueMessage(TestCaseFinished(this, summary.Time, summary.Total, summary.Failed, summary.Skipped))) then + cancellationTokenSource.Cancel() |> ignore + + summary + + if not (messageBus.QueueMessage(TestCaseStarting(this))) then + cancellationTokenSource.Cancel() |> ignore + + if not (messageBus.QueueMessage(TestStarting(test))) then + cancellationTokenSource.Cancel() |> ignore + + if not(String.IsNullOrEmpty(this.SkipReason)) then + summary.Skipped <- summary.Skipped + 1 + if not(messageBus.QueueMessage(TestSkipped(test, this.SkipReason))) then + cancellationTokenSource.Cancel() |> ignore + if not(messageBus.QueueMessage(TestCaseFinished(this, decimal 1, 0, 0, 1))) then + cancellationTokenSource.Cancel() |> ignore + Task.FromResult(summary) + else + Task.Run(testExec) +and +/// xUnit2 test case discoverer to link the method with the PropertyAttribute to the PropertyTestCase +/// so the test can be run via FsCheck. + PropertyDiscoverer(messageSink:IMessageSink) = + + new () = PropertyDiscoverer(null) + + member __.MessageSink = messageSink + + interface IXunitTestCaseDiscoverer with + override this.Discover(discoveryOptions:ITestFrameworkDiscoveryOptions, testMethod:ITestMethod, _:IAttributeInfo)= + let ptc = new PropertyTestCase(this.MessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) + Seq.singleton (ptc :> IXunitTestCase) diff --git a/src/FsCheck.Xunit.v3/Runner.fs b/src/FsCheck.Xunit.v3/Runner.fs new file mode 100644 index 00000000..a706017e --- /dev/null +++ b/src/FsCheck.Xunit.v3/Runner.fs @@ -0,0 +1,21 @@ +namespace FsCheck.Xunit.v3 + +open FsCheck +open global.Xunit.v3 + +/// 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: TestOutputHelper) = + interface IRunner with + member _.OnStartFixture t = + output.WriteLine (Runner.onStartFixtureToString t) + member _.OnArguments (ntest, args, every) = + output.WriteLine (every ntest args) + member _.OnShrink(args, everyShrink) = + output.WriteLine (everyShrink args) + member _.OnFinished(name,testResult) = + let resultText = Runner.onFinishedToString name testResult + match testResult with + | TestResult.Passed _ -> resultText |> output.WriteLine + | _ -> failwithf "%s" resultText \ No newline at end of file