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

Commit d58d284

Browse files
authored
Merge pull request #83 from hedgehogqa/refactor-generators
Refactor generators
2 parents 229926a + 93a9ab6 commit d58d284

17 files changed

Lines changed: 924 additions & 360 deletions

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -208,23 +208,29 @@ let! myVal =
208208
**Register generators for generic types in `AutoGenConfig`:**
209209

210210
```f#
211-
// An example of a generic type
212-
type Maybe<'a> = Just of 'a | Nothing
213-
214211
// a type containing generators for generic types
215-
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenConfig as parameters
212+
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenContext as parameters
216213
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-
]
214+
215+
// Generate generic types
216+
static member MyGenericType<'a>(valueGen : Gen<'a>) : Gen<MyGenericType<'a>> =
217+
valueGen | Gen.map (fun x -> MyGenericType(x))
223218
224-
let! myVal =
219+
// Generate generic types with recursion support and access to AutoGenContext
220+
static member ImmutableList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableList<'a>> =
221+
if context.CanRecurse then
222+
valueGen |> Gen.list context.CollectionRange |> Gen.map ImmutableList.CreateRange
223+
else
224+
Gen.constant ImmutableList<'a>.Empty
225+
226+
// register the generic generators in AutoGenConfig
227+
let config =
225228
GenX.defaults
226229
|> AutoGenConfig.addGenerators<GenericGenerators>
227-
|> GenX.autoWith<Maybe<int>>
230+
231+
// use the config to auto-generate types containing generic types
232+
let! myGenericType = GenX.autoWith<MyGenericTypes> config
233+
let! myImmutableList = GenX.autoWith<ImmutableList<int>> config
228234
```
229235

