From 87f450e81f75febaa7022f9e084c89bb1ee3fdd8 Mon Sep 17 00:00:00 2001 From: Johannes Sandgren Date: Wed, 28 May 2025 17:14:36 +0200 Subject: [PATCH 01/32] Implement xUnit testcases for xUnit.v3 Add simple tests --- Directory.Packages.props | 2 + FsCheck.sln | 12 + src/FsCheck.Xunit.v3/AssemblyInfo.fs | 23 ++ src/FsCheck.Xunit.v3/CheckExtensions.fs | 62 ++++ src/FsCheck.Xunit.v3/FsCheck.Xunit.v3.fsproj | 28 ++ src/FsCheck.Xunit.v3/PropertyAttribute.fs | 264 ++++++++++++++++++ src/FsCheck.Xunit.v3/Runner.fs | 20 ++ src/FsCheck.Xunit.v3/paket.template | 32 +++ tests/FsCheck.Test.v3/App.config | 6 + tests/FsCheck.Test.v3/FsCheck.Test.v3.fsproj | 28 ++ .../Fscheck.XUnit/PropertyAttributeTests.fs | 94 +++++++ 11 files changed, 571 insertions(+) create mode 100644 src/FsCheck.Xunit.v3/AssemblyInfo.fs create mode 100644 src/FsCheck.Xunit.v3/CheckExtensions.fs create mode 100644 src/FsCheck.Xunit.v3/FsCheck.Xunit.v3.fsproj create mode 100644 src/FsCheck.Xunit.v3/PropertyAttribute.fs create mode 100644 src/FsCheck.Xunit.v3/Runner.fs create mode 100644 src/FsCheck.Xunit.v3/paket.template create mode 100644 tests/FsCheck.Test.v3/App.config create mode 100644 tests/FsCheck.Test.v3/FsCheck.Test.v3.fsproj create mode 100644 tests/FsCheck.Test.v3/Fscheck.XUnit/PropertyAttributeTests.fs diff --git a/Directory.Packages.props b/Directory.Packages.props index 543f6d23..3f2903ed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,6 +37,8 @@ + + \ No newline at end of file diff --git a/FsCheck.sln b/FsCheck.sln index 97d79f45..90eedc0f 100644 --- a/FsCheck.sln +++ b/FsCheck.sln @@ -59,6 +59,10 @@ 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.Test.v3", "tests\FsCheck.Test.v3\FsCheck.Test.v3.fsproj", "{0CE5ED25-7AE1-C88B-2C70-D4C110D59BDE}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCheck.Xunit.v3", "src\FsCheck.Xunit.v3\FsCheck.Xunit.v3.fsproj", "{097DCA94-FF3D-BB6D-69FC-405B20621DA8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +117,14 @@ 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 + {0CE5ED25-7AE1-C88B-2C70-D4C110D59BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CE5ED25-7AE1-C88B-2C70-D4C110D59BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CE5ED25-7AE1-C88B-2C70-D4C110D59BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CE5ED25-7AE1-C88B-2C70-D4C110D59BDE}.Release|Any CPU.Build.0 = Release|Any CPU + {097DCA94-FF3D-BB6D-69FC-405B20621DA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {097DCA94-FF3D-BB6D-69FC-405B20621DA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {097DCA94-FF3D-BB6D-69FC-405B20621DA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {097DCA94-FF3D-BB6D-69FC-405B20621DA8}.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..12b8659d --- /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 + +[] +[] +[] +[] +[] +[] +[] +do () + +module internal AssemblyVersionInformation = + let [] AssemblyTitle = "FsCheck.Xunit.v3" + let [] AssemblyProduct = "FsCheck.Xunit.v3" + let [] AssemblyDescription = "Integrates FsCheck with xUnit.v3" + let [] AssemblyVersion = "3.2.0" + let [] AssemblyFileVersion = "3.2.0" + let [] AssemblyKeyFile = "../../FsCheckKey.snk" + let [] 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..ed8304a9 --- /dev/null +++ b/src/FsCheck.Xunit.v3/CheckExtensions.fs @@ -0,0 +1,62 @@ +namespace FsCheck.Xunit + +open System +open System.Runtime.CompilerServices +open Xunit +open Xunit.v3 + +open FsCheck + +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 + | 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) + +[] +type CheckExtensions = + [] + static member QuickCheck(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunit Config.Quick testOutputHelper,property) + [] + static member QuickCheck(property:Property, testName:string, testOutputHelper: ITestOutputHelper) = + Check.One(testName,Helper.writeToXunit Config.Quick testOutputHelper,property) + [] + static member QuickCheckThrowOnFailure(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunitThrow Config.QuickThrowOnFailure testOutputHelper,property) + [] + static member VerboseCheck(property:Property, testOutputHelper: ITestOutputHelper) = + Check.One(Helper.writeToXunit Config.Verbose testOutputHelper, property) + [] + static member VerboseCheck(property:Property, testName:string, testOutputHelper: ITestOutputHelper) = + Check.One(testName, Helper.writeToXunit Config.Verbose testOutputHelper, property) + [] + 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..d12abff3 --- /dev/null +++ b/src/FsCheck.Xunit.v3/FsCheck.Xunit.v3.fsproj @@ -0,0 +1,28 @@ + + + + FsCheck.Xunit.v3 + netstandard2.0 + true + false + + FsCheck.Xunit.v3 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. + + $(PackageTags);xunit.v3 + + + + + + + + + + + + + + + diff --git a/src/FsCheck.Xunit.v3/PropertyAttribute.fs b/src/FsCheck.Xunit.v3/PropertyAttribute.fs new file mode 100644 index 00000000..684c32e8 --- /dev/null +++ b/src/FsCheck.Xunit.v3/PropertyAttribute.fs @@ -0,0 +1,264 @@ +namespace rec FsCheck.Xunit + +open System +open System.Diagnostics +open System.Reflection +open System.Threading +open System.Threading.Tasks +open System.Collections.Generic + +open Xunit +open Xunit.Sdk +open Xunit.v3 + +open FsCheck + +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 + MaxRejected : Option + Replay : Option + Parallelism : Option + StartSize : Option + EndSize : Option + Verbose : Option + QuietOnSuccess : Option + Arbitrary : Type[] } + +[] +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. +[] +[)>] +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 + +///Set common configuration for all properties within this class or module +[] +type public PropertiesAttribute() = inherit PropertyAttribute() + +/// The xUnit.v3 test runner for the PropertyAttribute that executes the test via FsCheck +type PropertyTestCase = + inherit XunitTestCase + + [] + new() = { inherit XunitTestCase() } + + new(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason) = + { inherit XunitTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason) } + + static let combineAttributes (configs: (PropertyConfig option) list) = + configs + |> List.choose id + |> List.reduce(fun higherLevelAttribute lowerLevelAttribute -> + PropertyConfig.combine lowerLevelAttribute higherLevelAttribute) + + member this.Init(output:TestOutputHelper) = + let getPropertiesOnDeclaringClasses (testClass: IXunitTestClass) = + [ let mutable current: Type = testClass.Class + while not (isNull current) do + yield current.GetTypeInfo().GetCustomAttributes() + |> Seq.tryHead + |> Option.map (fun attr -> attr.Config) + current <- current.DeclaringType] + |> List.rev + + let getConfig (attr: PropertyAttribute) = + attr.Config + + let config = combineAttributes [ + yield this.TestMethod.TestClass.Class.Assembly.GetCustomAttributes() |> Seq.tryHead |> Option.map getConfig + yield! getPropertiesOnDeclaringClasses this.TestMethod.TestClass + yield this.TestMethod.Method.GetCustomAttributes() |> Seq.head |> getConfig |> Some] + + { config with Arbitrary = config.Arbitrary } + |> PropertyConfig.toConfig output + + member this.MakeInvoker() = + let runner = + { new XunitTestRunner() with + override _.InvokeTest(ctxt: XunitTestRunnerContext, instance) = + let target = instance |> Option.ofObj + let helper = TestOutputHelper() + helper.Initialize(ctxt.MessageBus, ctxt.Test) + use _cleanup = { new IDisposable with member _.Dispose() = helper.Uninitialize() } + let config = this.Init(helper) + let xunitRunner = if config.Runner :? XunitRunner then (config.Runner :?> XunitRunner) else XunitRunner() + + let sw = Stopwatch.StartNew() + Check.Method(config, ctxt.TestMethod, ?target=target) + let ts = sw.Elapsed + match xunitRunner.Result with + | TestResult.Passed _ -> + helper.WriteLine(Runner.onFinishedToString "" xunitRunner.Result) + | TestResult.Exhausted _ -> + raise (PropertyFailedException(xunitRunner.Result)) + | TestResult.Failed (testdata, originalArgs, shrunkArgs, Outcome.Failed e, originalSeed, lastSeed, lastSize) -> + let message = sprintf "%s%s" Environment.NewLine (Runner.onFailureToString "" testdata originalArgs shrunkArgs originalSeed lastSeed lastSize) + raise (PropertyFailedException(message, e)) + | TestResult.Failed _ -> + raise (PropertyFailedException(xunitRunner.Result)) + + ValueTask<_>(ts) + } + { new XunitTestCaseRunner() with + override _.RunTest(x, test) = + runner.Run(test, x.MessageBus, x.ConstructorArguments, x.ExplicitOption, x.Aggregator.Clone(), x.CancellationTokenSource, x.BeforeAfterTestAttributes) + } + + member this.TestExec(opts: ExplicitOption, buss: IMessageBus, ctorArgs: obj array, aggregator: ExceptionAggregator, cts: CancellationTokenSource) = async { + let invoker = this.MakeInvoker() + let! tests = aggregator.RunAsync(Func<_>(this.CreateTests), []).AsTask() |> Async.AwaitTask + return! invoker.Run(this, tests, buss, aggregator.Clone(), cts, this.TestCaseDisplayName, this.SkipReason, opts, ctorArgs).AsTask() |> Async.AwaitTask + } + + interface ISelfExecutingXunitTestCase with + member this.Run (opts: ExplicitOption, messageBus: IMessageBus, ctorArgs: obj array, aggregator: ExceptionAggregator, cts: CancellationTokenSource) = + ValueTask(Async.StartImmediateAsTask(this.TestExec(opts, messageBus, ctorArgs, aggregator, cts))) + + +/// xUnit.v3 test case discoverer to link the method with the PropertyAttribute to the PropertyTestCase +/// so the test can be run via FsCheck. +type PropertyDiscoverer(messageSink:IMessageSink) = + + new () = PropertyDiscoverer(null) + + member _.MessageSink = messageSink + + interface IXunitTestCaseDiscoverer with + override this.Discover(discoveryOptions: ITestFrameworkDiscoveryOptions, testMethod: IXunitTestMethod, attr: IFactAttribute)= + let result = ResizeArray() + let struct (testCaseDisplayName, explicit, skipExceptions, skipReason, _, _, _, _, uniqueID, testMethod) = + TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, attr) + + result.Add(PropertyTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipExceptions, skipReason)) + ValueTask<_>(result :> IReadOnlyCollection) + diff --git a/src/FsCheck.Xunit.v3/Runner.fs b/src/FsCheck.Xunit.v3/Runner.fs new file mode 100644 index 00000000..d2913ec2 --- /dev/null +++ b/src/FsCheck.Xunit.v3/Runner.fs @@ -0,0 +1,20 @@ +namespace FsCheck.Xunit + +open Xunit +open FsCheck +/// 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: ITestOutputHelper) = + 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 diff --git a/src/FsCheck.Xunit.v3/paket.template b/src/FsCheck.Xunit.v3/paket.template new file mode 100644 index 00000000..740e4d8e --- /dev/null +++ b/src/FsCheck.Xunit.v3/paket.template @@ -0,0 +1,32 @@ +type file +id + FsCheck.Xunit +authors + Kurt Schelfthout and contributors +owners + Kurt Schelfthout and contributors +projectUrl + https://github.com/fsharp/FsCheck +iconUrl + https://raw.githubusercontent.com/fscheck/FsCheck/master/docs/files/img/logo.png +licenseUrl + https://github.com/fsharp/FsCheck/blob/master/License.txt +requireLicenseAcceptance + false +copyright + Copyright 2017 +tags + test testing random fscheck quickcheck xunit xunit.net +summary + Integrates FsCheck with xUnit.NET +description + FsCheck.Xunit integrates FsCheck with xUnit.NET by adding a PropertyAttribute that runs FsCheck tests, similar to xUnit.NET's FactAttribute. + + All the options normally available in vanilla FsCheck via configuration can be controlled via the PropertyAttribute. +dependencies + FsCheck = CURRENTVERSION + xunit.extensibility.execution ~> 2.4 +files + bin/Release/netstandard2.0/FsCheck.Xunit.dll ==> lib/netstandard2.0 + bin/Release/netstandard2.0/FsCheck.Xunit.xml ==> lib/netstandard2.0 + diff --git a/tests/FsCheck.Test.v3/App.config b/tests/FsCheck.Test.v3/App.config new file mode 100644 index 00000000..7f4e1349 --- /dev/null +++ b/tests/FsCheck.Test.v3/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/FsCheck.Test.v3/FsCheck.Test.v3.fsproj b/tests/FsCheck.Test.v3/FsCheck.Test.v3.fsproj new file mode 100644 index 00000000..846ab5a1 --- /dev/null +++ b/tests/FsCheck.Test.v3/FsCheck.Test.v3.fsproj @@ -0,0 +1,28 @@ + + + + FsCheck.Test.v3 + net8.0 + true + false + true + true + + + DEBUG + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/FsCheck.Test.v3/Fscheck.XUnit/PropertyAttributeTests.fs b/tests/FsCheck.Test.v3/Fscheck.XUnit/PropertyAttributeTests.fs new file mode 100644 index 00000000..3eecb53d --- /dev/null +++ b/tests/FsCheck.Test.v3/Fscheck.XUnit/PropertyAttributeTests.fs @@ -0,0 +1,94 @@ +namespace Fscheck.Test.FsCheck.XUnit.PropertyAttribute + +open System.Threading.Tasks +open FsCheck.FSharp +open FsCheck.Xunit +open Xunit + +type AttributeLevel = +| Assembly +| ClassOrModule +| NestedClassOrModule +| MethodOrProperty + +type AttributeLevel_Assembly() = + static member Generator = + Assembly + |> Gen.constant + |> Arb.fromGen + +type AttributeLevel_ClassOrModule() = + static member Generator = + ClassOrModule + |> Gen.constant + |> Arb.fromGen + +type AttributeLevel_MethodOrProperty() = + static member Generator = + MethodOrProperty + |> Gen.constant + |> Arb.fromGen + +type AttributeLevel_NestedClassOrModule() = + static member Generator = + NestedClassOrModule + |> Gen.constant + |> Arb.fromGen + +[ |])>] +do() + +module ``when module does not have properties attribute``= + [] + let ``then the assembly attribute should be used`` = function + | Assembly -> true + | _ -> false + + [|])>] + let ``then the property attribute takes precient`` = function + | MethodOrProperty -> true + | _ -> false + +[|])>] +module ``when module has properties attribute`` = + + [] + let ``then the module's property takes precident`` = function + | ClassOrModule -> true + | _ -> false + + [|])>] + let ``then the property attribute takes precient`` = function + | MethodOrProperty -> true + | _ -> false + + [|])>] + module ``and there is and nested module`` = + [] + let ``then the nested module's property takes precident`` = function + | NestedClassOrModule -> true + | _ -> false + + +module ``when type implements IAsyncLifetime`` = + type Issue657() = + + let mutable executed = false; + + interface IAsyncLifetime with + member _.InitializeAsync() = + + async { + do! Async.Sleep 300 + executed <- true + return () + } + |> Async.StartAsTask + :> Task + |> ValueTask + + member _.DisposeAsync() = ValueTask() + + [] + member this.``then InitializeAsync() is invoked``() = + executed = true From 5609f60e3e8e3f0be40b116259f81ca8d59d4308 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Tue, 3 Jun 2025 22:06:52 +0100 Subject: [PATCH 02/32] New release. --- FsCheck Release Notes.md | 4 ++++ build.fsx | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/FsCheck Release Notes.md b/FsCheck Release Notes.md index a258d3f5..e084c218 100644 --- a/FsCheck Release Notes.md +++ b/FsCheck Release Notes.md @@ -1,3 +1,7 @@ +### 3.3.0 - 3 June 2025 + +* Added FsCheck.Xunit.v3. (by JohSand) + ### 3.2.0 - 10 April 2025 * Support C# struct record generation. (by Brian Rourke Boll) diff --git a/build.fsx b/build.fsx index 4176eccf..97cfcf02 100644 --- a/build.fsx +++ b/build.fsx @@ -208,7 +208,10 @@ let packages = Summary = "Integrates FsCheck with NUnit" } { Name = "FsCheck.Xunit" - Summary = "Integrates FsCheck with xUnit.NET" + Summary = "Integrates FsCheck with xUnit.NET v2" + } + { Name = "FsCheck.Xunit.v3" + Summary = "Integrates FsCheck with xUnit.NET v3" } ] @@ -315,7 +318,7 @@ let packNuGet (_ : HaveTested) : HavePacked = // via `dotnet pack`. Without this next bit, FsCheck.Xunit vA.B.C depends on >= FsCheck vA.B.C, not // on FsCheck exactly at vA.B.C. - for package in ["FsCheck.Xunit" ; "FsCheck.NUnit"] do + for package in ["FsCheck.Xunit" ; "FsCheck.NUnit" ; "FsCheck.Xunit.v3"] do let nupkg = FileInfo (Path.Combine ("bin", $"%s{package}.%s{buildVersion}.nupkg")) let stream = nupkg.Open (FileMode.Open, FileAccess.ReadWrite) use archive = new ZipArchive (stream, ZipArchiveMode.Update) @@ -348,7 +351,7 @@ let pushNuGet (_ : HaveTested) (_ : HavePacked) = ["NUGET_KEY", nugetKey] |> Map.ofList - for package in ["FsCheck" ; "FsCheck.NUnit" ; "FsCheck.Xunit"] do + for { Name = package } in packages do let package = Path.Combine ("bin", $"%s{package}.%s{buildVersion}.nupkg") // yup, `dotnet nuget` really does have no way to pass in the critical secret secretly, so we have // to get the shell to do it From ef50745db81b78ef8e145bf96409bbe390886f08 Mon Sep 17 00:00:00 2001 From: steve-isaac Date: Tue, 8 Jul 2025 11:35:43 +0100 Subject: [PATCH 03/32] Discover traits for FsCheck.Xunit.v3 Property tests --- src/FsCheck.Xunit.v3/PropertyAttribute.fs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/FsCheck.Xunit.v3/PropertyAttribute.fs b/src/FsCheck.Xunit.v3/PropertyAttribute.fs index 684c32e8..2801768c 100644 --- a/src/FsCheck.Xunit.v3/PropertyAttribute.fs +++ b/src/FsCheck.Xunit.v3/PropertyAttribute.fs @@ -175,6 +175,19 @@ type PropertyTestCase = new(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason) = { inherit XunitTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason) } + new(testMethod, testCaseDisplayName, uniqueID, explicit, ?skipException, ?skipReason, ?skipType, ?skipUnless, ?skipWhen, ?traits, ?testMethodArguments, ?sourceFilePath, ?sourceLineNumber, ?timeout) = + let skipException = Option.toObj skipException + let skipReason = Option.toObj skipReason + let skipType = Option.toObj skipType + let skipUnless = Option.toObj skipUnless + let skipWhen = Option.toObj skipWhen + let traits = Option.toObj traits + let testMethodArguments = Option.toObj testMethodArguments + let sourceFilePath = Option.toObj sourceFilePath + let sourceLineNumber = Option.toNullable sourceLineNumber + let timeout = Option.toNullable timeout + { inherit XunitTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason,skipType, skipUnless, skipWhen, traits, testMethodArguments, sourceFilePath, sourceLineNumber, timeout) } + static let combineAttributes (configs: (PropertyConfig option) list) = configs |> List.choose id @@ -258,7 +271,7 @@ type PropertyDiscoverer(messageSink:IMessageSink) = let result = ResizeArray() let struct (testCaseDisplayName, explicit, skipExceptions, skipReason, _, _, _, _, uniqueID, testMethod) = TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, attr) - - result.Add(PropertyTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipExceptions, skipReason)) + let traits = TestIntrospectionHelper.GetTraits(testMethod, null) + result.Add(PropertyTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipExceptions, skipReason, traits=traits)) ValueTask<_>(result :> IReadOnlyCollection) From ea205380a40b683f3f86e24b56c4c17ca0de074a Mon Sep 17 00:00:00 2001 From: steveisaac Date: Tue, 8 Jul 2025 13:24:48 +0100 Subject: [PATCH 04/32] Replace old constructor --- src/FsCheck.Xunit.v3/PropertyAttribute.fs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/FsCheck.Xunit.v3/PropertyAttribute.fs b/src/FsCheck.Xunit.v3/PropertyAttribute.fs index 2801768c..6f87486d 100644 --- a/src/FsCheck.Xunit.v3/PropertyAttribute.fs +++ b/src/FsCheck.Xunit.v3/PropertyAttribute.fs @@ -172,9 +172,6 @@ type PropertyTestCase = [] new() = { inherit XunitTestCase() } - new(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason) = - { inherit XunitTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason) } - new(testMethod, testCaseDisplayName, uniqueID, explicit, ?skipException, ?skipReason, ?skipType, ?skipUnless, ?skipWhen, ?traits, ?testMethodArguments, ?sourceFilePath, ?sourceLineNumber, ?timeout) = let skipException = Option.toObj skipException let skipReason = Option.toObj skipReason From 556a11d2eb7123171355723cd3819e95a1cd24e9 Mon Sep 17 00:00:00 2001 From: steveisaac Date: Tue, 8 Jul 2025 13:33:18 +0100 Subject: [PATCH 05/32] Formatting --- src/FsCheck.Xunit.v3/PropertyAttribute.fs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/FsCheck.Xunit.v3/PropertyAttribute.fs b/src/FsCheck.Xunit.v3/PropertyAttribute.fs index 6f87486d..0cea072e 100644 --- a/src/FsCheck.Xunit.v3/PropertyAttribute.fs +++ b/src/FsCheck.Xunit.v3/PropertyAttribute.fs @@ -173,17 +173,17 @@ type PropertyTestCase = new() = { inherit XunitTestCase() } new(testMethod, testCaseDisplayName, uniqueID, explicit, ?skipException, ?skipReason, ?skipType, ?skipUnless, ?skipWhen, ?traits, ?testMethodArguments, ?sourceFilePath, ?sourceLineNumber, ?timeout) = - let skipException = Option.toObj skipException - let skipReason = Option.toObj skipReason - let skipType = Option.toObj skipType - let skipUnless = Option.toObj skipUnless - let skipWhen = Option.toObj skipWhen - let traits = Option.toObj traits - let testMethodArguments = Option.toObj testMethodArguments - let sourceFilePath = Option.toObj sourceFilePath - let sourceLineNumber = Option.toNullable sourceLineNumber - let timeout = Option.toNullable timeout - { inherit XunitTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason,skipType, skipUnless, skipWhen, traits, testMethodArguments, sourceFilePath, sourceLineNumber, timeout) } + let skipException = skipException |> Option.toObj + let skipReason = skipReason |> Option.toObj + let skipType = skipType |> Option.toObj + let skipUnless = skipUnless |> Option.toObj + let skipWhen = skipWhen |> Option.toObj + let traits = traits |> Option.toObj + let testMethodArguments = testMethodArguments |> Option.toObj + let sourceFilePath = sourceFilePath |> Option.toObj + let sourceLineNumber = sourceLineNumber |> Option.toNullable + let timeout = timeout |> Option.toNullable + { inherit XunitTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipException, skipReason, skipType, skipUnless, skipWhen, traits, testMethodArguments, sourceFilePath, sourceLineNumber, timeout) } static let combineAttributes (configs: (PropertyConfig option) list) = configs From 76899cfa044456ca21a40154f81c162df37115ca Mon Sep 17 00:00:00 2001 From: Hyogeol Lee Date: Thu, 21 Aug 2025 17:25:04 +0900 Subject: [PATCH 06/32] - Update xunit.v3.extensibility.core to v3.0.1 - Fix build error --- Directory.Packages.props | 4 ++-- src/FsCheck.Xunit.v3/PropertyAttribute.fs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3f2903ed..cba68949 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,8 +37,8 @@ - - + + \ No newline at end of file diff --git a/src/FsCheck.Xunit.v3/PropertyAttribute.fs b/src/FsCheck.Xunit.v3/PropertyAttribute.fs index 0cea072e..cf101169 100644 --- a/src/FsCheck.Xunit.v3/PropertyAttribute.fs +++ b/src/FsCheck.Xunit.v3/PropertyAttribute.fs @@ -266,8 +266,8 @@ type PropertyDiscoverer(messageSink:IMessageSink) = interface IXunitTestCaseDiscoverer with override this.Discover(discoveryOptions: ITestFrameworkDiscoveryOptions, testMethod: IXunitTestMethod, attr: IFactAttribute)= let result = ResizeArray() - let struct (testCaseDisplayName, explicit, skipExceptions, skipReason, _, _, _, _, uniqueID, testMethod) = - TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, attr) + let struct (testCaseDisplayName, explicit, skipExceptions, skipReason, _, _, _, _, _, _, uniqueID, testMethod) = + TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, attr, null, Nullable(), null, null) let traits = TestIntrospectionHelper.GetTraits(testMethod, null) result.Add(PropertyTestCase(testMethod, testCaseDisplayName, uniqueID, explicit, skipExceptions, skipReason, traits=traits)) ValueTask<_>(result :> IReadOnlyCollection) From f442f0691ce4a1ae429a1a8afc3410160cc9c8f6 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 24 Aug 2025 16:55:23 +0100 Subject: [PATCH 07/32] Update release notes. --- FsCheck Release Notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FsCheck Release Notes.md b/FsCheck Release Notes.md index e084c218..ad18154a 100644 --- a/FsCheck Release Notes.md +++ b/FsCheck Release Notes.md @@ -1,3 +1,7 @@ +### 3.3.1 - 24 August 2025 + +* Update xunit.v3.extensibility.core to v3.0.1 to fix a test discovery issue. (by Hyogeol Lee) + ### 3.3.0 - 3 June 2025 * Added FsCheck.Xunit.v3. (by JohSand) From 529af43740359435c4ac38c059f85eab4561dd6e Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 24 Aug 2025 16:57:06 +0100 Subject: [PATCH 08/32] Bump version to 3.3.1 From 36492c833f299776c236e1e6f1f078c6f0b3e390 Mon Sep 17 00:00:00 2001 From: Graham Gower Date: Tue, 16 Sep 2025 08:18:00 +0930 Subject: [PATCH 09/32] Non-empty lists can't have length zero --- src/FsCheck/FSharp.Gen.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsCheck/FSharp.Gen.fs b/src/FsCheck/FSharp.Gen.fs index 62b1e97c..3df825c4 100644 --- a/src/FsCheck/FSharp.Gen.fs +++ b/src/FsCheck/FSharp.Gen.fs @@ -459,7 +459,7 @@ module Gen = piles k n |> bind (fun sizes -> sequenceToList [ for size in sizes -> resize size gen ]))) - /// Generates non-empty lists of random lengths between zero and size. + /// Generates non-empty lists of random lengths between one and size. //[category: Creating generators from generators] [] let nonEmptyListOf gen :Gen> = From bb81e8c8b7e621b1c97641bef5a830659b36eaf9 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Wed, 29 Oct 2025 11:39:39 -0400 Subject: [PATCH 10/32] Add FSharp.Core version override for test proj This lets us use the `task` computation expression in tests --- tests/FsCheck.Test/FsCheck.Test.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FsCheck.Test/FsCheck.Test.fsproj b/tests/FsCheck.Test/FsCheck.Test.fsproj index 48b1b278..57958e13 100644 --- a/tests/FsCheck.Test/FsCheck.Test.fsproj +++ b/tests/FsCheck.Test/FsCheck.Test.fsproj @@ -33,7 +33,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From 1fd1bed5ef8504f67f1adffa831d79d515b68831 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Wed, 29 Oct 2025 11:40:39 -0400 Subject: [PATCH 11/32] Fix #718 --- src/FsCheck.NUnit/FsCheckPropertyAttribute.fs | 32 ++++---- .../FsCheck.NUnit/PropertyAttributeTests.fs | 76 ++++++++++++++++++- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/FsCheck.NUnit/FsCheckPropertyAttribute.fs b/src/FsCheck.NUnit/FsCheckPropertyAttribute.fs index 6c049fd6..8198048b 100644 --- a/src/FsCheck.NUnit/FsCheckPropertyAttribute.fs +++ b/src/FsCheck.NUnit/FsCheckPropertyAttribute.fs @@ -1,4 +1,4 @@ -namespace FsCheck.NUnit +namespace FsCheck.NUnit open System open System.Threading @@ -273,6 +273,13 @@ and FsCheckTestMethod(mi : IMethodInfo, parentSuite : Test) = elif x.Method.MethodInfo.IsStatic then None else Some context.TestObject Check.Method(config, x.Method.MethodInfo, ?target = target) + + let rec (|NonFailingNUnitResultStateException|_|) (exn : exn) = + match exn with + | :? ResultStateException as e when e.ResultState.Status <> TestStatus.Failed -> Some e + | :? AggregateException as e -> (|NonFailingNUnitResultStateException|_|) e.InnerException + | _ -> None + match testRunner.Result with | TestResult.Passed _ -> if not config.QuietOnSuccess then @@ -281,21 +288,8 @@ and FsCheckTestMethod(mi : IMethodInfo, parentSuite : Test) = | TestResult.Exhausted _ -> let msg = sprintf "Exhausted: %s" (Runner.onFinishedToString "" testRunner.Result) testResult.SetResult(ResultState(TestStatus.Failed, msg), msg) - | TestResult.Failed (_, _, _, outcome, _, _, _) -> - let nonFailure = - match outcome with - | Outcome.Failed ex -> - let unwrappedEx = - match ex with - | :? AggregateException as ae -> ae.InnerException - | _ -> ex - match unwrappedEx with - | :? ResultStateException as rse when rse.ResultState.Status = TestStatus.Failed -> None - | _ -> Some unwrappedEx - | _ -> None - match nonFailure with - | Some ex -> - testResult.RecordException(ex) - | _ -> - let msg = sprintf "%s" (Runner.onFinishedToString "" testRunner.Result) - testResult.SetResult(ResultState(TestStatus.Failed, msg), msg) + | TestResult.Failed (_, _, _, Outcome.Failed (NonFailingNUnitResultStateException e), _, _, _) -> + testResult.RecordException e + | TestResult.Failed _ -> + let msg = sprintf "%s" (Runner.onFinishedToString "" testRunner.Result) + testResult.SetResult(ResultState(TestStatus.Failed, msg), msg) diff --git a/tests/FsCheck.Test/FsCheck.NUnit/PropertyAttributeTests.fs b/tests/FsCheck.Test/FsCheck.NUnit/PropertyAttributeTests.fs index 951104cb..b81c7c0e 100644 --- a/tests/FsCheck.Test/FsCheck.NUnit/PropertyAttributeTests.fs +++ b/tests/FsCheck.Test/FsCheck.NUnit/PropertyAttributeTests.fs @@ -1,6 +1,11 @@ -namespace Fscheck.Test.FsCheck.NUnit.PropertyAttribute +namespace Fscheck.Test.FsCheck.NUnit.PropertyAttribute +open System.Reflection +open System.Threading.Tasks open FsCheck +open NUnit.Framework +open NUnit.Framework.Internal +open FsCheck.FSharp open FsCheck.NUnit module ResultStateExceptionHandlingTest = @@ -14,3 +19,72 @@ module ResultStateExceptionHandlingTest = NUnit.Framework.Assert.Pass() } + [] + let ``should pass when AssertPass called inside task``() = + task { + NUnit.Framework.Assert.Pass() + } + +module ResultOutputTests = + [] + module TestModule = + type private T = T + let ty = typeof.DeclaringType + + [] + let ``should show full result info on exn in test method`` () = + do failwith "Some exception." + + [] + let ``should show full result info on failing property in test method`` () = + Prop.ofTestable false |> Prop.label "Some label." + + [] + let ``should show full result info on exn in Async<'Testable>-returning test method`` () = + async { + failwith "Some exception." + return () + } + + [] + let ``should show full result info on exn in Task-returning test method`` () : Task = + task { + failwith "Some exception." + return () + } + + [] + let ``should show full result info on failing property in Async-returning test method`` () = + async { + return Prop.ofTestable false |> Prop.label "Some label." + } + + [] + let ``should show full result info on failing property in Task-returning test method`` () = + task { + return Prop.ofTestable false |> Prop.label "Some label." + } + + [] + let ``should show full result info on failures in Async/Task-returning test methods`` () = + let test = TestSuite TestModule.ty + + let testMethods = + [| + for meth in (TypeWrapper TestModule.ty).GetMethods (BindingFlags.Public ||| BindingFlags.Static) do + if not (Array.isEmpty (meth.GetCustomAttributes (``inherit``=true))) then + FsCheckTestMethod (meth, test) + |] + + let ctx = TestExecutionContext () + let results = testMethods |> Array.map _.RunTest(ctx) + + let withoutFalsifiable = + results + |> Array.choose (fun result -> + if result.Message.Contains "Falsifiable, after 1 test" then None + else Some $"{result.Name}: {result.Message}\n{result.StackTrace}") + + withoutFalsifiable + |> Array.isEmpty + |> Prop.label $"Expected all messages to contain falsifiable message but got: '%A{withoutFalsifiable}'." From 252f44790732b0225fa4acb1a383aba49bda774e Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Thu, 30 Oct 2025 12:56:56 -0400 Subject: [PATCH 12/32] Add replay hint to failure message --- src/FsCheck/Runner.fs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/FsCheck/Runner.fs b/src/FsCheck/Runner.fs index e907f701..8fd5500d 100644 --- a/src/FsCheck/Runner.fs +++ b/src/FsCheck/Runner.fs @@ -499,9 +499,12 @@ module Runner = | 1 -> sprintf "Label of failing property: %s%s" (labelsToString l) newline | _ -> sprintf "Labels of failing property (one or more is failing):%s%s%s" newline (labelsToString l) newline - let onFailureToString name data originalArgs args usedSeed lastSeed lastSize = - sprintf "%sFalsifiable, after %i test%s (%i shrink%s) (%A)%sLast step was invoked with size of %i and seed of (%A):%s" - name data.NumberOfTests (pluralize data.NumberOfTests) data.NumberOfShrinks (pluralize data.NumberOfShrinks) usedSeed newline lastSize lastSeed newline + let onFailureToString name data originalArgs args usedSeed (lastSeed:Rnd) lastSize = + let replayHint = + if data.NumberOfTests > 1 then $".%s{newline}Replay directly at failing step with (%d{lastSeed.Seed},%d{lastSeed.Gamma},%d{lastSize})." + else ":" + + $"%s{name}Falsifiable, after %i{data.NumberOfTests} test%s{pluralize data.NumberOfTests} (%i{data.NumberOfShrinks} shrink%s{pluralize data.NumberOfShrinks}) (%A{usedSeed}). %s{newline}Last step was invoked with size of %i{lastSize} and seed of (%A{lastSeed})%s{replayHint}%s{newline}" + maybePrintLabels data.Labels + sprintf "Original:%s%s%s" newline (argumentsToString originalArgs) newline + if (data.NumberOfShrinks > 0 ) then sprintf "Shrunk:%s%s%s" newline (argumentsToString args) newline else "" From 37f66fcc8469210407f71a9b4921fd621b864957 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 9 Nov 2025 11:04:10 +0000 Subject: [PATCH 13/32] Update target frameworks from net6.0 to net8.0 --- examples/CSharp.DocSnippets/CSharp.DocSnippets.csproj | 2 +- examples/FsCheck.CSharpExamples/FsCheck.CSharpExamples.csproj | 2 +- examples/FsCheck.Examples/FsCheck.Examples.fsproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/CSharp.DocSnippets/CSharp.DocSnippets.csproj b/examples/CSharp.DocSnippets/CSharp.DocSnippets.csproj index 06a939b4..4c2481e4 100644 --- a/examples/CSharp.DocSnippets/CSharp.DocSnippets.csproj +++ b/examples/CSharp.DocSnippets/CSharp.DocSnippets.csproj @@ -2,7 +2,7 @@ CSharp.DocSnippets - net6.0 + net8.0 Exe false diff --git a/examples/FsCheck.CSharpExamples/FsCheck.CSharpExamples.csproj b/examples/FsCheck.CSharpExamples/FsCheck.CSharpExamples.csproj index be2456a3..2266320b 100644 --- a/examples/FsCheck.CSharpExamples/FsCheck.CSharpExamples.csproj +++ b/examples/FsCheck.CSharpExamples/FsCheck.CSharpExamples.csproj @@ -2,7 +2,7 @@ FsCheck.CSharpExamples - net6.0 + net8.0 Exe false diff --git a/examples/FsCheck.Examples/FsCheck.Examples.fsproj b/examples/FsCheck.Examples/FsCheck.Examples.fsproj index a24b53dd..6e0fec71 100644 --- a/examples/FsCheck.Examples/FsCheck.Examples.fsproj +++ b/examples/FsCheck.Examples/FsCheck.Examples.fsproj @@ -2,7 +2,7 @@ FsCheck.Examples - net6.0 + net8.0 Exe false From 50085226e65828286d77c68eda2289110a3c53ed Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 9 Nov 2025 11:04:33 +0000 Subject: [PATCH 14/32] Update sdk version in global.json to 9.0.306. --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 088e8bd6..b7bbcb26 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.306", "rollForward": "latestFeature" } } From 84e694f26337dec9d5d949d770e8943bc6bd7d3e Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 9 Nov 2025 12:03:30 +0000 Subject: [PATCH 15/32] More projects to net8.0. --- build.fsx | 3 --- .../FsCheck.MsTest.Examples.csproj | 2 +- .../FsCheck.NUnit.CSharpExamples.csproj | 2 +- .../FsCheck.NUnit.Examples/FsCheck.NUnit.Examples.fsproj | 2 +- .../FsCheck.NUnit.Examples/SyncVersusAsyncExamples.fs | 8 ++++---- .../FsCheck.XUnit.CSharpExamples.csproj | 2 +- tests/FsCheck.Test.CSharp/FsCheck.Test.CSharp.csproj | 2 +- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/build.fsx b/build.fsx index 97cfcf02..0f648908 100644 --- a/build.fsx +++ b/build.fsx @@ -149,9 +149,6 @@ let releaseNotesFile = "FsCheck Release Notes.md" /// Solution or project files to be built during the building process let solution = "FsCheck.sln" -/// Pattern specifying assemblies to be tested -let testAssemblies = "tests/**/bin/Release/net6.0/*.Test.dll" - // Git configuration (used for publishing documentation in gh-pages branch) // The profile where the project is posted let gitOwner = "fscheck" diff --git a/examples/FsCheck.MsTest.Examples/FsCheck.MsTest.Examples.csproj b/examples/FsCheck.MsTest.Examples/FsCheck.MsTest.Examples.csproj index 141329aa..127c9848 100644 --- a/examples/FsCheck.MsTest.Examples/FsCheck.MsTest.Examples.csproj +++ b/examples/FsCheck.MsTest.Examples/FsCheck.MsTest.Examples.csproj @@ -2,7 +2,7 @@ FsCheck.MsTest.Examples - net6.0 + net8.0 AnyCPU true 3 diff --git a/examples/FsCheck.NUnit.CSharpExamples/FsCheck.NUnit.CSharpExamples.csproj b/examples/FsCheck.NUnit.CSharpExamples/FsCheck.NUnit.CSharpExamples.csproj index f37eb3ef..7149c5b7 100644 --- a/examples/FsCheck.NUnit.CSharpExamples/FsCheck.NUnit.CSharpExamples.csproj +++ b/examples/FsCheck.NUnit.CSharpExamples/FsCheck.NUnit.CSharpExamples.csproj @@ -2,7 +2,7 @@ FsCheck.NUnit.CSharpExamples - net6.0 + net8.0 AnyCPU true 3 diff --git a/examples/FsCheck.NUnit.Examples/FsCheck.NUnit.Examples.fsproj b/examples/FsCheck.NUnit.Examples/FsCheck.NUnit.Examples.fsproj index 323c7dd2..0f7b9e5d 100644 --- a/examples/FsCheck.NUnit.Examples/FsCheck.NUnit.Examples.fsproj +++ b/examples/FsCheck.NUnit.Examples/FsCheck.NUnit.Examples.fsproj @@ -2,7 +2,7 @@ FsCheck.NUnit.Examples - net6.0 + net8.0 true false diff --git a/examples/FsCheck.NUnit.Examples/SyncVersusAsyncExamples.fs b/examples/FsCheck.NUnit.Examples/SyncVersusAsyncExamples.fs index e3b519ef..c360f320 100644 --- a/examples/FsCheck.NUnit.Examples/SyncVersusAsyncExamples.fs +++ b/examples/FsCheck.NUnit.Examples/SyncVersusAsyncExamples.fs @@ -7,22 +7,22 @@ module SyncVersusAsyncExamples = let private doSomethingAsync () = async { return () } [] - let ``Sync - should pass`` b = + let ``Sync - should pass`` (b: bool) = b = b |> Prop.label "b = b" [] - let ``Sync - should fail`` b = + let ``Sync - should fail`` (b: bool) = b = not b |> Prop.label "b = not b" [] - let ``Async - should pass`` b = + let ``Async - should pass`` (b: bool) = async { do! doSomethingAsync () return b = b |> Prop.label "b = b" } [] - let ``Async - should fail`` b = + let ``Async - should fail`` (b : bool) = async { do! doSomethingAsync () return b = not b |> Prop.label "b = not b" diff --git a/examples/FsCheck.XUnit.CSharpExamples/FsCheck.XUnit.CSharpExamples.csproj b/examples/FsCheck.XUnit.CSharpExamples/FsCheck.XUnit.CSharpExamples.csproj index 6ce16722..3663f6b7 100644 --- a/examples/FsCheck.XUnit.CSharpExamples/FsCheck.XUnit.CSharpExamples.csproj +++ b/examples/FsCheck.XUnit.CSharpExamples/FsCheck.XUnit.CSharpExamples.csproj @@ -2,7 +2,7 @@ FsCheck.Xunit.CSharpExamples - net6.0 + net8.0 AnyCPU true 3 diff --git a/tests/FsCheck.Test.CSharp/FsCheck.Test.CSharp.csproj b/tests/FsCheck.Test.CSharp/FsCheck.Test.CSharp.csproj index f6212c8f..1e3bf162 100644 --- a/tests/FsCheck.Test.CSharp/FsCheck.Test.CSharp.csproj +++ b/tests/FsCheck.Test.CSharp/FsCheck.Test.CSharp.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 10 false From 1bc547d0fa40de44d35f680101a6043ad7477643 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 9 Nov 2025 12:08:21 +0000 Subject: [PATCH 16/32] Update release notes and version. --- FsCheck Release Notes.md | 6 ++++++ src/FsCheck.NUnit/AssemblyInfo.fs | 8 ++++---- src/FsCheck.Xunit.v3/AssemblyInfo.fs | 16 ++++++++-------- src/FsCheck.Xunit/AssemblyInfo.fs | 12 ++++++------ src/FsCheck/AssemblyInfo.fs | 8 ++++---- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/FsCheck Release Notes.md b/FsCheck Release Notes.md index ad18154a..fb4108fa 100644 --- a/FsCheck Release Notes.md +++ b/FsCheck Release Notes.md @@ -1,3 +1,9 @@ +### 3.3.2 - 9 November 2025 + +* Fixed FsCheck.NUnit failure reporting. (by Brian Rourke Boll) + +* Added a replay hint to failure message. (by Brian Rourke Boll) + ### 3.3.1 - 24 August 2025 * Update xunit.v3.extensibility.core to v3.0.1 to fix a test discovery issue. (by Hyogeol Lee) diff --git a/src/FsCheck.NUnit/AssemblyInfo.fs b/src/FsCheck.NUnit/AssemblyInfo.fs index 2b2ec280..1f0f0608 100644 --- a/src/FsCheck.NUnit/AssemblyInfo.fs +++ b/src/FsCheck.NUnit/AssemblyInfo.fs @@ -7,8 +7,8 @@ open System.Runtime.CompilerServices [] [] [] -[] -[] +[] +[] [] do () @@ -17,7 +17,7 @@ module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck.NUnit" let [] AssemblyProduct = "FsCheck.NUnit" let [] AssemblyDescription = "Integrates FsCheck with NUnit" - let [] AssemblyVersion = "3.2.0" - let [] AssemblyFileVersion = "3.2.0" + let [] AssemblyVersion = "3.3.2" + let [] AssemblyFileVersion = "3.3.2" let [] AssemblyKeyFile = "../../FsCheckKey.snk" diff --git a/src/FsCheck.Xunit.v3/AssemblyInfo.fs b/src/FsCheck.Xunit.v3/AssemblyInfo.fs index 12b8659d..56b013e0 100644 --- a/src/FsCheck.Xunit.v3/AssemblyInfo.fs +++ b/src/FsCheck.Xunit.v3/AssemblyInfo.fs @@ -6,18 +6,18 @@ open System.Runtime.CompilerServices [] [] -[] -[] -[] +[] +[] +[] [] -[] + do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck.Xunit.v3" let [] AssemblyProduct = "FsCheck.Xunit.v3" - let [] AssemblyDescription = "Integrates FsCheck with xUnit.v3" - let [] AssemblyVersion = "3.2.0" - let [] AssemblyFileVersion = "3.2.0" + let [] AssemblyDescription = "Integrates FsCheck with xUnit.NET v3" + let [] AssemblyVersion = "3.3.2" + let [] AssemblyFileVersion = "3.3.2" let [] AssemblyKeyFile = "../../FsCheckKey.snk" - let [] InternalsVisibleTo = "FsCheck.Test" + diff --git a/src/FsCheck.Xunit/AssemblyInfo.fs b/src/FsCheck.Xunit/AssemblyInfo.fs index a5d61da7..91fdf064 100644 --- a/src/FsCheck.Xunit/AssemblyInfo.fs +++ b/src/FsCheck.Xunit/AssemblyInfo.fs @@ -6,9 +6,9 @@ open System.Runtime.CompilerServices [] [] -[] -[] -[] +[] +[] +[] [] [] do () @@ -16,8 +16,8 @@ do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck.Xunit" let [] AssemblyProduct = "FsCheck.Xunit" - let [] AssemblyDescription = "Integrates FsCheck with xUnit.NET" - let [] AssemblyVersion = "3.2.0" - let [] AssemblyFileVersion = "3.2.0" + let [] AssemblyDescription = "Integrates FsCheck with xUnit.NET v2" + let [] AssemblyVersion = "3.3.2" + let [] AssemblyFileVersion = "3.3.2" let [] AssemblyKeyFile = "../../FsCheckKey.snk" let [] InternalsVisibleTo = "FsCheck.Test" diff --git a/src/FsCheck/AssemblyInfo.fs b/src/FsCheck/AssemblyInfo.fs index 2d3350d5..aaf741ab 100644 --- a/src/FsCheck/AssemblyInfo.fs +++ b/src/FsCheck/AssemblyInfo.fs @@ -7,8 +7,8 @@ open System.Runtime.CompilerServices [] [] [] -[] -[] +[] +[] [] [] do () @@ -17,7 +17,7 @@ module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck" let [] AssemblyProduct = "FsCheck" let [] AssemblyDescription = "FsCheck is a tool for testing .NET programs automatically using randomly generated test cases." - let [] AssemblyVersion = "3.2.0" - let [] AssemblyFileVersion = "3.2.0" + let [] AssemblyVersion = "3.3.2" + let [] AssemblyFileVersion = "3.3.2" let [] AssemblyKeyFile = "../../FsCheckKey.snk" let [] InternalsVisibleTo = "FsCheck.Test" From 7df8a3c8ac35b3ef1213033fd63d1578dada7335 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 9 Nov 2025 12:09:57 +0000 Subject: [PATCH 17/32] Bump version to 3.3.2 From fb36783e3a39bc3422860b14fde4fc022c22849c Mon Sep 17 00:00:00 2001 From: Oleksandr Bogomaz Date: Wed, 17 Dec 2025 12:05:22 +0000 Subject: [PATCH 18/32] update fsdocs tool --- .config/dotnet-tools.json | 4 ++-- build.fsx | 2 -- docs/RunningTests.fsx | 3 ++- docs/_template.html | 6 +++++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c214efb3..92e4b415 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,8 +2,8 @@ "version": 1, "isRoot": true, "tools": { - "fsharp.formatting.commandtool": { - "version": "9.0.4", + "fsdocs-tool": { + "version": "21.0.0", "commands": [ "fsdocs" ] diff --git a/build.fsx b/build.fsx index 0f648908..0f38641a 100644 --- a/build.fsx +++ b/build.fsx @@ -371,7 +371,6 @@ let fsdocParameters = let fsdocProperties = [ "Configuration=Release" - "TargetFramework=netstandard2.0" ] type HaveGeneratedDocs = HaveGeneratedDocs @@ -381,7 +380,6 @@ let docs (_ : HaveBuilt) : HaveGeneratedDocs = [ "fsdocs" ; "build" ; "--strict" ; "--eval" ; "--clean" - "--projects" ; "src/FsCheck/FsCheck.fsproj" "--properties" ; yield! fsdocProperties "--parameters" ; yield! fsdocParameters ] diff --git a/docs/RunningTests.fsx b/docs/RunningTests.fsx index 31a1a57c..e3d80c8e 100644 --- a/docs/RunningTests.fsx +++ b/docs/RunningTests.fsx @@ -4,7 +4,7 @@ (*** hide ***) #I @"../src/FsCheck/bin/Release/netstandard2.0" #I @"../src/FsCheck.Xunit/bin/Release/netstandard2.0" -#I @"../src/FsCheck.NUnit/bin/Release/netstandard2.0" +#I @"../src/FsCheck.NUnit/bin/Release/net6.0" #r @"nuget: xunit.core" #r @"nuget: NUnit" #r "FsCheck" @@ -13,6 +13,7 @@ open FsCheck open System +open FsCheck.FSharp (** # Running tests diff --git a/docs/_template.html b/docs/_template.html index f80d6ed9..f2b074c2 100644 --- a/docs/_template.html +++ b/docs/_template.html @@ -26,6 +26,10 @@ + + @@ -73,7 +77,7 @@

From fac46f6f5c07b64d2983fd2e47b8dcc03ecc1b07 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 16 Dec 2025 13:49:58 +0000 Subject: [PATCH 19/32] Embed Untracked Sources Set `EmbedUntrackedSources=true` so that compiler-generated files are included. --- Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Build.props b/Directory.Build.props index 837c0560..8d9f9332 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,6 +7,7 @@ true + true Copyright 2008-2024 Kurt Schelfthout and contributors F# fsharp test random From 1ab6b6521df5d8b0fe7b362c3a91497c567b1a53 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Mon, 26 Jan 2026 21:38:58 +0100 Subject: [PATCH 20/32] Fix the "Useful methods" section in the TestData docs (#727) * Fix documentation about functions that have been moved to ArbMap * Remove reference to Arb.shrink function that no longer exists * Re-arrange bullet points in the 'Useful methods' section --- docs/TestData.fsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/TestData.fsx b/docs/TestData.fsx index f19fd591..be7c9423 100644 --- a/docs/TestData.fsx +++ b/docs/TestData.fsx @@ -632,14 +632,13 @@ However, there are some important things to notice that are listed here to avoid - The same thing with the default DoNotSize generators of the number-like types except Decimal. - Most of the default generators of the collection types are just "wrappers" around the F# list. Thus, you can assume that they are generated and shrinked the same way. -## Useful methods on the Arb module +## Useful methods on the Arb and ArbMap module -- `Arb.from<'a>` returns the registered Arbitrary instance for the given type 'a - `Arb.fromGen` makes a new Arbitrary instance from just a given generator - the shrinker return the empty sequence - `Arb.fromGenShrink` make a new Arbitrary instance from a given generator and shrinker. This is equivalent to implementing Arbitrary yourself, but may be shorter. -- `Arb.generate<'a>` returns the generator of the registered Arbitrary instance for the given type 'a -- `Arb.shrink` return the immediate shrinks of the registered Arbitrary instance for the given value - `Arb.convert` given conversion functions to ('a ->'b) and from ('b ->'a), converts an Arbitrary<'a> instance to an Arbitrary<'b> - `Arb.filter` filters the generator and shrinker for a given Arbitrary instance to contain only those values that match with the given filter function - `Arb.mapFilter` maps the generator and filter the shrinkers for a given Arbitrary instance. Mapping the generator is sometimes faster, e.g. for a PositiveInt it is faster to take the absolute value than to filter the negative values. -- `Arb.Default` is a type that contains all the default Arbitrary instances as they are shipped and registerd by FsCheck by default. This is useful when you override a default generator - typically this is because you want to filter certain values from it, and then you need to be able to refer to the default generator in your overriding generator.*) +- `ArbMap.arbitrary<'a> arbMap` returns the Arbitrary instance in `arbMap` for the given type 'a +- `ArbMap.generate<'a> arbMap` returns the generator of the Arbitrary instance in `arbMap` for the given type 'a +- `ArbMap.defaults` is a map that provides access to all the default Arbitrary instances as they are shipped by FsCheck by default. This is useful when you override a default generator - typically this is because you want to filter certain values from it, and then you need to be able to refer to the default generator in your overriding generator.*) From 7220fbe110c4d9b8a93048efbeefc88b6b67268a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:09:18 +0000 Subject: [PATCH 21/32] Add Gen.pick and Gen.tryPick functions (#730) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kurtschelfthout <164917+kurtschelfthout@users.noreply.github.com> --- src/FsCheck/FSharp.Gen.fs | 29 +++++++++++++++++++++++++++++ tests/FsCheck.Test/Gen.fs | 14 ++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/FsCheck/FSharp.Gen.fs b/src/FsCheck/FSharp.Gen.fs index 3df825c4..d8419ed9 100644 --- a/src/FsCheck/FSharp.Gen.fs +++ b/src/FsCheck/FSharp.Gen.fs @@ -400,6 +400,35 @@ module Gen = [] let filter (predicate:'T->bool) generator = where predicate generator + ///Tries to apply the given chooser function to successive values generated by the given generator, + ///returning the first result where the function returns Some(x). This function 'gives up' by generating None + ///if the given original generator did not generate any values for which the chooser function returned Some(x), + ///after trying to get values by increasing its size. + //[category: Creating generators from generators] + [] + let tryPick (chooser:'T->option<'U>) generator = + let rec tryValue k s = + match (k,s) with + | (_,0) -> constant None + // Resize with 2*k+s to progressively increase size with each retry attempt + | (k,s) -> (resize (2*k+s) generator) |> bind (fun x -> + match chooser x with + | Some v -> constant (Some v) + | None -> tryValue (k+1) (s-1)) + sized (max 1 >> tryValue 0) + + ///Applies the given chooser function to successive values generated by the given generator, + ///returning the first result where the function returns Some(x). Contrary to tryPick, this function + ///keeps re-trying by increasing the size of the original generator ad infinitum. Make sure there is + ///a high probability that the chooser function returns Some for some values. + //[category: Creating generators from generators] + [] + let rec pick (chooser:'T->option<'U>) generator = + tryPick chooser generator |> bind (fun mx -> + match mx with + | Some x -> constant x + | None -> sized (fun n -> resize (n+1) (pick chooser generator))) + let inline private shuffleInPlace (arr: array<_>) = // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle let inline swap (arr : array<_>) i j = diff --git a/tests/FsCheck.Test/Gen.fs b/tests/FsCheck.Test/Gen.fs index 36670fcd..c3833c57 100644 --- a/tests/FsCheck.Test/Gen.fs +++ b/tests/FsCheck.Test/Gen.fs @@ -241,6 +241,20 @@ module Gen = |> sample1 |> ((=) (abs v)) ) + [] + let TryPick (v:int) (chooser:int -> option) = + let expected = chooser v + assertTrue ( Gen.tryPick chooser (Gen.constant v) + |> sample1 + |> ((=) expected) ) + + [] + let Pick (v:int) = + let chooser x = if x >= 0 then Some (string x) else None + assertTrue ( Gen.pick chooser (Gen.elements [v;abs v]) + |> sample1 + |> ((=) (string (abs v))) ) + [] let ListOf (NonNegativeInt size) (v:char) = assertTrue ( Gen.resize size (Gen.listOf <| Gen.constant v) From c3e0144e4fb09ba73ec998fff72e0e814c08748c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:11:29 +0000 Subject: [PATCH 22/32] Fix NullReferenceException in takeWhilePlusLast with single-element sequences (#728) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kurtschelfthout <164917+kurtschelfthout@users.noreply.github.com> --- .../FsCheck.NUnit.Examples/PropertyExamples.fs | 18 +++++++++++++++++- src/FsCheck/Internals.Common.fs | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/examples/FsCheck.NUnit.Examples/PropertyExamples.fs b/examples/FsCheck.NUnit.Examples/PropertyExamples.fs index 34c67014..81219790 100644 --- a/examples/FsCheck.NUnit.Examples/PropertyExamples.fs +++ b/examples/FsCheck.NUnit.Examples/PropertyExamples.fs @@ -4,6 +4,7 @@ open NUnit.Framework open FsCheck open FsCheck.FSharp open FsCheck.NUnit +open FsCheck.Fluent [] type NUnitTest() = @@ -69,4 +70,19 @@ type NUnitTest() = [] member __.NoOutputOnSuccess (x:char) = - List.rev [x] = [x] \ No newline at end of file + List.rev [x] = [x] + + // Regression test for issue: Implies with Replay causes TargetInvocationException/NullReferenceException + // This test verifies that using Implies with Replay doesn't throw a NullReferenceException + // Note: This test will be exhausted because the condition is always false, but that's expected behavior + [] + member __.ImpliesWithReplayFalseCondition_shouldFail() = + // This will result in "Exhausted" because the condition is always false, + // but it should NOT throw a NullReferenceException (which was the bug) + false.Implies(true) + + // Regression test with a passing condition + [] + member __.ImpliesWithReplayTrueCondition() = + // This should pass because the condition is true and the property is true + true.Implies(true) \ No newline at end of file diff --git a/src/FsCheck/Internals.Common.fs b/src/FsCheck/Internals.Common.fs index e9a2be68..78d48e93 100644 --- a/src/FsCheck/Internals.Common.fs +++ b/src/FsCheck/Internals.Common.fs @@ -51,8 +51,8 @@ module internal Common = if en.MoveNext() then // Always yield the current, stop if predicate does not hold yield en.Current - if predicate en.Current then - yield! loop en } + if predicate en.Current then + yield! loop en } // Get enumerator of the sequence and yield all results // (making sure that the enumerator gets disposed) From 2ea24edaa19cc30b83eb709c1c2c47e9726b349f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:23:49 +0000 Subject: [PATCH 23/32] Merge stamps and labels in And/Or property combinators (#729) --- Directory.Packages.props | 1 - src/FsCheck/Testable.fs | 28 ++++++++----- tests/FsCheck.Test/Property.fs | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) 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/Testable.fs b/src/FsCheck/Testable.fs index 523a987c..456a6a7c 100644 --- a/src/FsCheck/Testable.fs +++ b/src/FsCheck/Testable.fs @@ -21,23 +21,31 @@ type Result = { Outcome : Outcome Stamp : list Labels : Set - Arguments : list } + Arguments : list } + member private t.MergeLabels(o) = + { t with Labels = Set.union t.Labels o.Labels } + member private t.MergeStamps(o) = + { t with Stamp = List.append t.Stamp o.Stamp } ///Returns a new result that is Passed if and only if both this ///and the given Result are Passed. static member internal ResAnd(l, r) = match (l.Outcome,r.Outcome) with - | (Outcome.Failed _,_) -> l //here a potential Failed in r is thrown away... + // no need to merge stamps when overall outcome is failed - they won't be shown anyway. + // no need to merge labels when overall outcome is not failed (passed, rejected) - they won't be shown anyway. + | (Outcome.Failed _,Outcome.Failed _) -> l.MergeLabels(r) + | (Outcome.Failed _,_) -> l | (_,Outcome.Failed _) -> r - | (_,Outcome.Passed) -> l - | (Outcome.Passed,_) -> r - | (Outcome.Rejected,Outcome.Rejected) -> l //or r, whatever + | (_,Outcome.Rejected) -> r.MergeStamps(l) + | (Outcome.Rejected,_) -> l.MergeStamps(r) + | (Outcome.Passed,Outcome.Passed) -> l.MergeStamps(r) static member internal ResOr(l, r) = match (l.Outcome, r.Outcome) with - | (Outcome.Failed _,_) -> r - | (_,Outcome.Failed _) -> l - | (Outcome.Passed,_) -> l - | (_,Outcome.Passed) -> r - | (Outcome.Rejected,Outcome.Rejected) -> l //or r, whatever + | (Outcome.Failed _,Outcome.Failed _) -> l.MergeLabels(r) + | (Outcome.Failed _,_) -> r.MergeStamps(l) + | (_,Outcome.Failed _) -> l.MergeStamps(r) + | (Outcome.Passed,_) -> l.MergeStamps(r) + | (_,Outcome.Passed) -> r.MergeStamps(l) + | (Outcome.Rejected,Outcome.Rejected) -> l.MergeStamps(r) //or r.MergeStamps(l), whatever type ResultContainer = | Value of Result diff --git a/tests/FsCheck.Test/Property.fs b/tests/FsCheck.Test/Property.fs index ed1858f1..dba2468f 100644 --- a/tests/FsCheck.Test/Property.fs +++ b/tests/FsCheck.Test/Property.fs @@ -312,4 +312,77 @@ module Property = failwith "Expected label to be applied" | t -> failwithf "Expected failing test with exception, got %A" t + [] + let ``And should merge stamps from both properties when both pass``() = + let prop1 = true |> Prop.collect "Property 1" + let prop2 = true |> Prop.collect "Property 2" + let resultRunner = GetResultRunner() + let config = Config.Quick.WithRunner(resultRunner).WithMaxTest(1) + Check.One(config, prop1 .&. prop2) + test <@ match resultRunner.Result with + | TestResult.Passed (d, _) -> + let stamps = d.Stamps |> Seq.collect snd |> Set.ofSeq + stamps.Contains("\"Property 1\"") && stamps.Contains("\"Property 2\"") + | _ -> false @> + + [] + let ``And should merge labels from both failing properties``() = + let prop1 = false |> Prop.label "L1" + let prop2 = false |> Prop.label "L2" + let resultRunner = GetResultRunner() + let config = Config.Quick.WithRunner(resultRunner).WithMaxTest(1) + Check.One(config, prop1 .&. prop2) + test <@ match resultRunner.Result with + | TestResult.Failed (d, _, _, _, _, _, _) -> + d.Labels.Contains("L1") && d.Labels.Contains("L2") + | _ -> false @> + + [] + let ``And should keep label from the failing property``() = + let prop1 = false |> Prop.label "L1" + let prop2 = true |> Prop.label "L2" + let resultRunner = GetResultRunner() + let config = Config.Quick.WithRunner(resultRunner).WithMaxTest(1) + Check.One(config, prop1 .&. prop2) + test <@ match resultRunner.Result with + | TestResult.Failed (d, _, _, _, _, _, _) -> + d.Labels.Contains("L1") && not(d.Labels.Contains("L2")) + | _ -> false @> + [] + let ``Or should merge stamps from both properties when both pass``() = + let prop1 = true |> Prop.collect "Property 1" + let prop2 = true |> Prop.collect "Property 2" + let resultRunner = GetResultRunner() + let config = Config.Quick.WithRunner(resultRunner).WithMaxTest(1) + Check.One(config, prop1 .|. prop2) + test <@ match resultRunner.Result with + | TestResult.Passed (d, _) -> + let stamps = d.Stamps |> Seq.collect snd |> Set.ofSeq + stamps.Contains("\"Property 1\"") && stamps.Contains("\"Property 2\"") + | _ -> false @> + + [] + let ``Or should merge stamps from both properties when one passes``() = + let prop1 = true |> Prop.collect "Property 1" + let prop2 = false |> Prop.collect "Property 2" + let resultRunner = GetResultRunner() + let config = Config.Quick.WithRunner(resultRunner).WithMaxTest(1) + Check.One(config, prop1 .|. prop2) + test <@ match resultRunner.Result with + | TestResult.Passed (d, _) -> + let stamps = d.Stamps |> Seq.collect snd |> Set.ofSeq + stamps.Contains("\"Property 1\"") && stamps.Contains("\"Property 2\"") + | _ -> false @> + + [] + let ``Or should merge labels from both failing properties when both fail``() = + let prop1 = false |> Prop.label "L1" + let prop2 = false |> Prop.label "L2" + let resultRunner = GetResultRunner() + let config = Config.Quick.WithRunner(resultRunner).WithMaxTest(1) + Check.One(config, prop1 .|. prop2) + test <@ match resultRunner.Result with + | TestResult.Failed (d, _, _, _, _, _, _) -> + d.Labels.Contains("L1") && d.Labels.Contains("L2") + | _ -> false @> From 2aad30f7c33cc8effef4646c3c52434719fc477d Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 26 Apr 2026 13:55:17 +0100 Subject: [PATCH 24/32] Update xunit runner. --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6eeced38..10f524b7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,6 @@ - + \ No newline at end of file From 50b1d62e46772fb9e860033148276ca6b544e646 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 26 Apr 2026 13:55:27 +0100 Subject: [PATCH 25/32] Tidy. --- .../FsCheck.XUnit.CSharpExamples/ReverseFixture.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/FsCheck.XUnit.CSharpExamples/ReverseFixture.cs b/examples/FsCheck.XUnit.CSharpExamples/ReverseFixture.cs index b696482c..9458ac1e 100644 --- a/examples/FsCheck.XUnit.CSharpExamples/ReverseFixture.cs +++ b/examples/FsCheck.XUnit.CSharpExamples/ReverseFixture.cs @@ -8,26 +8,26 @@ namespace FsCheck.XUnit.CSharpExamples public class ReverseFixture { [Property(QuietOnSuccess = true, EndSize = 10000)] - public Task Task_shouldFail(int i) + public Task ShouldFail_Task(int i) { - return System.Threading.Tasks.Task.FromResult(i < 2000); + return Task.FromResult(i < 2000); } [Property(QuietOnSuccess = true, EndSize = 1000)] - public async Task TaskDelay_shouldFail(int i) + public async Task ShouldFail_TaskDelay(int i) { - await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(4)).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false); return false; } [Property(QuietOnSuccess = true)] - public bool Bcl(int[] xs) + public bool ShouldPass_Bool(int[] xs) { return xs.Reverse().Reverse().SequenceEqual(xs); } [Property(QuietOnSuccess = true)] - public void Bcl2(int[] xs) + public void ShouldPass_Void(int[] xs) { if(true == xs.Reverse().Reverse().SequenceEqual(xs)) { From de611a9e566551c6f81c86d38f937b563a916ea8 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 26 Apr 2026 14:02:18 +0100 Subject: [PATCH 26/32] Bump version to 3.3.3. --- FsCheck Release Notes.md | 8 ++++++++ src/FsCheck.NUnit/AssemblyInfo.fs | 8 ++++---- src/FsCheck.Xunit.v3/AssemblyInfo.fs | 8 ++++---- src/FsCheck.Xunit/AssemblyInfo.fs | 8 ++++---- src/FsCheck/AssemblyInfo.fs | 8 ++++---- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/FsCheck Release Notes.md b/FsCheck Release Notes.md index fb4108fa..cc9dd972 100644 --- a/FsCheck Release Notes.md +++ b/FsCheck Release Notes.md @@ -1,3 +1,11 @@ +## 3.3.3 - 26 April 2026 + +* Add Gen.pick and Gen.tryPick functions. + +* Fill NullReferenceException when using Replay with an Implies combinator. + +* Improve behavior of stamps and labels when using And/Or property combinators. + ### 3.3.2 - 9 November 2025 * Fixed FsCheck.NUnit failure reporting. (by Brian Rourke Boll) diff --git a/src/FsCheck.NUnit/AssemblyInfo.fs b/src/FsCheck.NUnit/AssemblyInfo.fs index 1f0f0608..a0f67ef9 100644 --- a/src/FsCheck.NUnit/AssemblyInfo.fs +++ b/src/FsCheck.NUnit/AssemblyInfo.fs @@ -7,8 +7,8 @@ open System.Runtime.CompilerServices [] [] [] -[] -[] +[] +[] [] do () @@ -17,7 +17,7 @@ module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck.NUnit" let [] AssemblyProduct = "FsCheck.NUnit" let [] AssemblyDescription = "Integrates FsCheck with NUnit" - let [] AssemblyVersion = "3.3.2" - let [] AssemblyFileVersion = "3.3.2" + let [] AssemblyVersion = "3.3.3" + let [] AssemblyFileVersion = "3.3.3" let [] AssemblyKeyFile = "../../FsCheckKey.snk" diff --git a/src/FsCheck.Xunit.v3/AssemblyInfo.fs b/src/FsCheck.Xunit.v3/AssemblyInfo.fs index 56b013e0..16d8127f 100644 --- a/src/FsCheck.Xunit.v3/AssemblyInfo.fs +++ b/src/FsCheck.Xunit.v3/AssemblyInfo.fs @@ -7,8 +7,8 @@ open System.Runtime.CompilerServices [] [] [] -[] -[] +[] +[] [] do () @@ -17,7 +17,7 @@ module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck.Xunit.v3" let [] AssemblyProduct = "FsCheck.Xunit.v3" let [] AssemblyDescription = "Integrates FsCheck with xUnit.NET v3" - let [] AssemblyVersion = "3.3.2" - let [] AssemblyFileVersion = "3.3.2" + let [] AssemblyVersion = "3.3.3" + let [] AssemblyFileVersion = "3.3.3" let [] AssemblyKeyFile = "../../FsCheckKey.snk" diff --git a/src/FsCheck.Xunit/AssemblyInfo.fs b/src/FsCheck.Xunit/AssemblyInfo.fs index 91fdf064..621012bd 100644 --- a/src/FsCheck.Xunit/AssemblyInfo.fs +++ b/src/FsCheck.Xunit/AssemblyInfo.fs @@ -7,8 +7,8 @@ open System.Runtime.CompilerServices [] [] [] -[] -[] +[] +[] [] [] do () @@ -17,7 +17,7 @@ module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck.Xunit" let [] AssemblyProduct = "FsCheck.Xunit" let [] AssemblyDescription = "Integrates FsCheck with xUnit.NET v2" - let [] AssemblyVersion = "3.3.2" - let [] AssemblyFileVersion = "3.3.2" + let [] AssemblyVersion = "3.3.3" + let [] AssemblyFileVersion = "3.3.3" let [] AssemblyKeyFile = "../../FsCheckKey.snk" let [] InternalsVisibleTo = "FsCheck.Test" diff --git a/src/FsCheck/AssemblyInfo.fs b/src/FsCheck/AssemblyInfo.fs index aaf741ab..2d9f6897 100644 --- a/src/FsCheck/AssemblyInfo.fs +++ b/src/FsCheck/AssemblyInfo.fs @@ -7,8 +7,8 @@ open System.Runtime.CompilerServices [] [] [] -[] -[] +[] +[] [] [] do () @@ -17,7 +17,7 @@ module internal AssemblyVersionInformation = let [] AssemblyTitle = "FsCheck" let [] AssemblyProduct = "FsCheck" let [] AssemblyDescription = "FsCheck is a tool for testing .NET programs automatically using randomly generated test cases." - let [] AssemblyVersion = "3.3.2" - let [] AssemblyFileVersion = "3.3.2" + let [] AssemblyVersion = "3.3.3" + let [] AssemblyFileVersion = "3.3.3" let [] AssemblyKeyFile = "../../FsCheckKey.snk" let [] InternalsVisibleTo = "FsCheck.Test" From ec83a8d2361d3fffe874f1e504e9b08344f14b26 Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 26 Apr 2026 14:05:43 +0100 Subject: [PATCH 27/32] Bump version to 3.3.3 From c2363acfd5a283ba5366338036e94b1eb186c0ee Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 26 Apr 2026 14:20:30 +0100 Subject: [PATCH 28/32] Clone repo via https. --- build.fsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.fsx b/build.fsx index 0f38641a..9845a48c 100644 --- a/build.fsx +++ b/build.fsx @@ -411,7 +411,7 @@ let releaseDocs (_ : HaveGeneratedDocs) = let tempDocsDir = "temp/gh-pages" cleanDirectories [tempDocsDir] let tempDocsDir = Directory.CreateDirectory tempDocsDir - runProcess "git" ["clone" ; "git@github.com:fscheck/FsCheck.git" ; "--single-branch" ; "--branch" ; "gh-pages" ; tempDocsDir.FullName] + runProcess "git" ["clone" ; "https://github.com/fscheck/FsCheck.git" ; "--single-branch" ; "--branch" ; "gh-pages" ; tempDocsDir.FullName] copyDir (DirectoryInfo "output") tempDocsDir true From b04f3b0f662bdd58da8ea606e18c08970c9fdcfc Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sun, 26 Apr 2026 15:35:43 +0100 Subject: [PATCH 29/32] Delete outdated docs/_template.html, it messed up the docs site --- docs/_template.html | 99 --------------------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 docs/_template.html diff --git a/docs/_template.html b/docs/_template.html deleted file mode 100644 index f2b074c2..00000000 --- a/docs/_template.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - {{fsdocs-page-title}} - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- {{fsdocs-content}} - {{fsdocs-tooltips}} -
- - - - - - - - -
- - - - From 08d6d5c3711383d7ad8bd603cbddc52a0b7fbb1d Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sat, 25 Jul 2026 23:35:37 +0100 Subject: [PATCH 30/32] C# record generation: fix bug manifesting as cast exception, if a positional record's properties do not appear in the same order as in the ctor. Happens (only?) with inheritance. Fixes #735 --- src/FsCheck/Internals.Reflect.fs | 48 +++++++++++++++++----------- tests/FsCheck.Test.CSharp/Records.cs | 7 ++-- tests/FsCheck.Test/Arbitrary.fs | 23 +++++++++++-- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/FsCheck/Internals.Reflect.fs b/src/FsCheck/Internals.Reflect.fs index 31db1321..fcaec806 100644 --- a/src/FsCheck/Internals.Reflect.fs +++ b/src/FsCheck/Internals.Reflect.fs @@ -96,7 +96,6 @@ module internal Reflect = "ImmutableDictionary`2"; "ImmutableSortedDictionary`2" |] - /// Get information on the fields of an F# record type let getRecordFieldTypes (recordType: System.Type) = if isRecordType recordType then @@ -135,29 +134,42 @@ module internal Reflect = f.Invoke let getCSharpRecordFields (recordType: Type) = - if isCSharpRecordType recordType then - getProperties recordType - |> Seq.filter (fun p -> p.CanWrite) - |> Seq.map (fun p -> p.PropertyType) - else + if not <| isCSharpRecordType recordType then failwithf "The input type must be a C# record-like type. Got %A" recordType - let getCSharpRecordConstructor (t:Type) = - let props = getProperties t |> Seq.filter (fun p -> p.CanWrite) |> Seq.toArray + let maybeCtor = getPublicCtors recordType |> Seq.tryHead + let pars = + maybeCtor + |> Option.map (_.GetParameters()) + |> Option.defaultValue [||] + + getProperties recordType + |> Seq.filter (fun p -> p.CanWrite) + |> Seq.sortBy (fun p -> pars |> Array.tryFindIndex (fun param -> param.Name = p.Name) |> Option.defaultValue Int32.MaxValue) + |> Seq.map (fun p -> p.PropertyType) + + + let getCSharpRecordConstructor (recordType:Type) = + let maybeCtor = getPublicCtors recordType |> Seq.tryHead + let ctorPars = + maybeCtor + |> Option.map (_.GetParameters()) + |> Option.defaultValue [||] + + let props = getProperties recordType + |> Seq.filter (fun p -> p.CanWrite) + |> Seq.sortBy (fun p -> ctorPars |> Array.tryFindIndex (fun param -> param.Name = p.Name) |> Option.defaultValue Int32.MaxValue) + |> Seq.toArray let par = Expression.Parameter (typeof, "args") - - let maybeCtor = getPublicCtors t |> Seq.tryHead - let pars = - maybeCtor - |> Option.map (_.GetParameters() >> Array.mapi (fun i p -> + ctorPars + |> Array.mapi (fun i p -> Expression.Convert - (Expression.ArrayIndex (par, Expression.Constant i), p.ParameterType) : Expression)) - |> Option.defaultValue [||] + (Expression.ArrayIndex (par, Expression.Constant i), p.ParameterType) : Expression) let values = - props + props |> Seq.mapi (fun i p -> let idx = Expression.ArrayIndex (par, Expression.Constant i) Expression.Convert (idx, p.PropertyType) :> Expression) @@ -169,10 +181,10 @@ module internal Reflect = let newExpr = maybeCtor |> Option.map (fun ctor -> Expression.New (ctor, pars)) - |> Option.defaultWith (fun () -> Expression.New t) + |> Option.defaultWith (fun () -> Expression.New recordType) let body = - if t.IsValueType then + if recordType.IsValueType then Expression.Convert (Expression.MemberInit (newExpr, bindings), typeof) : Expression else Expression.MemberInit (newExpr, bindings) diff --git a/tests/FsCheck.Test.CSharp/Records.cs b/tests/FsCheck.Test.CSharp/Records.cs index d4119f1e..3d329705 100644 --- a/tests/FsCheck.Test.CSharp/Records.cs +++ b/tests/FsCheck.Test.CSharp/Records.cs @@ -17,9 +17,6 @@ public record Person public record PersonWithHeight : Person { public int HeightInInches { get; init; } - - public PersonWithHeight Grow(int inches) => - this with { HeightInInches = HeightInInches + inches }; } public record CtorAndProps(int A) @@ -27,6 +24,10 @@ public record CtorAndProps(int A) public int B { get; init; } } + public abstract record AbstractPositionalRecord(string S); + + public record PositionalSubRecord(string S, int I) : AbstractPositionalRecord(S); + public readonly record struct ReadOnlyStructPositionalRecord(byte Red, byte Green, byte Blue); public readonly record struct ReadOnlyStructInitOnlyRecord diff --git a/tests/FsCheck.Test/Arbitrary.fs b/tests/FsCheck.Test/Arbitrary.fs index 2ff8daed..18df0c24 100644 --- a/tests/FsCheck.Test/Arbitrary.fs +++ b/tests/FsCheck.Test/Arbitrary.fs @@ -749,9 +749,12 @@ module Arbitrary = [] let ``should derive generator for csharp record types``() = - generate |> sample 10 |> ignore - generate |> sample 10 |> ignore - generate |> sample 10 |> ignore + let s = generate |> sample 10 + test <@ s |> Seq.forall (fun rgb -> let b = rgb.Blue in true) @> + let s = generate |> sample 10 + test <@ s |> Seq.forall (fun r -> let b = r.C in true) @> + let s = generate |> sample 10 + test <@ s |> Seq.forall (fun r -> let b = r.Ex1 in true) @> let persons = generate |> sample 10 test <@ persons |> Seq.exists(fun p -> not (System.String.IsNullOrEmpty(p.FirstName))) @> @@ -763,6 +766,12 @@ module Arbitrary = let mixed = generate |> sample 10 test <@ mixed |> Seq.exists(fun p -> p.B <> 0) @> + [] + let ``should derive generator for csharp positional record subtype``() = + let sample = generate |> sample 10 + test <@ sample |> Seq.forall(fun p -> let s = (p :> CSharp.AbstractPositionalRecord).S in true) @> + + [] let ``should derive generator for csharp struct record types`` () = generate |> sample 10 |> ignore @@ -794,6 +803,14 @@ module Arbitrary = shrunkv <> value && (abs shrunkv.A <= abs value.A || abs shrunkv.B <= abs value.B)) + [] + let ``Derived generator for c# record types shrinks - PositionalSubRecord`` (value: CSharp.PositionalSubRecord) = + let shrunk = shrink value + shrunk + |> Seq.forall (fun shrunkv -> + shrunkv <> value + && (shrunkv.S <= value.S || abs shrunkv.I <= abs value.I)) + [] let ``should derive generator for Immutable collections``() = From 32e5701143e5cc90ee670b866c9b0cd8abf654bd Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sat, 25 Jul 2026 23:43:56 +0100 Subject: [PATCH 31/32] Docs: removed the note about 3.x being in pre-release. --- docs/index.fsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.fsx b/docs/index.fsx index 888a6a75..87b9dc9a 100644 --- a/docs/index.fsx +++ b/docs/index.fsx @@ -15,7 +15,7 @@ you can [find on NuGet](https://www.nuget.org/packages?q=fscheck). Warning: Documentation ---------------------- -The Documentation section below was written for version 2.x, but 3.x is the version that is currently maintained. Only v3 will get new features and fixes, even though it's in pre-release. +The Documentation section below was written for version 2.x, but 3.x is the version that is currently maintained. Only v3 will get new features and fixes. This leaves us in the unfortunate position that some documentation is out of date and incomplete. One exception is the API docs. They are generated from the 3.x code and are accurate - if in doubt, believe the API docs. From 7c583d6df4939643fd36f0439694be1456833aff Mon Sep 17 00:00:00 2001 From: Kurt Schelfthout Date: Sat, 25 Jul 2026 23:49:59 +0100 Subject: [PATCH 32/32] Bump version to 3.3.4 --- FsCheck Release Notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FsCheck Release Notes.md b/FsCheck Release Notes.md index cc9dd972..7cb2737d 100644 --- a/FsCheck Release Notes.md +++ b/FsCheck Release Notes.md @@ -1,3 +1,7 @@ +## 3.3.4 - 25 July 2026 + +* Fixed a bug in C# record type generation. + ## 3.3.3 - 26 April 2026 * Add Gen.pick and Gen.tryPick functions.