Skip to content
Closed
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
7 changes: 6 additions & 1 deletion src/Hedgehog.Stateful/Action.fs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,10 @@ with
/// A sequence of actions to execute
type internal Actions<'TSystem, 'TState> = {
Initial: 'TState
Steps: Action<'TSystem, 'TState> list
/// Setup actions (executed first, in order, stops on first failure)
Setup: Action<'TSystem, 'TState> list
/// Test actions (executed after setup, in order, stops on first failure)
Test: Action<'TSystem, 'TState> list
/// Cleanup actions (always executed, even if Setup/Test fail; all are attempted)
Cleanup: Action<'TSystem, 'TState> list
}
226 changes: 110 additions & 116 deletions src/Hedgehog.Stateful/Parallel.fs

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions src/Hedgehog.Stateful/ParallelSpecification.fs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,7 @@ type ParallelSpecification<'TSystem, 'TState>() =
let cleanupActions = this.CleanupCommands |> Seq.map _.ToActionGen() |> List.ofSeq
let gen = Parallel.genActions this.PrefixRange this.BranchRange setupActions testActions cleanupActions this.InitialState

property {
let! actions = gen
do! Parallel.executeWithSUT sut actions
}
gen |> Property.forAll (Parallel.executeWithSUT sut)

/// <summary>
/// Convert this specification to a property using a SUT factory.
Expand All @@ -87,8 +84,7 @@ type ParallelSpecification<'TSystem, 'TState>() =
let cleanupActions = this.CleanupCommands |> Seq.map _.ToActionGen() |> List.ofSeq
let gen = Parallel.genActions this.PrefixRange this.BranchRange setupActions testActions cleanupActions this.InitialState

property {
let! actions = gen
gen |> Property.forAll (fun actions ->
let sut = createSut() // Create fresh SUT for this test run
do! Parallel.executeWithSUT sut actions
}
Parallel.executeWithSUT sut actions
)
324 changes: 242 additions & 82 deletions src/Hedgehog.Stateful/Sequential.fs

Large diffs are not rendered by default.

21 changes: 9 additions & 12 deletions src/Hedgehog.Stateful/SequentialSpecification.fs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,11 @@ type SequentialSpecification<'TSystem, 'TState>() =
let setupActions = this.SetupCommands |> Seq.map _.ToActionGen() |> List.ofSeq
let testActions = this.Commands |> Seq.map _.ToActionGen() |> List.ofSeq
let cleanupActions = this.CleanupCommands |> Seq.map _.ToActionGen() |> List.ofSeq
let gen = Sequential.genActions this.Range setupActions testActions cleanupActions this.InitialState Env.empty

let gen = Sequential.genActions this.Range setupActions testActions cleanupActions this.InitialState

property {
let! actions = gen
do! Sequential.executeWithSUT sut actions
}
gen |> Property.forAll (fun actions ->
Sequential.executeWithSUT sut actions
)

/// <summary>
/// Convert this specification to a property using a SUT factory.
Expand All @@ -81,10 +79,9 @@ type SequentialSpecification<'TSystem, 'TState>() =
let setupActions = this.SetupCommands |> Seq.map _.ToActionGen() |> List.ofSeq
let testActions = this.Commands |> Seq.map _.ToActionGen() |> List.ofSeq
let cleanupActions = this.CleanupCommands |> Seq.map _.ToActionGen() |> List.ofSeq
let gen = Sequential.genActions this.Range setupActions testActions cleanupActions this.InitialState
let gen = Sequential.genActions this.Range setupActions testActions cleanupActions this.InitialState Env.empty

property {
let! actions = gen
let sut = createSut() // Create fresh SUT for this test run
do! Sequential.executeWithSUT sut actions
}
gen |> Property.forAll (fun actions ->
let sut = createSut()
Sequential.executeWithSUT sut actions
)
68 changes: 20 additions & 48 deletions src/Hedgehog.Stateful/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,11 @@ type Var<'T> = private {
/// Handles unboxing and any projections/mappings applied via Var.map.
/// </summary>
Transform: obj -> 'T
/// <summary>
/// Mutable field used for caching resolved values to avoid redundant environment lookups and Transform applications.
/// Eagerly populated when a concrete value is bound via Env.add.
/// Also used for displaying resolved values in counterexamples.
///
/// The cache eliminates the need to repeatedly look up values from Env and apply Transform,
/// which is especially beneficial since most Var instances are created with concrete values
/// that are immediately known, not from symbolic references that need delayed resolution.
/// </summary>
mutable ResolvedValue: 'T option
}
with

member private this.DisplayText =
// If resolved for display (during counterexample formatting), show the resolved value
let value = this.ResolvedValue |> Option.orElse this.Default
match value with
| Some value -> $"%A{value}"
| None -> "<unused>"
$"Var_{this.Name}<{typeof<'T>}>"