230236
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.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using static Hedgehog.Linq.Property;
5+
using Xunit;
6+
7+
namespace Hedgehog.Linq.Tests;
8+
9+
public sealed class DefaultGeneratorsTests
10+
{
11+
private readonly AutoGenConfig _config = GenX.defaults.WithCollectionRange(Range.FromValue(5));
12+
13+
[Fact]
14+
public void ShouldGenerateImmutableSet() =>
15+
ForAll(GenX.autoWith<ImmutableHashSet<int>>(_config)).Select(x => x.Count > 0).Check();
16+
17+
[Fact]
18+
public void ShouldGenerateIImmutableSet() =>
19+
ForAll(GenX.autoWith<IImmutableSet<int>>(_config)).Select(x => x.Count > 0).Check();
20+
21+
[Fact]
22+
public void ShouldGenerateImmutableSortedSet() =>
23+
ForAll(GenX.autoWith<ImmutableSortedSet<int>>(_config)).Select(x => x.Count > 0).Check();
24+
25+
[Fact]
26+
public void ShouldGenerateImmutableList() =>
27+
ForAll(GenX.autoWith<ImmutableList<int>>(_config)).Select(x => x.Count == 5).Check();
28+
29+
[Fact]
30+
public void ShouldGenerateIImmutableList() =>
31+
ForAll(GenX.autoWith<IImmutableList<int>>(_config)).Select(x => x.Count == 5).Check();
32+
33+
[Fact]
34+
public void ShouldGenerateImmutableArray() =>
35+
ForAll(GenX.autoWith<ImmutableArray<int>>(_config)).Select(x => x.Length == 5).Check();
36+
37+
[Fact]
38+
public void ShouldGenerateDictionary() =>
39+
ForAll(GenX.autoWith<Dictionary<int, string>>(_config)).Select(x => x.Count > 0).Check();
40+
41+
[Fact]
42+
public void ShouldGenerateIDictionary() =>
43+
ForAll(GenX.autoWith<IDictionary<int, string>>(_config)).Select(x => x.Count > 0).Check();
44+
45+
[Fact]
46+
public void ShouldGenerateIReadOnlyDictionary() =>
47+
ForAll(GenX.autoWith<IReadOnlyDictionary<int, string>>(_config)).Select(x => x.Count > 0).Check();
48+
49+
[Fact]
50+
public void ShouldGenerateList() =>
51+
ForAll(GenX.autoWith<List<int>>(_config)).Select(x => x.Count == 5).Check();
52+
53+
[Fact]
54+
public void ShouldGenerateIList() =>
55+
ForAll(GenX.autoWith<IList<int>>(_config)).Select(x => x.Count == 5).Check();
56+
57+
[Fact]
58+
public void ShouldGenerateIReadOnlyList() =>
59+
ForAll(GenX.autoWith<IReadOnlyList<int>>(_config)).Select(x => x.Count == 5).Check();
60+
61+
[Fact]
62+
public void ShouldGenerateIEnumerable() =>
63+
ForAll(GenX.autoWith<IEnumerable<int>>(_config)).Select(x => x.Count() == 5).Check();
64+
65+
[Fact]
66+
public void StressTest() =>
67+
ForAll(GenX.autoWith<List<List<List<int>>>>(_config))
68+
.Select(x => x.Count == 5 && x.All(inner => inner.Count == 5 && inner.All(innerMost => innerMost.Count == 5)))
69+
.Check();
70+
71+
[Fact]
72+
public void ShouldGenerateRecursiveTreeWithImmutableList()
73+
{
74+
// Tree node with ImmutableList of children - tests recursive generation with generic types
75+
var config = GenX.defaults
76+
.WithCollectionRange(Range.FromValue(2))
77+
.WithRecursionDepth(1);
78+
79+
ForAll(GenX.autoWith<TreeNode<int>>(config))
80+
.Select(tree =>
81+
{
82+
// At depth 1, should have children
83+
// At depth 2, children's children should be empty (recursion limit)
84+
return tree.Children.Count == 2 &&
85+
tree.Children.All(child => child.Children.Count == 0);
86+
})
87+
.Check();
88+
}
89+
}
90+
91+
// Recursive data structure for testing
92+
public record TreeNode<T>
93+
{
94+
public T Value { get; init; }
95+
public List<TreeNode<T>> Children { get; init; } = [];
96+
97+
public override string ToString()
98+
{
99+
if (Children.Count == 0)
100+
return $"Node({Value})";
101+
102+
var childrenStr = string.Join(", ", Children.Select(c => c.ToString()));
103+
return $"Node({Value}, [{childrenStr}])";
104+
}
105+
}

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public sealed class OuterClass
3434
public Maybe<Guid> Value { get; set; }
3535
}
3636

37+
public sealed record RecursiveRec(Maybe<RecursiveRec> Value);
38+
3739
public sealed class GenericTestGenerators
3840
{
3941
public static Gen<Guid> Guid() =>
@@ -51,24 +53,30 @@ public static Gen<Uuid> UuidGen() =>
5153
public static Gen<Name> NameGen(Gen<string> gen) =>
5254
gen.Select(value => new Name("Name: " + value));
5355

54-
public static Gen<Maybe<T>> AlwaysJust<T>(Gen<T> gen) =>
55-
gen.Select(Maybe<T> (value) => new Maybe<T>.Just(value));
56+
public static Gen<Maybe<T>> AlwaysJust<T>(AutoGenContext context, Gen<T> gen) =>
57+
context.CanRecurse
58+
? gen.Select(Maybe<T> (value) => new Maybe<T>.Just(value))
59+
: Gen.FromValue<Maybe<T>>(new Maybe<T>.Nothing());
5660

5761
public static Gen<Either<TLeft, TRight>> AlwaysLeft<TLeft, TRight>(Gen<TRight> genB, Gen<TLeft> genA) =>
5862
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);
6563
}
6664

