Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 0.7.1 (2025-05-12)

* `AutoGenConfig`s are automatically merged when using both `Properties` and `Property`.

### 0.7.0 (2025-05-01)

* Updated to .NET 8
Expand Down
32 changes: 19 additions & 13 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,19 +213,25 @@ let ``"i" mostly ranges between -1 and 1`` i =
This optional attribute can decorate modules or classes. It sets default arguments for [`AutoGenConfig`, `AutoGenConfigArgs`](#-autogenconfig-and-autogenconfigargs), [`Tests`](#-tests), [`Shrinks`](#-shrinks), and [`Size`](#-size). These will be overridden by any explicit arguments on `[<Property>]`.

```f#
type Int13 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13)
type Int2718 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 2718)

[<Properties(typeof<Int13>, 1<tests>)>]
module ``Module with <Properties> tests`` =

[<Property>]
let ``this passes and runs once`` (i: int) =
i = 13

[<Property(typeof<Int2718>, 2<tests>)>]
let ``this passes and runs twice`` (i: int) =
i = 2718
type Int13A = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13 ) |> AutoGenConfig.addGenerator (Gen.constant "A")
type Int2718 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 2718)
type Int2718Minimal = static member __ = AutoGenConfig.defaults |> AutoGenConfig.addGenerator (Gen.constant 2718)

[<Properties(typeof<Int13A>, 1<tests>)>]
module ``Module with <Properties> tests`` =

[<Property>]
let ``this passes and runs once`` (i: int) =
i = 13

[<Property(typeof<Int2718>, 2<tests>)>]
let ``this passes and runs twice`` (i: int) =
i = 2718

[<Property(typeof<Int2718Minimal>)>]
let ``the `Int13A` and `Int2718Minimal` configs are merged, so this passes`` (i: int, s: string) =
// `<Property>` uses the "minimal" `AutoGenConfig.defaults`. Using `GenX.defaults` would override the `A` in `Int13A`.
i = 2718 && s = "A"
```

### 🔖 `GenAttribute`
Expand Down
2 changes: 1 addition & 1 deletion src/Hedgehog.Xunit/Hedgehog.Xunit.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageProjectUrl>https://github.com/hedgehogqa/fsharp-hedgehog-xunit</PackageProjectUrl>
<PackageTags>f# fsharp testing xunit</PackageTags>
<PackageIcon>hedgehog-logo.png</PackageIcon>
<Version>0.7.0</Version>
<Version>0.7.1</Version>
<PackageId>Hedgehog.Xunit</PackageId>
<PackageDescription>
Hedgehog with convenience attributes for xUnit.
Expand Down
71 changes: 39 additions & 32 deletions src/Hedgehog.Xunit/InternalLogic.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ let private genxAutoBoxWith<'T> x = x |> GenX.autoWith<'T> |> Gen.map box
let private genxAutoBoxWithMethodInfo =
typeof<Marker>.DeclaringType.GetTypeInfo().GetDeclaredMethod "genxAutoBoxWith"

let config (configType: Type) (configArgs: Option<obj array>) =
let configArgs = configArgs |> Option.defaultValue [||]
configType.GetMethods()
|> Seq.filter (fun p ->
p.IsStatic &&
p.ReturnType = typeof<AutoGenConfig>
) |> seqTryExactlyOne
|> Option.requireSome (sprintf "%s must have exactly one public static property that returns an AutoGenConfig.

An example type definition:

type %s =
static member __ =
GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13)
" configType.FullName configType.Name)
|> fun methodInfo ->
let methodInfo =
if methodInfo.IsGenericMethod then
methodInfo.GetParameters()
|> Array.map (fun p -> p.ParameterType.IsGenericParameter)
|> Array.zip configArgs
|> Array.filter snd
|> Array.map (fun (arg, _) -> arg.GetType())
|> fun argTypes -> methodInfo.MakeGenericMethod argTypes
else methodInfo
methodInfo.Invoke(null, configArgs)
:?> AutoGenConfig

let parseAttributes (testMethod:MethodInfo) (testClass:Type) =
let classAutoGenConfig, classAutoGenConfigArgs, classTests, classShrinks, classSize =
testClass.GetCustomAttributes(typeof<PropertiesAttribute>)
Expand All @@ -45,14 +73,22 @@ let parseAttributes (testMethod:MethodInfo) (testClass:Type) =
|> function
| Some x -> x.GetAutoGenConfig, x.GetAutoGenConfigArgs, x.GetTests, x.GetShrinks, x.GetSize
| None -> None , None , None , None , None
let configType, configArgs, tests, shrinks, size =
let config, tests, shrinks, size =
typeof<PropertyAttribute>
|> testMethod.GetCustomAttributes
|> Seq.exactlyOne
:?> PropertyAttribute
|> fun methodAttribute ->
methodAttribute.GetAutoGenConfig ++ classAutoGenConfig ,
methodAttribute.GetAutoGenConfigArgs ++ classAutoGenConfigArgs |> Option.defaultValue [||],
let config =
match classAutoGenConfig, methodAttribute.GetAutoGenConfig with
| Some c, Some m ->
let methodConfig = config m methodAttribute.GetAutoGenConfigArgs
let classConfig = config c classAutoGenConfigArgs
AutoGenConfig.merge classConfig methodConfig
| Some c, None -> config c classAutoGenConfigArgs
| None , Some m -> config m methodAttribute.GetAutoGenConfigArgs
| None , None -> GenX.defaults
config,
methodAttribute.GetTests ++ classTests ,
methodAttribute.GetShrinks ++ classShrinks ,
methodAttribute.GetSize ++ classSize
Expand All @@ -65,35 +101,6 @@ let parseAttributes (testMethod:MethodInfo) (testClass:Type) =
:?> RecheckAttribute
|> fun x -> x.GetRecheckData
)
let config =
match configType with
| None -> GenX.defaults
| Some t ->
t.GetMethods()
|> Seq.filter (fun p ->
p.IsStatic &&
p.ReturnType = typeof<AutoGenConfig>
) |> seqTryExactlyOne
|> Option.requireSome (sprintf "%s must have exactly one public static property that returns an AutoGenConfig.

An example type definition:

type %s =
static member __ =
GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13)
" t.FullName t.Name)
|> fun methodInfo ->
let methodInfo =
if methodInfo.IsGenericMethod then
methodInfo.GetParameters()
|> Array.map (fun p -> p.ParameterType.IsGenericParameter)
|> Array.zip configArgs
|> Array.filter snd
|> Array.map (fun (arg, _) -> arg.GetType())
|> fun argTypes -> methodInfo.MakeGenericMethod argTypes
else methodInfo
methodInfo.Invoke(null, configArgs)
:?> AutoGenConfig
config, tests, shrinks, recheck, size