/// <summary>
/// Resolve the variable using its default if not found in the environment.
Expand Down Expand Up @@ -97,36 +83,29 @@ Commands that use Var<T> inputs must override Require to call TryResolve and ret
/// <param name="value">When this method returns, contains the resolved value if successful; otherwise, the default value for the type.</param>
/// <returns>true if the variable was successfully resolved; otherwise, false.</returns>
member this.TryResolve(env: Env, [<System.Runtime.InteropServices.Out>] [<NotNullWhen(true)>] value: byref<'T>) : bool =
// Check cache first to avoid redundant environment lookups and Transform applications
match this.ResolvedValue with
| Some cached ->
value <- cached
true
| None ->
// Try to look up in environment
match Map.tryFind (Name this.Name) env.values with
| Some v ->
try
let resolved = this.Transform v
// Cache the resolved value for future calls (memoization)
this.ResolvedValue <- Some resolved
value <- resolved
true
with _ ->
value <- Unchecked.defaultof<'T>
false
match Map.tryFind (Name this.Name) env.values with
| Some v ->
try
let resolved = this.Transform v
// Cache the resolved value for future calls (memoization)
value <- resolved
true
with _ ->
value <- Unchecked.defaultof<'T>
false
| None ->
// Try default value
match this.Default with
| Some d ->
value <- d
true
| None ->
// Try default value
match this.Default with
| Some d ->
value <- d
true
| None ->
value <- Unchecked.defaultof<'T>
false
value <- Unchecked.defaultof<'T>
false

static member internal CreateSymbolic(value: 'T) : Var<'T> =
{ Name = -1; Default = Some value; Transform = unbox<'T>; ResolvedValue = None }
{ Name = -1; Default = Some value; Transform = unbox<'T> }

override this.Equals(other: obj) : bool =
match other with
Expand All @@ -153,13 +132,6 @@ module internal Env =

/// Store a concrete value for a variable
let add (v: Var<'a>) (value: 'a) (env: Env) : Env =
// Eagerly cache the resolved value to avoid future environment lookups
// Apply Transform to ensure the cache contains the correctly transformed value
try
let resolved = v.Transform (box value)
v.ResolvedValue <- Some resolved
with
| _ -> () // If Transform fails, leave cache empty and let Resolve handle it later
{ env with values = Map.add (Name v.Name) (box value) env.values }

/// Resolve a variable to its concrete value
Expand Down
10 changes: 4 additions & 6 deletions src/Hedgehog.Stateful/Var.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Var =
/// <returns>A new symbolic (unbound) <c>Var&lt;T&gt;</c> with the given default value.</returns>
[<CompiledName("Symbolic")>]
let symbolic (defaultValue: 'T) : Var<'T> =
{ Name = -1; Default = Some defaultValue; Transform = unbox<'T>; ResolvedValue = None }
{ Name = -1; Default = Some defaultValue; Transform = unbox<'T> }

namespace Hedgehog.Stateful.FSharp

Expand Down Expand Up @@ -66,18 +66,16 @@ module Var =
let transform = v.Transform >> f
{ Name = v.Name
Default = v.Default |> Option.map f
Transform = transform
ResolvedValue = v.ResolvedValue |> Option.map transform }
Transform = transform }

/// Create a bounded var from a Name (used during generation)
let internal bound (name: Name) : Var<'T> =
let (Name n) = name
{ Name = n; Default = None; Transform = unbox<'T>; ResolvedValue = None }
{ Name = n; Default = None; Transform = unbox<'T> }

/// Convert from obj var to typed var (used internally)
let internal convertFrom<'T> (v: Var<obj>) : Var<'T> =
let transform = v.Transform >> unbox<'T>
{ Name = v.Name
Default = v.Default |> Option.map unbox<'T>
Transform = transform
ResolvedValue = v.ResolvedValue |> Option.map transform }
Transform = transform }
7 changes: 6 additions & 1 deletion src/Hedgehog/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ module Property =
/// The property succeeds when the async computation completes successfully, and fails if it throws an exception.
/// This enables testing of asynchronous F# code.
let ofAsync (asyncComputation : Async<'T>) : Property<'T> =
Gen.constant (lazy (PropertyResult.ofAsyncWith asyncComputation))
Gen.constant (lazy (PropertyResult.ofAsync asyncComputation))
|> Property

/// Create Property from an async computation that produces a journal and outcome
let ofAsyncWithJournal (asyncComputation : Async<Journal * Outcome<'T>>) : Property<'T> =
Gen.constant (lazy (PropertyResult.ofAsyncWithJournal asyncComputation))
|> Property

/// Discards test cases where the predicate returns false, causing Hedgehog to generate a new test case.
Expand Down
4 changes: 2 additions & 2 deletions src/Hedgehog/PropertyResult.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ module internal PropertyResult =
PropertyResult.Sync (journal, outcome)

/// Create an async PropertyResult from an async computation that produces a journal and outcome
let ofAsync (asyncResult : Async<Journal * Outcome<'a>>) : PropertyResult<'a> =
let ofAsyncWithJournal (asyncResult : Async<Journal * Outcome<'a>>) : PropertyResult<'a> =
PropertyResult.Async asyncResult

/// Create an async PropertyResult by capturing exceptions from an async computation
let ofAsyncWith (asyncComputation : Async<'a>) : PropertyResult<'a> =
let ofAsync (asyncComputation : Async<'a>) : PropertyResult<'a> =
PropertyResult.Async (async {
try
let! result = asyncComputation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,6 @@ type CounterSpec() =
let ``Counter test with clean SUT parameter API``() =
let sut = Counter()
// Use CheckWith to create a fresh Counter for each property test run
CounterSpec().ToProperty(sut).Check()
CounterSpec().ToProperty(sut)
.Recheck("3_11438778048498838243_14419596045992193899_2")
// .Check()
Loading