6765
public class GenericGenTests
6866
{
6967
private static bool IsCustomGuid(Guid guid) =>
7068
new Span<byte>(guid.ToByteArray(), 0, 4).ToArray().All(b => b == 0);
7169

70+
[Fact]
71+
public void ShouldGenerateRecursiveRecords()
72+
{
73+
var config = GenX.defaults.WithGenerators<GenericTestGenerators>();
74+
var prop = from x in ForAll(GenX.autoWith<RecursiveRec>(config))
75+
select x != null;
76+
77+
prop.Check();
78+
}
79+
7280
[Fact]
7381
public void ShouldGenerateValueWithPhantomGenericType_Id()
7482
{
@@ -151,8 +159,6 @@ public void ShouldGenerateImmutableListUsingAutoGenConfigParameter()
151159
.WithCollectionRange(Range.FromValue(7))
152160
.WithGenerators<GenericTestGenerators>();
153161

154-
// The ImmutableListGen<int> will be called with config and Gen<int>
155-
// This demonstrates that generators can receive AutoGenConfig to access configuration
156162
var prop = from x in ForAll(GenX.autoWith<ImmutableList<int>>(config))
157163
select x.Count == 7;
158164

src/Hedgehog.Experimental.Tests/AutoGenConfigTests.fs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Hedgehog.Experimental.Tests.AutoGenConfigTests
22

3+
open Hedgehog.Experimental
34
open Xunit
45
open Swensen.Unquote
56
open Hedgehog
@@ -78,11 +79,9 @@ let ``addGenerators supports methods with AutoGenConfig parameter``() =
7879
open System.Collections.Immutable
7980

8081
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-
}
82+
static member ImmutableListGen<'T>(config: AutoGenConfig, genItem: Gen<'T>) : Gen<ImmutableList<'T>> =
83+
genItem |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map ImmutableList.CreateRange
84+
8685

