Skip to content
This repository was archived by the owner on Nov 27, 2025. It is now read-only.
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
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,23 +208,29 @@ let! myVal =
**Register generators for generic types in `AutoGenConfig`:**

```f#
// An example of a generic type
type Maybe<'a> = Just of 'a | Nothing

// a type containing generators for generic types
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenConfig as parameters
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenContext as parameters
type GenericGenerators =
// Generator for Maybe<'a>
static member MaybeGen<'a>(valueGen : Gen<'a>) : Gen<Maybe<'a>> =
Gen.frequency [
1, Gen.constant None
8, valueGen
]

// Generate generic types
static member MyGenericType<'a>(valueGen : Gen<'a>) : Gen<MyGenericType<'a>> =
valueGen | Gen.map (fun x -> MyGenericType(x))

let! myVal =
// Generate generic types with recursion support and access to AutoGenContext
static member ImmutableList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableList<'a>> =
if context.CanRecurse then
valueGen |> Gen.list context.CollectionRange |> Gen.map ImmutableList.CreateRange
else
Gen.constant ImmutableList<'a>.Empty

// register the generic generators in AutoGenConfig
let config =
GenX.defaults
|> AutoGenConfig.addGenerators<GenericGenerators>
|> GenX.autoWith<Maybe<int>>

// use the config to auto-generate types containing generic types
let! myGenericType = GenX.autoWith<MyGenericTypes> config
let! myImmutableList = GenX.autoWith<ImmutableList<int>> config
```

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.
Expand Down
105 changes: 105 additions & 0 deletions src/Hedgehog.Experimental.CSharp.Tests/DefaultGeneratorsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using static Hedgehog.Linq.Property;
using Xunit;

namespace Hedgehog.Linq.Tests;

public sealed class DefaultGeneratorsTests
{
private readonly AutoGenConfig _config = GenX.defaults.WithCollectionRange(Range.FromValue(5));

[Fact]
public void ShouldGenerateImmutableSet() =>
ForAll(GenX.autoWith<ImmutableHashSet<int>>(_config)).Select(x => x.Count > 0).Check();

[Fact]
public void ShouldGenerateIImmutableSet() =>
ForAll(GenX.autoWith<IImmutableSet<int>>(_config)).Select(x => x.Count > 0).Check();

[Fact]
public void ShouldGenerateImmutableSortedSet() =>
ForAll(GenX.autoWith<ImmutableSortedSet<int>>(_config)).Select(x => x.Count > 0).Check();

[Fact]
public void ShouldGenerateImmutableList() =>
ForAll(GenX.autoWith<ImmutableList<int>>(_config)).Select(x => x.Count == 5).Check();

[Fact]
public void ShouldGenerateIImmutableList() =>
ForAll(GenX.autoWith<IImmutableList<int>>(_config)).Select(x => x.Count == 5).Check();

[Fact]
public void ShouldGenerateImmutableArray() =>
ForAll(GenX.autoWith<ImmutableArray<int>>(_config)).Select(x => x.Length == 5).Check();

[Fact]
public void ShouldGenerateDictionary() =>
ForAll(GenX.autoWith<Dictionary<int, string>>(_config)).Select(x => x.Count > 0).Check();

[Fact]
public void ShouldGenerateIDictionary() =>
ForAll(GenX.autoWith<IDictionary<int, string>>(_config)).Select(x => x.Count > 0).Check();

[Fact]
public void ShouldGenerateIReadOnlyDictionary() =>
ForAll(GenX.autoWith<IReadOnlyDictionary<int, string>>(_config)).Select(x => x.Count > 0).Check();

[Fact]
public void ShouldGenerateList() =>
ForAll(GenX.autoWith<List<int>>(_config)).Select(x => x.Count == 5).Check();

[Fact]
public void ShouldGenerateIList() =>
ForAll(GenX.autoWith<IList<int>>(_config)).Select(x => x.Count == 5).Check();

[Fact]
public void ShouldGenerateIReadOnlyList() =>
ForAll(GenX.autoWith<IReadOnlyList<int>>(_config)).Select(x => x.Count == 5).Check();

[Fact]
public void ShouldGenerateIEnumerable() =>
ForAll(GenX.autoWith<IEnumerable<int>>(_config)).Select(x => x.Count() == 5).Check();

[Fact]
public void StressTest() =>
ForAll(GenX.autoWith<List<List<List<int>>>>(_config))
.Select(x => x.Count == 5 && x.All(inner => inner.Count == 5 && inner.All(innerMost => innerMost.Count == 5)))
.Check();

[Fact]
public void ShouldGenerateRecursiveTreeWithImmutableList()
{
// Tree node with ImmutableList of children - tests recursive generation with generic types
var config = GenX.defaults
.WithCollectionRange(Range.FromValue(2))
.WithRecursionDepth(1);

ForAll(GenX.autoWith<TreeNode<int>>(config))
.Select(tree =>
{
// At depth 1, should have children
// At depth 2, children's children should be empty (recursion limit)
return tree.Children.Count == 2 &&
tree.Children.All(child => child.Children.Count == 0);
})
.Check();
}
}

