Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
20 changes: 20 additions & 0 deletions src/Hedgehog/Linq/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ type PropertyExtensions private () =
static member TryWith (property : Property<'T>, onError : Func<exn, Property<'T>>) : Property<'T> =
Property.tryWith onError.Invoke property

/// <summary>
/// Discards the result of a property, converting it to <see cref="Property"/>.
/// This is useful when using assertion libraries that return non-unit types (e.g., fluent assertions).
/// Assertions that throw exceptions will still cause the property to fail.
/// </summary>
/// <param name="property">The property whose result should be ignored.</param>
/// <returns>A property that produces unit.</returns>
/// <example>
/// <code>
/// var property =
/// from x in Gen.Int32(Range.Linear(0, 100)).ForAll()
/// select x.Should().Be(42); // Returns IAssertion
///
/// property.IgnoreResult().Check(); // Convert to <see cref="Property"/> and run
/// </code>
/// </example>
[<Extension>]
static member IgnoreResult (property : Property<'T>) : Property =
property |> Property.ignoreResult |> Property

[<Extension>]
static member Report (property : Property) : Report =
let (Property property) = property
Expand Down
6 changes: 6 additions & 0 deletions src/Hedgehog/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ module Property =
let internal set (a: 'a) (property : Property<'b>) : Property<'a> =
property |> map (fun _ -> a)

/// Discards the result of a property, converting it to Property<unit>.
/// This is useful when using assertion libraries that return non-unit types (e.g., fluent assertions).
/// Assertions that throw exceptions will still cause the property to fail.
let ignoreResult (property : Property<'a>) : Property<unit> =
property |> map (fun _ -> ())

// Helper to handle Failure/Discard cases in bind - just wrap and return without calling continuation.
// Note: Failure and Discard don't carry values, so we can safely change the type parameter.
let private shortCircuit (journal : Journal) (outcome : Outcome<'a>) : Gen<Lazy<PropertyResult<'b>>> =
Expand Down
37 changes: 37 additions & 0 deletions tests/Hedgehog.Linq.Tests/LinqTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,5 +302,42 @@ from subset in Gen.SubsetOf(distinctOriginal).ForAll()

prop.Check();
}

// Test record to simulate fluent assertion library
private record AssertionResult(bool Success);

private static AssertionResult ShouldBe(int expected, int actual)
{
if (actual == expected)
return new AssertionResult(true);
throw new Exception($"Expected {expected} but got {actual}");
}

[Fact]
public void IgnoreResult_ConvertsNonUnitPropertyToUnit()
{
var testRan = false;
var property =
from x in Gen.Int32(Range.Constant(42, 42)).ForAll()
let _ = testRan = true
select ShouldBe(42, x); // Returns AssertionResult, not unit

// Should compile and run
property.IgnoreResult().Check();

Assert.True(testRan);
}

[Fact]
public void IgnoreResult_PreservesFailures()
{
var property =
from x in Gen.Int32(Range.Constant(42, 42)).ForAll()
select ShouldBe(99, x); // Will throw

var report = property.IgnoreResult().Report();

Assert.True(report.Status.IsFailed);
}
}
}
1 change: 1 addition & 0 deletions tests/Hedgehog.Tests/Hedgehog.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Compile Include="ReportTests.fs" />
<Compile Include="PropertyTests.fs" />
<Compile Include="PropertyBindTests.fs" />
<Compile Include="TryWithTryFinallyTests.fs" />
<Compile Include="AsyncTests.fs" />
<Compile Include="AsyncAndTaskTests.fs" />
<Compile Include="Program.fs" />
Expand Down
1 change: 1 addition & 0 deletions tests/Hedgehog.Tests/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let allTests = testList "All tests" [
ReportTests.reportTests
PropertyTests.propertyTests
PropertyBindTests.propertyBindTests
TryWithTryFinallyTests.tryWithTryFinallyTests
PropertyAsyncTests.asyncTests
#if !FABLE_COMPILER
PropertyAsyncAndTaskTests.asyncAndTaskTests
Expand Down
66 changes: 66 additions & 0 deletions tests/Hedgehog.Tests/TryWithTryFinallyTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module Hedgehog.Tests.TryWithTryFinallyTests

open Hedgehog
open Hedgehog.FSharp
open TestDsl

// Simulate fluent assertion library for testing
type AssertionResult = { Success: bool }
let shouldBe expected actual =
if actual = expected then { Success = true }
else failwithf $"Expected %d{expected} but got %d{actual}"

/// Tests for Property.tryFinally and Property.ignoreResult
let tryWithTryFinallyTests = testList "Property.tryFinally and ignoreResult tests" [

testCase "tryFinally cleanup runs on success" <| fun () ->
let mutable cleanupCalled = false
property {
let! x = Gen.constant 42
return x = 42
}
|> Property.tryFinally (fun () -> cleanupCalled <- true)
|> Property.checkBool

Expect.isTrue cleanupCalled

testCase "tryFinally cleanup runs on failure" <| fun () ->
let mutable cleanupCalled = false
let report =
property {
let! x = Gen.constant 42
return false
}
|> Property.tryFinally (fun () -> cleanupCalled <- true)
|> Property.reportBool

match report.Status with
| Failed _ -> ()
| _ -> failwith "Expected Failed status"

Expect.isTrue cleanupCalled

testCase "ignoreResult converts Property<'a> to Property<unit>" <| fun () ->
let mutable testRan = false
property {
let! x = Gen.constant 42
testRan <- true
return shouldBe 42 x // Returns AssertionResult
}
|> Property.ignoreResult // Convert to Property<unit>
|> Property.check


testCase "ignoreResult preserves failures" <| fun () ->
let report =
property {
let! x = Gen.constant 42
return shouldBe 99 x // Will throw
}
|> Property.ignoreResult
|> Property.report

match report.Status with
| Failed _ -> ()
| _ -> failwith "Expected Failed status"
]
Loading