From 3deb95518c08d0343271093cc4d9d40fdd664cc1 Mon Sep 17 00:00:00 2001 From: John Efford Date: Thu, 22 Jun 2023 17:47:49 +0100 Subject: [PATCH] part way through docs --- .vscode/settings.json | 3 + Hedgehog.Xunit.sln | 17 +- documentation/readme-cSharp.md | 378 ++++++++++++++++++ .../csharp-examples/DocumentationSamples.cs | 42 ++ .../Usings.cs | 0 ...itiveAndNegativeGeneratorContainerTypes.cs | 2 +- .../PositiveAndNegativeSimpleAttribute.cs | 4 +- ...dNegativeUtilizingIntegerRangeAttribute.cs | 2 +- .../csharp-examples.csproj} | 2 +- examples/fsharp-examples/Tests.fs | 69 ++++ .../fsharp-examples/fsharp-examples.fsproj | 32 ++ readme.md | 157 ++++++-- src/Hedgehog.Xunit/Attributes.fs | 2 +- src/Hedgehog.Xunit/InternalLogic.fs | 2 +- tests/Hedgehog.Xunit.Tests/PropertyTests.fs | 6 +- 15 files changed, 662 insertions(+), 56 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 documentation/readme-cSharp.md create mode 100644 examples/csharp-examples/DocumentationSamples.cs rename examples/{csharp-attribute-based-parameters-comparision => csharp-examples}/Usings.cs (100%) rename examples/{csharp-attribute-based-parameters-comparision => csharp-examples/attribute-based-parameter-comparison}/PositiveAndNegativeGeneratorContainerTypes.cs (93%) rename examples/{csharp-attribute-based-parameters-comparision => csharp-examples/attribute-based-parameter-comparison}/PositiveAndNegativeSimpleAttribute.cs (84%) rename examples/{csharp-attribute-based-parameters-comparision => csharp-examples/attribute-based-parameter-comparison}/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs (91%) rename examples/{csharp-attribute-based-parameters-comparision/csharp-attribute-based-parameters-comparision.csproj => csharp-examples/csharp-examples.csproj} (92%) create mode 100644 examples/fsharp-examples/Tests.fs create mode 100644 examples/fsharp-examples/fsharp-examples.fsproj diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..80720f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Hedgehog.Xunit.sln" +} \ No newline at end of file diff --git a/Hedgehog.Xunit.sln b/Hedgehog.Xunit.sln index 41aa603..9df59b0 100644 --- a/Hedgehog.Xunit.sln +++ b/Hedgehog.Xunit.sln @@ -10,10 +10,13 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E5FCE96F-A6AE-434F-B901-19359F39B504}" ProjectSection(SolutionItems) = preProject CHANGELOG.md = CHANGELOG.md + documentation\readme-cSharp.md = documentation\readme-cSharp.md readme.md = readme.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp-attribute-based-parameters-comparision", "examples\csharp-attribute-based-parameters-comparision\csharp-attribute-based-parameters-comparision.csproj", "{2048061B-0561-4297-A02C-A12A263177A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp-examples", "examples\csharp-examples\csharp-examples.csproj", "{601273CF-7FBF-4263-9EC4-8206CFDF6DC8}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "fsharp-examples", "examples\fsharp-examples\fsharp-examples.fsproj", "{7F2616F0-500D-488F-A2BC-B4D6D4833DE1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,10 +32,14 @@ Global {63A8D184-519E-4061-8A74-F1EACAF3B0D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {63A8D184-519E-4061-8A74-F1EACAF3B0D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {63A8D184-519E-4061-8A74-F1EACAF3B0D5}.Release|Any CPU.Build.0 = Release|Any CPU - {2048061B-0561-4297-A02C-A12A263177A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2048061B-0561-4297-A02C-A12A263177A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2048061B-0561-4297-A02C-A12A263177A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2048061B-0561-4297-A02C-A12A263177A5}.Release|Any CPU.Build.0 = Release|Any CPU + {601273CF-7FBF-4263-9EC4-8206CFDF6DC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {601273CF-7FBF-4263-9EC4-8206CFDF6DC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {601273CF-7FBF-4263-9EC4-8206CFDF6DC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {601273CF-7FBF-4263-9EC4-8206CFDF6DC8}.Release|Any CPU.Build.0 = Release|Any CPU + {7F2616F0-500D-488F-A2BC-B4D6D4833DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F2616F0-500D-488F-A2BC-B4D6D4833DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F2616F0-500D-488F-A2BC-B4D6D4833DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F2616F0-500D-488F-A2BC-B4D6D4833DE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/documentation/readme-cSharp.md b/documentation/readme-cSharp.md new file mode 100644 index 0000000..4377695 --- /dev/null +++ b/documentation/readme-cSharp.md @@ -0,0 +1,378 @@ +# fsharp-hedgehog-xunit + +[![][nuget-shield]][nuget] [![][workflow-shield]][workflow] [![Coverage Status](https://coveralls.io/repos/github/dharmaturtle/fsharp-hedgehog-xunit/badge.svg?branch=main)](https://coveralls.io/github/dharmaturtle/fsharp-hedgehog-xunit?branch=main) + +[Hedgehog][hedgehog] with convenience attributes for [xUnit.net][xunit]. + + + +## Features + +- Test method arguments generated by the customizable [`GenX.auto`](https://github.com/hedgehogqa/fsharp-hedgehog-experimental/#auto-generation). +- `Property.check` called for each test. + + +## Getting Started + +Install the _Hedgehog.Xunit_ [package][nuget] from Visual Studio's Package Manager Console: + +```powershell +PM> Install-Package Hedgehog.Xunit +``` + +Suppose you have a test that uses [Hedgehog.Experimental](https://github.com/hedgehogqa/fsharp-hedgehog-experimental) and looks similar to the following: + +```CSharp +using Hedgehog; +using Hedgehog.Linq; +using Hedgehog.Xunit; +using Property = Hedgehog.Linq.Property; + +public class DocumentationSamples +{ + [Fact] + public void Reversing_a_list_twice_yields_the_original_list() + { + var gen = GenX.auto>(); + var prop = from data in Property.ForAll(gen) + let testList = Enumerable.Reverse(data).Reverse().ToList() + select Assert.Equivalent(data, testList, true); + prop.Check(); + } +} +``` + +Then using Hedgehog.Xunit, you can simplify the above test to + +```CSharp +[Property] +public void Reversing_a_list_twice_yields_the_original_list_with_xunit(List xs) +{ + var testList = Enumerable.Reverse(xs).ToList(); + Assert.Equivalent(xs, testList, true); +} +``` + + + +## Documentation + +`Hedgehog.Xunit` provides the `Property`, `Properties`, and `Recheck` attributes. + +### `Property` attribute + +Methods with the `Property` attribute have their arguments generated by [`GenX.auto`](https://github.com/hedgehogqa/fsharp-hedgehog-experimental/#auto-generation). + +
+F# Example showing data generation + +```f# +type ``class with a test`` (output: Xunit.Abstractions.ITestOutputHelper) = + [] + let ``Can generate an int`` (i: int) = + output.WriteLine $"Test input: {i}" +``` +
+ +
+C# Examples showing data generation + +```CSharp + private readonly ITestOutputHelper _output; + + public DocumentationSamples(ITestOutputHelper output) + { + _output = output; + } + + [Property] + public void Can_generate_an_int( + int i) + { + _output.WriteLine($"Test input: {i}"); + } +``` +
+ + +``` +=== Output === +Test input: 0 +Test input: -1 +Test input: 1 +... +Test input: 522317518 +Test input: 404306656 +Test input: 1550509078 +``` + +`Property.check` is also run. + +```f# +[] +let ``This test fails`` (b: bool) = + b + +=== Output === +Hedgehog.FailedException: *** Failed! Falsifiable (after 2 tests): +(false) +``` + +If the test returns `Async<_>` or `Task<_>`, then `Async.RunSynchronously` is called, _which blocks the thread._ This may have significant performance implications as tests run 100 times by default. + +```f# +[] +let ``Async with exception shrinks`` (i: int) = async { + do! Async.Sleep 100 + if i > 10 then + failwith "whoops!" + } + +=== Output === +Hedgehog.FailedException: *** Failed! Falsifiable (after 12 tests): +(11) +``` +A test returning a `Result` in an `Error` state will be treated as a failure. + +```f# +[] +let ``Result with Error shrinks`` (i: int) = + if i > 10 then + Error () + else + Ok () + +=== Output === +Hedgehog.FailedException: *** Failed! Falsifiable (after 13 tests and 2 shrinks): +[11] +``` + +Tests returning `Async>` or `Task>` are run synchronously and are expected to be in the `Ok` state. + +Tests returning a `Property` or `Property` will have `Property.check` automatically called: + +```f# +[] +let ``returning a failing property with an external number gen fails and shrinks`` i = property { + let! _50 = Gen.constant 50 + return i <= _50 +} + +=== Output === +System.Exception: *** Failed! Falsifiable (after 23 tests and 5 shrinks): +[51] +50 +``` + +The `Property` attribute's constructor may take several arguments: `AutoGenConfig`, `AutoGenConfigArgs`, `Tests` (count), `Shrinks` (count), and `Size`. Since the `Property` attribute extends `Xunit.FactAttribute`, it may also take `DisplayName`, `Skip`, and `Timeout`. + +#### `AutoGenConfig` and `AutoGenConfigArgs` + +* Default: `GenX.defaults` + +Create a class with a single static property or method that returns an instance of `AutoGenConfig`. Then provide the type of this class as an argument to the `Property` attribute. This works around the constraint that [`Attribute` parameters must be a constant.](https://stackoverflow.com/a/33007272) + +```f# +type AutoGenConfigContainer = + static member __ = + GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) + +[)>] +let ``This test passes`` (i: int) = + i = 13 +``` + +If the method takes arguments, you must provide them using `AutoGenConfigArgs`. + +```f# +type ConfigWithArgs = + static member __ a b = + GenX.defaults + |> AutoGenConfig.addGenerator (Gen.constant a) + |> AutoGenConfig.addGenerator (Gen.constant b) + +[, AutoGenConfigArgs = [|"foo"; 13|])>] +let ``This also passes`` s i = + s = "foo" && i = 13 +``` + +#### `Tests` (count) + +Specifies the number of tests to be run, though more or less may occur due to shrinking or early failure. + +```f# +[)>] +let ``This runs 3 times`` () = + () +``` + +#### `Shrinks` (count) + +Specifies the maximal number of shrinks that may run. + +```f# +[)>] +let ``No shrinks occur`` i = + if i > 50 then failwith "oops" +``` + +#### `Size` + +Sets the `Size` to a value for all runs. + +```f# +[] +let ``"i" mostly ranges between -1 and 1`` i = + printfn "%i" i +``` + +### `Properties` attribute + +This optional attribute can decorate modules or classes. It sets default arguments for `AutoGenConfig`, `AutoGenConfigArgs`, `Tests`, `Shrinks`, and `Size`. These will be overridden by any arguments provided by the `Property` attribute. + +```f# +type Int13 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) +type Int2718 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 2718) + +[, 1)>] +module ``Module with tests`` = + + [] + let ``this passes and runs once`` (i: int) = + i = 13 + + [, 2)>] + let ``this passes and runs twice`` (i: int) = + i = 2718 +``` + +### `Recheck` attribute + +This optional method attribute invokes `Property.recheck` with the given `Size` and `Seed`. It must be used with `Property`. + +```f# +[] +[] +let ``this passes`` i = + i = 12345 +``` + +## Tips + +Use named arguments to select the desired constructor overload. + +```f# +[, AutoGenConfig = typeof)>] +module __ = + [, Tests = 2718, Skip = "just because")>] + let ``Not sure why you'd do this, but okay`` () = + () +``` + +Consider extending `PropertyAttribute` or `PropertiesAttribute` to hardcode commonly used arguments. + +```f# +type Int13 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) + +type PropertyInt13Attribute() = inherit PropertyAttribute(typeof) +module __ = + [] + let ``this passes`` (i: int) = + i = 13 + +type PropertiesInt13Attribute() = inherit PropertiesAttribute(typeof) +[] +module ___ = + [] + let ``this also passes`` (i: int) = + i = 13 +``` + +
+ Known issue with generating a single tuple. + +`GenX.autoWith` can generate a tuple. + +```f# +[] +let ``This passes`` () = + Property.check <| property { + let! a, b = + GenX.defaults + |> AutoGenConfig.addGenerator (Gen.constant (1, 2)) + |> GenX.autoWith + Assert.Equal(1, a) + Assert.Equal(2, b) + } +``` + +However, blindly converting the above test to `Hedgehog.Xunit` will fail. + +```f# +type CustomTupleGen = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant (1, 2)) +[)>] +let ``This fails`` ((a,b) : int*int) = + Assert.Equal(1, a) + Assert.Equal(2, b) +``` + +This is because F# functions whose only parameter is a tuple will generate IL that un-tuples that parameter, yielding a function whose arity is the number of elements in the tuple. More concretely, this F# + +```f# +let ``This fails`` ((a,b) : int*int) = () +``` + +yields this IL (in debug mode) + +```IL +.method public static + void 'This fails' ( + valuetype [System.Private.CoreLib]System.Int32 _arg1_0, + valuetype [System.Private.CoreLib]System.Int32 _arg1_1 + ) cil managed +{ + .maxstack 8 + IL_0000: ret +} +``` + +Due to this behavior `Hedgehog.Xunit` can't know that the original parameter was a tuple. It will therefore not use the registered tuple generator. A workaround is to pass a second (possibly unused) parameter. + +```f# +type CustomTupleGen = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant (1, 2)) +[)>] +let ``This passes`` (((a,b) : int*int), _: bool) = + Assert.Equal(1, a) + Assert.Equal(2, b) +``` + +The updated F# + +```f# +let ``This passes`` (((a,b) : int*int), _: bool) = () +``` + +yields this IL + +```IL +.method public static + void 'This passes' ( + class [System.Private.CoreLib]System.Tuple`2 _arg1, + valuetype [System.Private.CoreLib]System.Boolean _arg2 + ) cil managed +{ + .maxstack 8 + IL_0000: ret +} +``` + +[Source of IL.](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+BbA9sgrsAKYAEAsgJ5l6FECwAUI8TCQHY4BOWAhsAGL42AYxjwcbEjxIAjEgF4pJNLMbMirAAaaAKgAt4EEmB6II2kgApLPKDICUJECXhsYAKlcxHiy/bUMLCTa+oYkAA48EBBE5ppW1rYOTi5unm72UCQA+s4yODjAPlb2QA) +
+ +[hedgehog]: https://github.com/hedgehogqa/fsharp-hedgehog +[xunit]: https://xunit.net/ + +[nuget]: https://www.nuget.org/packages/Hedgehog.Xunit/ +[nuget-shield]: https://img.shields.io/nuget/v/Hedgehog.Xunit.svg +[workflow]: https://github.com/dharmaturtle/fsharp-hedgehog-xunit/actions?query=workflow%3AMain +[workflow-shield]: https://github.com/dharmaturtle/fsharp-hedgehog-xunit/workflows/Main/badge.svg diff --git a/examples/csharp-examples/DocumentationSamples.cs b/examples/csharp-examples/DocumentationSamples.cs new file mode 100644 index 0000000..3958375 --- /dev/null +++ b/examples/csharp-examples/DocumentationSamples.cs @@ -0,0 +1,42 @@ +using Hedgehog; +using Hedgehog.Linq; +using Hedgehog.Xunit; +using Xunit.Abstractions; +using Property = Hedgehog.Linq.Property; + +namespace csharp_examples; + +public class DocumentationSamples +{ + private readonly ITestOutputHelper _output; + + public DocumentationSamples(ITestOutputHelper output) + { + _output = output; + } + + [Property] + public void Can_generate_an_int( + int i) + { + _output.WriteLine($"Test input: {i}"); + } + + + [Fact] + public void Reversing_a_list_twice_yields_the_original_list() + { + var gen = GenX.auto>(); + var prop = from data in Property.ForAll(gen) + let testList = Enumerable.Reverse(data).Reverse().ToList() + select Assert.Equivalent(data, testList, true); + prop.Check(); + } + + [Property] + public void Reversing_a_list_twice_yields_the_original_list_with_xunit(List xs) + { + var testList = Enumerable.Reverse(xs).ToList(); + Assert.Equivalent(xs, testList, true); + } +} diff --git a/examples/csharp-attribute-based-parameters-comparision/Usings.cs b/examples/csharp-examples/Usings.cs similarity index 100% rename from examples/csharp-attribute-based-parameters-comparision/Usings.cs rename to examples/csharp-examples/Usings.cs diff --git a/examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeGeneratorContainerTypes.cs b/examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeGeneratorContainerTypes.cs similarity index 93% rename from examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeGeneratorContainerTypes.cs rename to examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeGeneratorContainerTypes.cs index 1fcb2b2..c882e1d 100644 --- a/examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeGeneratorContainerTypes.cs +++ b/examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeGeneratorContainerTypes.cs @@ -5,7 +5,7 @@ using Range = Hedgehog.Linq.Range; -namespace csharp_attribute_based_parameters_comparision; +namespace csharp_examples.attribute_based_parameter_comparison; public record PositiveInt(int Value); public record NegativeInt( int Value ); diff --git a/examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeSimpleAttribute.cs b/examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeSimpleAttribute.cs similarity index 84% rename from examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeSimpleAttribute.cs rename to examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeSimpleAttribute.cs index 87d2b25..76a58c4 100644 --- a/examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeSimpleAttribute.cs +++ b/examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeSimpleAttribute.cs @@ -5,11 +5,11 @@ namespace csharp_attribute_based_parameters_comparision; -public class Negative : ParameterGeneraterBaseType +public class Negative : ParameterGeneratorBaseType { public override Gen Generator => Gen.Int32(Range.Constant(Int32.MinValue, -1)); } -public class Positive : ParameterGeneraterBaseType +public class Positive : ParameterGeneratorBaseType { public override Gen Generator => Gen.Int32(Range.Constant(1, Int32.MaxValue)); } diff --git a/examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs b/examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs similarity index 91% rename from examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs rename to examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs index 8b4054b..ba125cc 100644 --- a/examples/csharp-attribute-based-parameters-comparision/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs +++ b/examples/csharp-examples/attribute-based-parameter-comparison/PositiveAndNegativeUtilizingIntegerRangeAttribute.cs @@ -5,7 +5,7 @@ namespace csharp_attribute_based_parameters_comparision; -public class Int32Range : ParameterGeneraterBaseType +public class Int32Range : ParameterGeneratorBaseType { private readonly int _min; private readonly int _max; diff --git a/examples/csharp-attribute-based-parameters-comparision/csharp-attribute-based-parameters-comparision.csproj b/examples/csharp-examples/csharp-examples.csproj similarity index 92% rename from examples/csharp-attribute-based-parameters-comparision/csharp-attribute-based-parameters-comparision.csproj rename to examples/csharp-examples/csharp-examples.csproj index 34ba708..5bf33f5 100644 --- a/examples/csharp-attribute-based-parameters-comparision/csharp-attribute-based-parameters-comparision.csproj +++ b/examples/csharp-examples/csharp-examples.csproj @@ -2,7 +2,7 @@ net7.0 - csharp_attribute_based_parameters_comparision + csharp_examples enable enable diff --git a/examples/fsharp-examples/Tests.fs b/examples/fsharp-examples/Tests.fs new file mode 100644 index 0000000..9d325e5 --- /dev/null +++ b/examples/fsharp-examples/Tests.fs @@ -0,0 +1,69 @@ +namespace fsharp_examples +module Tests = + +open Xunit +open Hedgehog +open Hedgehog.Xunit + +//Properties containing multiple parameter of the same type with different +//generator requirements. + +///Initial property +let positiveInt() = Range.constant 0 System.Int32.MaxValue |> Gen.int32 +let negativeInt() = Range.constant System.Int32.MinValue 0 |> Gen.int32 + +[] +let ``Positive + Negative <= Positive`` () = + property { + let! positive = positiveInt() + let! negative = negativeInt() + return positive + negative <= positive + } |> Property.checkBool + +//Using property attribute we need to create container types so that +//the parameters of the property can be of different types. +type PositiveInt = {value : int} +type NegativeInt = {value : int} + +type AutoGenConfigContainer = + static member __ = + GenX.defaults + |> AutoGenConfig.addGenerator (positiveInt() |> Gen.map(fun x-> {PositiveInt.value=x})) + |> AutoGenConfig.addGenerator (negativeInt() |> Gen.map(fun x -> {NegativeInt.value=x})) + + +[)>] +let ``Positive + Negative <= Positive xunit`` (positive:PositiveInt) (negative:NegativeInt) = + positive.value + negative.value <= positive.value + +//Using attributes to configure what generator the property should use +type Posint() = + inherit ParameterGeneratorBaseType() + override this.Generator = positiveInt() + +type NegInt() = + inherit ParameterGeneratorBaseType() + override this.Generator = negativeInt() + +[] +let ``Positive + Negative <= Positive xunit attribute`` ([] positive) ([] negative) = + positive + negative <= positive + + + + + + +type Posint() = + inherit ParameterGeneratorBaseType() + override this.Generator = positiveInt() + +type NegInt() = + inherit ParameterGeneratorBaseType() + override this.Generator = negativeInt() + +[] +let ``Positive + Negative <= Positive xunit`` ([] positive) ([] negative) = + positive + negative <= positive + + diff --git a/examples/fsharp-examples/fsharp-examples.fsproj b/examples/fsharp-examples/fsharp-examples.fsproj new file mode 100644 index 0000000..637fe45 --- /dev/null +++ b/examples/fsharp-examples/fsharp-examples.fsproj @@ -0,0 +1,32 @@ + + + + net7.0 + fsharp_examples + + false + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/readme.md b/readme.md index 6e186fe..25311ab 100644 --- a/readme.md +++ b/readme.md @@ -8,8 +8,9 @@ ## Features -- Test method arguments generated by the customizable [`GenX.auto`](https://github.com/hedgehogqa/fsharp-hedgehog-experimental/#auto-generation). +- Test method arguments can be generated [`GenX.auto`](https://github.com/hedgehogqa/fsharp-hedgehog-experimental/#auto-generation). - `Property.check` called for each test. +- Attribute based generator definitions ## Getting Started @@ -43,11 +44,18 @@ let ``Reversing a list twice yields the original list, with Hedgehog.Xunit`` (xs ## Documentation -`Hedgehog.Xunit` provides the `Property`, `Properties`, and `Recheck` attributes. +`Hedgehog.Xunit` provides the following attributes: +* [Property](#properties-attribute) +Converts xunit 'Fact' into a property, allowing you to easily configure property parameters. +* [Properties](#properties-attribute) +Allows the easy configuration of all properties in a test suite. +* [Recheck](#recheck-attribute) +Rerun a particular test case. +* [ParameterGeneratorBaseType]() Control what generatore is used on a parameter by parameter basis. configure ### `Property` attribute - -Methods with the `Property` attribute have their arguments generated by [`GenX.auto`](https://github.com/hedgehogqa/fsharp-hedgehog-experimental/#auto-generation). +--- +Methods with the `Property` attribute have their arguments automatically generated by [`GenX.auto`](https://github.com/hedgehogqa/fsharp-hedgehog-experimental/#auto-generation). ```f# type ``class with a test`` (output: Xunit.Abstractions.ITestOutputHelper) = @@ -65,7 +73,7 @@ Test input: 404306656 Test input: 1550509078 ``` -`Property.check` is also run. +`Property.check` is called automatically. ```f# [] @@ -123,40 +131,47 @@ System.Exception: *** Failed! Falsifiable (after 23 tests and 5 shrinks): 50 ``` -The `Property` attribute's constructor may take several arguments: `AutoGenConfig`, `AutoGenConfigArgs`, `Tests` (count), `Shrinks` (count), and `Size`. Since the `Property` attribute extends `Xunit.FactAttribute`, it may also take `DisplayName`, `Skip`, and `Timeout`. - -#### `AutoGenConfig` and `AutoGenConfigArgs` +### `Property` Configuration +--- +The `Property` attribute's constructor may take several arguments: +* [`AutoGenConfig` and `AutoGenConfigArgs`](#autogenconfig-and-autogenconfigargs): Allow the manual control of generators +* [`Tests`](#tests-count): Specifies the number of tests to run +* [`Shrinks`](#shrinks-count): Specifies the number of shrinks -* Default: `GenX.defaults` +The `Property` attribute extends `Xunit.FactAttribute`, so it may also take `DisplayName`, `Skip`, and `Timeout`. -Create a class with a single static property or method that returns an instance of `AutoGenConfig`. Then provide the type of this class as an argument to the `Property` attribute. This works around the constraint that [`Attribute` parameters must be a constant.](https://stackoverflow.com/a/33007272) +#### `AutoGenConfig` and `AutoGenConfigArgs` +--- +The default generator used to create arguments is `GenX.defaults`. To specify different generators: +* Create a class with a single static property or method that returns an instance of `AutoGenConfig`. +* Provide the type of this class as an argument to the `Property` attribute. This works around the constraint that [`Attribute` parameters must be a constant.](https://stackoverflow.com/a/33007272) + + ```f# + type AutoGenConfigContainer = + static member __ = + GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) + + [)>] + let ``This test passes`` (i: int) = + i = 13 + ``` -```f# -type AutoGenConfigContainer = - static member __ = - GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) +* If the method takes arguments, you must provide them using `AutoGenConfigArgs`. -[)>] -let ``This test passes`` (i: int) = - i = 13 -``` + ```f# + type ConfigWithArgs = + static member __ a b = + GenX.defaults + |> AutoGenConfig.addGenerator (Gen.constant a) + |> AutoGenConfig.addGenerator (Gen.constant b) -If the method takes arguments, you must provide them using `AutoGenConfigArgs`. - -```f# -type ConfigWithArgs = - static member __ a b = - GenX.defaults - |> AutoGenConfig.addGenerator (Gen.constant a) - |> AutoGenConfig.addGenerator (Gen.constant b) - -[, AutoGenConfigArgs = [|"foo"; 13|])>] -let ``This also passes`` s i = - s = "foo" && i = 13 -``` + [, AutoGenConfigArgs = [|"foo"; 13|])>] + let ``This also passes`` s i = + s = "foo" && i = 13 + ``` #### `Tests` (count) - +--- Specifies the number of tests to be run, though more or less may occur due to shrinking or early failure. ```f# @@ -166,7 +181,7 @@ let ``This runs 3 times`` () = ``` #### `Shrinks` (count) - +--- Specifies the maximal number of shrinks that may run. ```f# @@ -175,8 +190,8 @@ let ``No shrinks occur`` i = if i > 50 then failwith "oops" ``` -#### `Size` - +##### `Size` +--- Sets the `Size` to a value for all runs. ```f# @@ -185,9 +200,9 @@ let ``"i" mostly ranges between -1 and 1`` i = printfn "%i" i ``` -### `Properties` attribute - -This optional attribute can decorate modules or classes. It sets default arguments for `AutoGenConfig`, `AutoGenConfigArgs`, `Tests`, `Shrinks`, and `Size`. These will be overridden by any arguments provided by the `Property` attribute. +#### Properties attribute +--- +This optional attribute can decorate modules or classes. It sets default arguments for [`AutoGenConfig`, `AutoGenConfigArgs`](#autogenconfig-and-autogenconfigargs), [`Tests`](#tests-count), [`Shrinks`](#shrinks-count), and [`Size`](#size). These will be overridden by any arguments provided by the `Property` attribute. ```f# type Int13 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) @@ -205,9 +220,9 @@ module ``Module with tests`` = i = 2718 ``` -### `Recheck` attribute - -This optional method attribute invokes `Property.recheck` with the given `Size` and `Seed`. It must be used with `Property`. +### Recheck attribute +--- +This optional method attribute invokes `Property.recheck` with the given `Size` and `Seed`, it must be used with `Property`. ```f# [] @@ -216,6 +231,66 @@ let ``this passes`` i = i = 12345 ``` +### ParameterGeneratorBaseType +--- +This is the base type of an attribute that can applied property function arguments, it allows you to provide a generator on a parameter by parameter basis. Lets look at the following property: + +```F# +open Xunit +open Hedgehog + +let positiveInt() = Range.constant 0 System.Int32.MaxValue |> Gen.int32 +let negativeInt() = Range.constant System.Int32.MinValue 0 |> Gen.int32 + +[] +let ``Positive + Negative <= Positive`` () = + property { + let! positive = positiveInt() + let! negative = negativeInt() + return positive + negative <= positive + } |> Property.checkBool +``` + +Using a `Property` attribute on this turns out to be quite tricky as we require two different generators for the same type, we may end up with something like this. + +```F# +type PositiveInt = {value : int} +type NegativeInt = {value : int} + +let positiveInt() = positiveInt() |> Gen.map(fun x-> {PositiveInt.value=x}) +let negativeInt() = negativeInt() |> Gen.map(fun x -> {NegativeInt.value=x}) + +type AutoGenConfigContainer = + static member __ = + GenX.defaults + |> AutoGenConfig.addGenerator (positiveInt()) + |> AutoGenConfig.addGenerator (negativeInt()) + +[)>] +let ``Positive + Negative <= Positive`` (positive:PositiveInt) (negative:NegativeInt) = + positive.value + negative.value <= positive.value +``` + + Using the `ParameterGeneratorBaseType` attribute is would look like this: + + ```F# + type Posint() = + inherit ParameterGeneratorBaseType() + override this.Generator = positiveInt() + +type NegInt() = + inherit ParameterGeneratorBaseType() + override this.Generator = negativeInt() + +[] +let ``Positive + Negative <= Positive xunit`` ([] positive) ([] negative) = + positive + negative <= positive +``` + + + + + ## Tips Use named arguments to select the desired constructor overload. diff --git a/src/Hedgehog.Xunit/Attributes.fs b/src/Hedgehog.Xunit/Attributes.fs index 7328cd8..ef83f40 100644 --- a/src/Hedgehog.Xunit/Attributes.fs +++ b/src/Hedgehog.Xunit/Attributes.fs @@ -105,7 +105,7 @@ type RecheckAttribute(recheckData) = [] [] -type ParameterGeneraterBaseType<'a>() = +type ParameterGeneratorBaseType<'a>() = inherit Attribute() abstract member Generator : Gen<'a> diff --git a/src/Hedgehog.Xunit/InternalLogic.fs b/src/Hedgehog.Xunit/InternalLogic.fs index 18bdf30..10e9e59 100644 --- a/src/Hedgehog.Xunit/InternalLogic.fs +++ b/src/Hedgehog.Xunit/InternalLogic.fs @@ -164,7 +164,7 @@ let report (testMethod:MethodInfo) testClass testClassInstance = attributes |> List.tryPick(fun x -> let attType = x.GetType().BaseType - if attType.IsGenericType && attType.GetGenericTypeDefinition().IsAssignableFrom(typedefof>) then + if attType.IsGenericType && attType.GetGenericTypeDefinition().IsAssignableFrom(typedefof>) then let method = attType.GetMethods() |> Array.pick(fun x -> if x.Name = "Box" then Some x else None) method.Invoke(x, null) :?> Gen |> Some else diff --git a/tests/Hedgehog.Xunit.Tests/PropertyTests.fs b/tests/Hedgehog.Xunit.Tests/PropertyTests.fs index 73af2a5..39378f6 100644 --- a/tests/Hedgehog.Xunit.Tests/PropertyTests.fs +++ b/tests/Hedgehog.Xunit.Tests/PropertyTests.fs @@ -12,15 +12,15 @@ open Common type Int13 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13) type Int5() = - inherit ParameterGeneraterBaseType() + inherit ParameterGeneratorBaseType() override this.Generator = Gen.constant 5 type Int6() = - inherit ParameterGeneraterBaseType() + inherit ParameterGeneratorBaseType() override this.Generator = Gen.constant 6 type IntCRange(max:int, min:int)= - inherit ParameterGeneraterBaseType() + inherit ParameterGeneratorBaseType() override this.Generator = (Range.constant max min) |> Gen.int32