// Recursive data structure for testing
public record TreeNode<T>
{
public T Value { get; init; }

Check warning on line 94 in src/Hedgehog.Experimental.CSharp.Tests/DefaultGeneratorsTests.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Value' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 94 in src/Hedgehog.Experimental.CSharp.Tests/DefaultGeneratorsTests.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Value' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public List<TreeNode<T>> Children { get; init; } = [];

public override string ToString()
{
if (Children.Count == 0)
return $"Node({Value})";

var childrenStr = string.Join(", ", Children.Select(c => c.ToString()));
return $"Node({Value}, [{childrenStr}])";
}
}
26 changes: 16 additions & 10 deletions src/Hedgehog.Experimental.CSharp.Tests/GenericGenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public sealed class OuterClass
public Maybe<Guid> Value { get; set; }
}

public sealed record RecursiveRec(Maybe<RecursiveRec> Value);

public sealed class GenericTestGenerators
{
public static Gen<Guid> Guid() =>
Expand All @@ -51,24 +53,30 @@ public static Gen<Uuid> UuidGen() =>
public static Gen<Name> NameGen(Gen<string> gen) =>
gen.Select(value => new Name("Name: " + value));

public static Gen<Maybe<T>> AlwaysJust<T>(Gen<T> gen) =>
gen.Select(Maybe<T> (value) => new Maybe<T>.Just(value));
public static Gen<Maybe<T>> AlwaysJust<T>(AutoGenContext context, Gen<T> gen) =>
context.CanRecurse
? gen.Select(Maybe<T> (value) => new Maybe<T>.Just(value))
: Gen.FromValue<Maybe<T>>(new Maybe<T>.Nothing());

public static Gen<Either<TLeft, TRight>> AlwaysLeft<TLeft, TRight>(Gen<TRight> genB, Gen<TLeft> genA) =>
genA.Select(Either<TLeft, TRight> (value) => new Either<TLeft, TRight>.Left(value));

// Generator for ImmutableList<T> that uses AutoGenConfig's seqRange
public static Gen<ImmutableList<T>> ImmutableListGen<T>(AutoGenConfig config, Gen<T> genItem) =>
genItem
.List(config.GetCollectionRange())
.Select(ImmutableList.CreateRange);
}

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

[Fact]
public void ShouldGenerateRecursiveRecords()
{
var config = GenX.defaults.WithGenerators<GenericTestGenerators>();
var prop = from x in ForAll(GenX.autoWith<RecursiveRec>(config))
select x != null;

prop.Check();
}

[Fact]
public void ShouldGenerateValueWithPhantomGenericType_Id()
{
Expand Down Expand Up @@ -151,8 +159,6 @@ public void ShouldGenerateImmutableListUsingAutoGenConfigParameter()
.WithCollectionRange(Range.FromValue(7))
.WithGenerators<GenericTestGenerators>();

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

Expand Down
9 changes: 4 additions & 5 deletions src/Hedgehog.Experimental.Tests/AutoGenConfigTests.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Hedgehog.Experimental.Tests.AutoGenConfigTests

open Hedgehog.Experimental
open Xunit
open Swensen.Unquote
open Hedgehog
Expand Down Expand Up @@ -78,11 +79,9 @@ let ``addGenerators supports methods with AutoGenConfig parameter``() =
open System.Collections.Immutable

type ImmutableListGenerators =
// Generic generator for ImmutableList<T> that uses AutoGenConfig's seqRange
static member ImmutableListGen<'T>(config: AutoGenConfig, genItem: Gen<'T>) : Gen<ImmutableList<'T>> = gen {
let! items = genItem |> Gen.list (AutoGenConfig.seqRange config)
return items |> ImmutableList.CreateRange
}
static member ImmutableListGen<'T>(config: AutoGenConfig, genItem: Gen<'T>) : Gen<ImmutableList<'T>> =
genItem |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map ImmutableList.CreateRange


[<Fact>]
let ``addGenerators supports generic methods with AutoGenConfig and Gen parameters``() =
Expand Down
35 changes: 35 additions & 0 deletions src/Hedgehog.Experimental.Tests/AutoGenContextTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module Hedgehog.Experimental.Tests.AutoGenContextTests

open Hedgehog
open Xunit
open Swensen.Unquote