8786
[<Fact>]
8887
let ``addGenerators supports generic methods with AutoGenConfig and Gen parameters``() =
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module Hedgehog.Experimental.Tests.AutoGenContextTests
2+
3+
open Hedgehog
4+
open Xunit
5+
open Swensen.Unquote
6+
7+
type RecursiveType<'a> =
8+
{ Value: Option<RecursiveType<'a>>}
9+
member this.Depth =
10+
match this.Value with
11+
| None -> 0
12+
| Some x -> x.Depth + 1
13+
14+
type RecursiveGenerators =
15+
// override Option to always generate Some when recursion is allowed
16+
// using the AutoGenContext to assert recursion context preservation
17+
static member Option<'a>(context: AutoGenContext) =
18+
if context.CanRecurse then
19+
printfn "CurrentRecursionDepth: %d" context.CurrentRecursionDepth
20+
context.AutoGenerate<'a>() |> Gen.map Some
21+
else
22+
Gen.constant None
23+
24+
[<Fact>]
25+
let ``Should preserve recursion with generic types when using AutoGenContext.AutoGenerate``() =
26+
property {
27+
let! recDepth = Gen.int32 (Range.constant 2 5)
28+
let config =
29+
GenX.defaults
30+
|> AutoGenConfig.addGenerators<RecursiveGenerators>
31+
|> AutoGenConfig.setRecursionDepth recDepth
32+
33+
let! result = GenX.autoWith<RecursiveType<int>> config
34+
test <@ result.Depth = recDepth @>
35+
} |> Property.check
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
module ComplexGenericTest
2+
3+
open Xunit
4+
open Swensen.Unquote
5+
open Hedgehog
6+
7+
// A type with complex generic parameter repetition: <A, A, B, C, A, A, D>
8+
type ComplexType<'A, 'B, 'C, 'D, 'E, 'F, 'G> = {
9+
First: 'A
10+
Second: 'B
11+
Third: 'C
12+
Fifth: 'E
13+
Fourth: 'D
14+
Sixth: 'F
15+
Seventh: 'G
16+
}
17+
18+
type ComplexGenerators =
19+
// Method with pattern: method has <A, B, C, D> but type uses <A, A, B, C, A, A, D>
20+
static member Complex<'A, 'B, 'C, 'D>(
21+
genA: Gen<'A>,
22+
genC: Gen<'C>,
23+
genB: Gen<'B>,
24+
genD: Gen<'D>) : Gen<ComplexType<'A, 'A, 'B, 'C, 'A, 'A, 'D>> =
25+
gen {
26+
let! a = genA
27+
let! b = genB
28+
let! c = genC
29+
let! d = genD
30+
return {
31+
First = a
32+
Second = a
33+
Third = b
34+
Fourth = c
35+
Fifth = a
36+
Sixth = a
37+
Seventh = d
38+
}
39+
}
40+
41+
[<Fact>]
42+
let ``Should handle complex generic parameter repetition pattern``() =
43+
let config =
44+
GenX.defaults
45+
|> AutoGenConfig.addGenerators<ComplexGenerators>
46+
47+
// Generate ComplexType<int, int, string, bool, int, int, float>
48+
// Method is Complex<int, string, bool, float>
49+
let gen = GenX.autoWith<ComplexType<int, int, string, bool, int, int, float>> config
50+
let sample = Gen.sample 0 1 gen |> Seq.head
51+
52+
// Verify the structure is correct
53+
test <@ sample.First = sample.Second @> // Both should be the same 'A value
54+
test <@ sample.First = sample.Fifth @> // All 'A positions should be the same
55+
test <@ sample.Second = sample.Sixth @>
56+
test <@ sample.Third.GetType() = typeof<string> @>
57+
test <@ sample.Fourth.GetType() = typeof<bool> @>
58+
test <@ sample.Seventh.GetType() = typeof<float> @>
59+
60+
// Better test with specific verifiable values
61+
type VerifiableGenerators =
62+
static member VerifiableComplex<'A, 'B, 'C, 'D>(
63+
genA: Gen<'A>,
64+
genC: Gen<'C>,
65+
genB: Gen<'B>,
66+
genD: Gen<'D>) : Gen<ComplexType<'A, 'A, 'B, 'C, 'A, 'A, 'D>> =
67+
gen {
68+
let! a = genA
69+
let! b = genB
70+
let! c = genC
71+
let! d = genD
72+
return {
73+
First = a
74+
Second = a
75+
Third = b
76+
Fourth = c
77+
Fifth = a
78+
Sixth = a
79+
Seventh = d
80+
}
81+
}
82+
83+
// Specific constant generators to verify correct parameter mapping
84+
type SpecificGenerators =
85+
static member Int() = Gen.constant 42
86+
static member String() = Gen.constant "test"
87+
static member Bool() = Gen.constant true
88+
static member Float() = Gen.constant 3.14
89+
90+
[<Fact>]
91+
let ``Should map parameters correctly with swapped parameter order``() =
92+
let config =
93+
GenX.defaults
94+
|> AutoGenConfig.addGenerators<SpecificGenerators>
95+
|> AutoGenConfig.addGenerators<VerifiableGenerators>
96+
97+
let gen = GenX.autoWith<ComplexType<int, int, string, bool, int, int, float>> config
98+
let sample = Gen.sample 0 1 gen |> Seq.head
99+
100+
// With swapped parameters (genA, genC, genB, genD), the mapping should be:
101+
// 'A -> int (42) goes to positions: First, Second, Fifth, Sixth
102+
// 'B -> string ("test") goes to position: Third
103+
// 'C -> bool (true) goes to position: Fourth
104+
// 'D -> float (3.14) goes to position: Seventh
105+
106+
test <@ sample.First = 42 @>
107+
test <@ sample.Second = 42 @>
108+
test <@ sample.Third = "test" @>
109+
test <@ sample.Fourth = true @>
110+
test <@ sample.Fifth = 42 @>
111+
test <@ sample.Sixth = 42 @>
112+
test <@ sample.Seventh = 3.14 @>

0 commit comments

Comments
 (0)