let resultIsOk r =
Expand Down
21 changes: 19 additions & 2 deletions tests/Hedgehog.Xunit.Tests.FSharp/PropertyTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Int6() =
inherit GenAttribute<int>()
override _.Generator = Gen.constant 6

type IntConstantRange(max: int, min: int)=
type IntConstantRange(max: int, min: int) =
inherit GenAttribute<int>()
override _.Generator = Range.constant max min |> Gen.int32

Expand Down Expand Up @@ -246,7 +246,7 @@ type NonAutoGenConfig =
GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 13)
", e.Message)

type Int2718 = static member __ = GenX.defaults |> AutoGenConfig.addGenerator (Gen.constant 2718)
type Int2718 = static member __ = AutoGenConfig.defaults |> AutoGenConfig.addGenerator (Gen.constant 2718)

[<Properties(typeof<Int13>, 200<tests>)>]
module ``Module with <Properties> tests`` =
Expand Down Expand Up @@ -781,3 +781,20 @@ module ``GenAttribute with Properties Tests`` =
[<Property(typeof<Int13>)>]
let ``overrides Properties' and Property's autoGenConfig`` ([<Int5>] i) =
Assert.StrictEqual(5, i)

type Int13A =
static member __ =
AutoGenConfig.defaults
|> AutoGenConfig.addGenerator (Gen.constant 13)
|> AutoGenConfig.addGenerator (Gen.constant "A")

[<Properties(typeof<Int13A>)>]
module ``Module with <Properties(typeof<Int13A>)>`` =

[<Property>]
let ``Module's <Properties> works`` (i, s) =
i = 13 && s = "A"

[<Property(typeof<Int2718>)>]
let ``Module's <Properties> merges with Method level <Property>`` (i, s) =
i = 2718 && s = "A"