Skip to content
This repository was archived by the owner on Nov 27, 2025. It is now read-only.

Commit 229926a

Browse files
authored
Merge pull request #82 from hedgehogqa/allow-autogen-config
Allow AutogenConfig to be received as a parameter for generic generators
2 parents 5f94344 + 321e142 commit 229926a

File tree

8 files changed

+153
-25
lines changed

8 files changed

+153
-25
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ Thumbs.db
5151
paket-files
5252

5353
# Visual Studio cache/options directory
54-
.vs/
54+
.vs/
55+
.idea/

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,28 @@ let! myVal =
205205
|> GenX.autoWith<MyType>
206206
```
207207

208+
**Register generators for generic types in `AutoGenConfig`:**
209+
210+
```f#
211+
// An example of a generic type
212+
type Maybe<'a> = Just of 'a | Nothing
213+
214+
// a type containing generators for generic types
215+
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenConfig as parameters
216+
type GenericGenerators =
217+
// Generator for Maybe<'a>
218+
static member MaybeGen<'a>(valueGen : Gen<'a>) : Gen<Maybe<'a>> =
219+
Gen.frequency [
220+
1, Gen.constant None
221+
8, valueGen
222+
]
223+
224+
let! myVal =
225+
GenX.defaults
226+
|> AutoGenConfig.addGenerators<GenericGenerators>
227+
|> GenX.autoWith<Maybe<int>>
228+
```
229+
208230
If you’re not happy with the auto-gen defaults, you can of course create your own generator that calls `GenX.autoWith` with your chosen config and use that everywhere.
209231

210232
Deployment checklist

src/Hedgehog.Experimental.CSharp.Tests/GenericGenTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Linq;
22
using System;
3+
using System.Collections.Immutable;
34
using Xunit;
45
using static Hedgehog.Linq.Property;
56

@@ -55,6 +56,12 @@ public static Gen<Maybe<T>> AlwaysJust<T>(Gen<T> gen) =>
5556

5657
public static Gen<Either<TLeft, TRight>> AlwaysLeft<TLeft, TRight>(Gen<TRight> genB, Gen<TLeft> genA) =>
5758
genA.Select(Either<TLeft, TRight> (value) => new Either<TLeft, TRight>.Left(value));
59+
60+
// Generator for ImmutableList<T> that uses AutoGenConfig's seqRange
61+
public static Gen<ImmutableList<T>> ImmutableListGen<T>(AutoGenConfig config, Gen<T> genItem) =>
62+
genItem
63+
.List(config.GetCollectionRange())
64+
.Select(ImmutableList.CreateRange);
5865
}
5966

6067
public class GenericGenTests
@@ -136,4 +143,25 @@ public void ShouldGenerateOuterClassWithGenericTypeInside()
136143
};
137144
prop.Check();
138145
}
146+
147+
[Fact]
148+
public void ShouldGenerateImmutableListUsingAutoGenConfigParameter()
149+
{
150+
var config = GenX.defaults
151+
.WithCollectionRange(Range.FromValue(7))
152+
.WithGenerators<GenericTestGenerators>();
153+
154+
// The ImmutableListGen<int> will be called with config and Gen<int>
155+
// This demonstrates that generators can receive AutoGenConfig to access configuration
156+
var prop = from x in ForAll(GenX.autoWith<ImmutableList<int>>(config))
157+
select x.Count == 7;
158+
159+
prop.Check();
160+
161+
// Also test with a different type to verify generics work
162+
var propString = from x in ForAll(GenX.autoWith<ImmutableList<string>>(config))
163+
select x.Count == 7;
164+
165+
propString.Check();
166+
}
139167
}

src/Hedgehog.Experimental.Tests/AutoGenConfigTests.fs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,59 @@ let ``merging AutoGenConfig overrides values``() =
4545
}
4646

4747
Property.check property
48+
49+
type CustomType = { Value: int; Items: string list }
50+
51+
type CustomGenerators =
52+
// Generator that uses AutoGenConfig to access seqRange
53+
static member CustomTypeGen(config: AutoGenConfig) : Gen<CustomType> = gen {
54+
let! value = Gen.int32 (Range.exponentialBounded())
55+
let! items = Gen.string (Range.linear 1 5) Gen.alpha |> Gen.list (AutoGenConfig.seqRange config)
56+
return { Value = value; Items = items }
57+
}
58+
59+
// Generator that takes both AutoGenConfig and Gen<_> parameters
60+
static member CustomTypeWithGen(config: AutoGenConfig, genValue: Gen<int>) : Gen<CustomType> = gen {
61+
let! value = genValue
62+
let! items = Gen.string (Range.linear 1 5) Gen.alpha |> Gen.list (AutoGenConfig.seqRange config)
63+
return { Value = value; Items = items }
64+
}
65+
66+
[<Fact>]
67+
let ``addGenerators supports methods with AutoGenConfig parameter``() =
68+
let customConfig =
69+
GenX.defaults
70+
|> AutoGenConfig.setSeqRange (Range.constant 3 3)
71+
|> AutoGenConfig.addGenerators<CustomGenerators>
72+
73+
// Create the generator and sample a value to ensure it works
74+
let gen = customConfig |> GenX.autoWith<CustomType>
75+
let sample = Gen.sample 0 1 gen |> Seq.head
76+
test <@ sample.Items.Length = 3 @>
77+
78+
open System.Collections.Immutable
79+
80+
type ImmutableListGenerators =
81+
// Generic generator for ImmutableList<T> that uses AutoGenConfig's seqRange
82+
static member ImmutableListGen<'T>(config: AutoGenConfig, genItem: Gen<'T>) : Gen<ImmutableList<'T>> = gen {
83+
let! items = genItem |> Gen.list (AutoGenConfig.seqRange config)
84+
return items |> ImmutableList.CreateRange
85+
}
86+
87+
[<Fact>]
88+
let ``addGenerators supports generic methods with AutoGenConfig and Gen parameters``() =
89+
let customConfig =
90+
GenX.defaults
91+
|> AutoGenConfig.setSeqRange (Range.constant 5 5)
92+
|> AutoGenConfig.addGenerators<ImmutableListGenerators>
93+
94+
// The ImmutableListGen<int> will be called with config and Gen<int>
95+
// This demonstrates that generic generators work with both AutoGenConfig and Gen<T> parameters
96+
let gen = customConfig |> GenX.autoWith<ImmutableList<int>>
97+
let sample = Gen.sample 0 1 gen |> Seq.head
98+
test <@ sample.Count = 5 @>
99+
100+
// Also test with a different type to verify generics work
101+
let genString = customConfig |> GenX.autoWith<ImmutableList<string>>
102+
let sampleString = Gen.sample 0 1 genString |> Seq.head
103+
test <@ sampleString.Count = 5 @>

src/Hedgehog.Experimental/AutoGenConfig.fs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,36 @@ module AutoGenConfig =
4848
/// The type is expected to have static methods that return Gen<_>.
4949
/// These methods can have parameters which are required to be of type Gen<_>.
5050
let addGenerators<'a> (config: AutoGenConfig) =
51-
let isGen (t: Type) =
52-
t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Gen<_>>
51+
let getGenType (t: Type) =
52+
if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Gen<_>>
53+
then Some (t.GetGenericArguments().[0])
54+
else None
5355

54-
let tryUnwrapGenParameters (methodInfo: MethodInfo) : Option<Type[]> =
56+
let getAutoGenConfigType (t: Type) =
57+
if t = typeof<AutoGenConfig>
58+
then Some t
59+
else None
60+
61+
let tryUnwrapParameters (methodInfo: MethodInfo) : Option<Type[]> =
5562
methodInfo.GetParameters()
5663
|> Array.fold (fun acc param ->
57-
match acc, isGen param.ParameterType with
58-
| Some types, true ->
59-
Some (Array.append types [| param.ParameterType.GetGenericArguments().[0] |])
60-
| _ -> None
64+
match acc with
65+
| None -> None
66+
| Some types ->
67+
getGenType param.ParameterType
68+
|> Option.orElseWith (fun () -> getAutoGenConfigType param.ParameterType)
69+
|> Option.map (fun t -> Array.append types [| t |])
6170
) (Some [||])
6271

6372
typeof<'a>.GetMethods(BindingFlags.Static ||| BindingFlags.Public)
6473
|> Seq.choose (fun methodInfo ->
65-
match isGen methodInfo.ReturnType, tryUnwrapGenParameters methodInfo with
66-
| true, Some typeArray ->
67-
let targetType = methodInfo.ReturnType.GetGenericArguments().[0]
68-
let factory: Type[] -> obj[] -> obj = fun types gens ->
74+
match getGenType methodInfo.ReturnType, tryUnwrapParameters methodInfo with
75+
| Some targetType, Some typeArray ->
76+
let factory: Type[] -> obj[] -> obj = fun types args ->
6977
let methodToCall =
7078
if Array.isEmpty types then methodInfo
7179
else methodInfo.MakeGenericMethod(types)
72-
methodToCall.Invoke(null, gens)
80+
methodToCall.Invoke(null, args)
7381
Some (targetType, typeArray, factory)
7482
| _ -> None)
7583
|> Seq.fold (fun cfg (targetType, typeArray, factory) ->

src/Hedgehog.Experimental/GenExtensions.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ type GenExtraExtensions() =
1818
static member WithGenerators<'T>(self : AutoGenConfig) =
1919
self |> AutoGenConfig.addGenerators<'T>
2020

21+
[<Extension>]
22+
static member GetCollectionRange(self : AutoGenConfig) =
23+
AutoGenConfig.seqRange self
24+
2125
[<Extension>]
2226
static member WithCollectionRange(self : AutoGenConfig, range : Range<int>) =
2327
self |> AutoGenConfig.setSeqRange range

src/Hedgehog.Experimental/GenX.fs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ module GenX =
368368
array.SetValue(en.Current, currentIndices)
369369
| currentLength :: remainingLengths ->
370370
for i in 0..currentLength - 1 do
371-
currentIndices.[currentDimensionIndex] <- i
371+
currentIndices[currentDimensionIndex] <- i
372372
loop (currentDimensionIndex + 1) remainingLengths
373373
loop 0 lengths
374374
array
@@ -385,11 +385,15 @@ module GenX =
385385
let addGenMsg = "You can use 'GenX.defaults |> AutoGenConfig.addGenerator myGen |> GenX.autoWith' to generate types not inherently supported by GenX.auto."
386386
let unsupportedTypeException = NotSupportedException (sprintf "Unable to auto-generate %s. %s" typeof<'a>.FullName addGenMsg)
387387

388+
// Prevent auto-generating AutoGenConfig itself - it should only be passed as a parameter
389+
if typeof<'a> = typeof<AutoGenConfig> then
390+
raise (NotSupportedException "Cannot auto-generate AutoGenConfig type. It should be provided as a parameter to generator methods.")
391+
388392
let genPoco (shape: ShapePoco<'a>) =
389393
let bestCtor =
390394
shape.Constructors
391-
|> Seq.filter (fun c -> c.IsPublic)
392-
|> Seq.sortBy (fun c -> c.Arity)
395+
|> Seq.filter _.IsPublic
396+
|> Seq.sortBy _.Arity
393397
|> Seq.tryHead
394398

395399
match bestCtor with
@@ -458,10 +462,10 @@ module GenX =
458462
if arg.IsGenericParameter then
459463
typeArgs
460464
|> Array.tryFind (fun p -> p.argTypeDefinition.Name = arg.Name)
461-
|> Option.map (_.argType)
465+
|> Option.map _.argType
462466
|> Option.defaultWith (fun _ -> raise unsupportedTypeException)
463467
else arg)
464-
{| genericTypes = typeArgs |> Array.map (_.argType); argumentTypes = argTypes |}
468+
{| genericTypes = typeArgs |> Array.map _.argType; argumentTypes = argTypes |}
465469

466470
| _ -> {| genericTypes = Array.empty; argumentTypes = args |}
467471

@@ -470,10 +474,15 @@ module GenX =
470474
let targetArgs =
471475
factoryArgs.argumentTypes
472476
|> Array.map (fun t ->
473-
let ts = TypeShape.Create(t)
474-
ts.Accept { new ITypeVisitor<obj> with
475-
member __.Visit<'b> () = autoInner<'b> config recursionDepths |> box
476-
})
477+
// Check if this is AutoGenConfig type
478+
if t = typeof<AutoGenConfig> then
479+
box config
480+
else
481+
// Otherwise, generate a value for this type
482+
let ts = TypeShape.Create(t)
483+
ts.Accept { new ITypeVisitor<obj> with
484+
member __.Visit<'b> () = autoInner<'b> config recursionDepths |> box
485+
})
477486

478487
let resGen = factory factoryArgs.genericTypes targetArgs
479488
resGen |> unbox<Gen<'a>>
@@ -560,8 +569,8 @@ module GenX =
560569
|> ListGen.traverse memberSetterGenerator)
561570
gen {
562571
let! caseIdx = Gen.integral <| Range.constant 0 (cases.Length - 1)
563-
let! fs = cases.[caseIdx]
564-
return fs |> List.fold (|>) (shape.UnionCases.[caseIdx].CreateUninitialized ())
572+
let! fs = cases[caseIdx]
573+
return fs |> List.fold (|>) (shape.UnionCases[caseIdx].CreateUninitialized ())
565574
}
566575

567576
| Shape.Enum _ ->

src/Hedgehog.Experimental/GeneratorCollection.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ open System
44
open System.Collections.Immutable
55

66
// A generator factory which can be backed by a generic method.
7-
// It takes an array of genetic type parameters, and an array of arguments to create the generator.
7+
// It takes an array of genetic type parameters and an array of arguments to create the generator.
88
type private GeneratorFactory = Type[] -> obj[] -> obj
99

1010
[<Struct>]

0 commit comments

Comments
 (0)