type RecursiveType<'a> =
{ Value: Option<RecursiveType<'a>>}
member this.Depth =
match this.Value with
| None -> 0
| Some x -> x.Depth + 1

type RecursiveGenerators =
// override Option to always generate Some when recursion is allowed
// using the AutoGenContext to assert recursion context preservation
static member Option<'a>(context: AutoGenContext) =
if context.CanRecurse then
printfn "CurrentRecursionDepth: %d" context.CurrentRecursionDepth
context.AutoGenerate<'a>() |> Gen.map Some
else
Gen.constant None

[<Fact>]
let ``Should preserve recursion with generic types when using AutoGenContext.AutoGenerate``() =
property {
let! recDepth = Gen.int32 (Range.constant 2 5)
let config =
GenX.defaults
|> AutoGenConfig.addGenerators<RecursiveGenerators>
|> AutoGenConfig.setRecursionDepth recDepth

let! result = GenX.autoWith<RecursiveType<int>> config
test <@ result.Depth = recDepth @>
} |> Property.check

Check warning on line 35 in src/Hedgehog.Experimental.Tests/AutoGenContextTests.fs

View workflow job for this annotation

GitHub Actions / build

Main module of program is empty: nothing will happen when it is run

Check warning on line 35 in src/Hedgehog.Experimental.Tests/AutoGenContextTests.fs

View workflow job for this annotation

GitHub Actions / build

Main module of program is empty: nothing will happen when it is run
112 changes: 112 additions & 0 deletions src/Hedgehog.Experimental.Tests/ComplexGenericTest.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
module ComplexGenericTest

open Xunit
open Swensen.Unquote
open Hedgehog

// A type with complex generic parameter repetition: <A, A, B, C, A, A, D>
type ComplexType<'A, 'B, 'C, 'D, 'E, 'F, 'G> = {
First: 'A
Second: 'B
Third: 'C
Fifth: 'E
Fourth: 'D
Sixth: 'F
Seventh: 'G
}

type ComplexGenerators =
// Method with pattern: method has <A, B, C, D> but type uses <A, A, B, C, A, A, D>
static member Complex<'A, 'B, 'C, 'D>(
genA: Gen<'A>,
genC: Gen<'C>,
genB: Gen<'B>,
genD: Gen<'D>) : Gen<ComplexType<'A, 'A, 'B, 'C, 'A, 'A, 'D>> =
gen {
let! a = genA
let! b = genB
let! c = genC
let! d = genD
return {
First = a
Second = a
Third = b
Fourth = c
Fifth = a
Sixth = a
Seventh = d
}
}

[<Fact>]
let ``Should handle complex generic parameter repetition pattern``() =
let config =
GenX.defaults
|> AutoGenConfig.addGenerators<ComplexGenerators>

// Generate ComplexType<int, int, string, bool, int, int, float>
// Method is Complex<int, string, bool, float>
let gen = GenX.autoWith<ComplexType<int, int, string, bool, int, int, float>> config
let sample = Gen.sample 0 1 gen |> Seq.head

// Verify the structure is correct
test <@ sample.First = sample.Second @> // Both should be the same 'A value
test <@ sample.First = sample.Fifth @> // All 'A positions should be the same
test <@ sample.Second = sample.Sixth @>
test <@ sample.Third.GetType() = typeof<string> @>
test <@ sample.Fourth.GetType() = typeof<bool> @>
test <@ sample.Seventh.GetType() = typeof<float> @>

// Better test with specific verifiable values
type VerifiableGenerators =
static member VerifiableComplex<'A, 'B, 'C, 'D>(
genA: Gen<'A>,
genC: Gen<'C>,
genB: Gen<'B>,
genD: Gen<'D>) : Gen<ComplexType<'A, 'A, 'B, 'C, 'A, 'A, 'D>> =
gen {
let! a = genA
let! b = genB
let! c = genC
let! d = genD
return {
First = a
Second = a
Third = b
Fourth = c
Fifth = a
Sixth = a
Seventh = d
}
}

// Specific constant generators to verify correct parameter mapping
type SpecificGenerators =
static member Int() = Gen.constant 42
static member String() = Gen.constant "test"
static member Bool() = Gen.constant true
static member Float() = Gen.constant 3.14

[<Fact>]
let ``Should map parameters correctly with swapped parameter order``() =
let config =
GenX.defaults
|> AutoGenConfig.addGenerators<SpecificGenerators>
|> AutoGenConfig.addGenerators<VerifiableGenerators>

let gen = GenX.autoWith<ComplexType<int, int, string, bool, int, int, float>> config
let sample = Gen.sample 0 1 gen |> Seq.head

// With swapped parameters (genA, genC, genB, genD), the mapping should be:
// 'A -> int (42) goes to positions: First, Second, Fifth, Sixth
// 'B -> string ("test") goes to position: Third
// 'C -> bool (true) goes to position: Fourth
// 'D -> float (3.14) goes to position: Seventh

test <@ sample.First = 42 @>
test <@ sample.Second = 42 @>
test <@ sample.Third = "test" @>
test <@ sample.Fourth = true @>
test <@ sample.Fifth = 42 @>
test <@ sample.Sixth = 42 @>
test <@ sample.Seventh = 3.14 @>
Loading