Skip to content

Commit 06a84ae

Browse files
authored
Merge pull request #469 from hedgehogqa/add-ignore-combinator
Add Property.ignoreResult combinator
2 parents 5cca871 + f174b65 commit 06a84ae

File tree

6 files changed

+132
-0
lines changed

6 files changed

+132
-0
lines changed

src/Hedgehog/Linq/Property.fs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ type PropertyExtensions private () =
9696
static member TryWith (property : Property<'T>, onError : Func<exn, Property<'T>>) : Property<'T> =
9797
Property.tryWith onError.Invoke property
9898

99+
/// <summary>
100+
/// Discards the result of a property, converting it to <see cref="Property"/>.
101+
/// This is useful when using assertion libraries that return non-unit types (e.g., fluent assertions).
102+
/// Note: The assertion must throw an exception on failure for the property to fail.
103+
/// Assertions that throw exceptions will still cause the property to fail.
104+
/// </summary>
105+
/// <param name="property">The property whose result should be ignored.</param>
106+
/// <returns>A property that produces unit.</returns>
107+
/// <example>
108+
/// <code>
109+
/// var property =
110+
/// from x in Gen.Int32(Range.Linear(0, 100)).ForAll()
111+
/// select x.Should().Be(42); // Returns IAssertion
112+
///
113+
/// property.IgnoreResult().Check(); // Convert to <see cref="Property"/> and run
114+
/// </code>
115+
/// </example>
116+
[<Extension>]
117+
static member IgnoreResult (property : Property<'T>) : Property =
118+
property |> Property.ignoreResult |> Property
119+
99120
[<Extension>]
100121
static member Report (property : Property) : Report =
101122
let (Property property) = property

src/Hedgehog/Property.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ module Property =
181181
let internal set (a: 'a) (property : Property<'b>) : Property<'a> =
182182
property |> map (fun _ -> a)
183183

184+
/// Discards the result of a property, converting it to Property<unit>.
185+
/// This is useful when using assertion libraries that return non-unit types (e.g., fluent assertions).
186+
/// Assertions that throw exceptions will still cause the property to fail.
187+
let ignoreResult (property : Property<'a>) : Property<unit> =
188+
property |> map (fun _ -> ())
189+
184190
// Helper to handle Failure/Discard cases in bind - just wrap and return without calling continuation.
185191
// Note: Failure and Discard don't carry values, so we can safely change the type parameter.
186192
let private shortCircuit (journal : Journal) (outcome : Outcome<'a>) : Gen<Lazy<PropertyResult<'b>>> =

tests/Hedgehog.Linq.Tests/LinqTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,5 +302,42 @@ from subset in Gen.SubsetOf(distinctOriginal).ForAll()
302302

303303
prop.Check();
304304
}
305+
306+
// Test record to simulate fluent assertion library
307+
private record AssertionResult(bool Success);
308+
309+
private static AssertionResult ShouldBe(int expected, int actual)
310+
{
311+
if (actual == expected)
312+
return new AssertionResult(true);
313+
throw new Exception($"Expected {expected} but got {actual}");
314+
}
315+
316+
[Fact]
317+
public void IgnoreResult_ConvertsNonUnitPropertyToUnit()
318+
{
319+
var testRan = false;
320+
var property =
321+
from x in Gen.Int32(Range.Constant(42, 42)).ForAll()
322+
let _ = testRan = true
323+
select ShouldBe(42, x); // Returns AssertionResult, not unit
324+
325+
// Should compile and run
326+
property.IgnoreResult().Check();
327+
328+
Assert.True(testRan);
329+
}
330+
331+
[Fact]
332+
public void IgnoreResult_PreservesFailures()
333+
{
334+
var property =
335+
from x in Gen.Int32(Range.Constant(42, 42)).ForAll()
336+
select ShouldBe(99, x); // Will throw
337+
338+
var report = property.IgnoreResult().Report();
339+
340+
Assert.True(report.Status.IsFailed);
341+
}
305342
}
306343
}

tests/Hedgehog.Tests/Hedgehog.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<Compile Include="ReportTests.fs" />
2121
<Compile Include="PropertyTests.fs" />
2222
<Compile Include="PropertyBindTests.fs" />
23+
<Compile Include="TryWithTryFinallyTests.fs" />
2324
<Compile Include="AsyncTests.fs" />
2425
<Compile Include="AsyncAndTaskTests.fs" />
2526
<Compile Include="Program.fs" />

tests/Hedgehog.Tests/Program.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let allTests = testList "All tests" [
1717
ReportTests.reportTests
1818
PropertyTests.propertyTests
1919
PropertyBindTests.propertyBindTests
20+
TryWithTryFinallyTests.tryWithTryFinallyTests
2021
PropertyAsyncTests.asyncTests
2122
#if !FABLE_COMPILER
2223
PropertyAsyncAndTaskTests.asyncAndTaskTests
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
module Hedgehog.Tests.TryWithTryFinallyTests
2+
3+
open Hedgehog
4+
open Hedgehog.FSharp
5+
open TestDsl
6+
7+
// Simulate fluent assertion library for testing
8+
type AssertionResult = { Success: bool }
9+
let shouldBe expected actual =
10+
if actual = expected then { Success = true }
11+
else failwithf $"Expected %d{expected} but got %d{actual}"
12+
13+
/// Tests for Property.tryFinally and Property.ignoreResult
14+
let tryWithTryFinallyTests = testList "Property.tryFinally and ignoreResult tests" [
15+
16+
testCase "tryFinally cleanup runs on success" <| fun () ->
17+
let mutable cleanupCalled = false
18+
property {
19+
let! x = Gen.constant 42
20+
return x = 42
21+
}
22+
|> Property.tryFinally (fun () -> cleanupCalled <- true)
23+
|> Property.checkBool
24+
25+
Expect.isTrue cleanupCalled
26+
27+
testCase "tryFinally cleanup runs on failure" <| fun () ->
28+
let mutable cleanupCalled = false
29+
let report =
30+
property {
31+
let! x = Gen.constant 42
32+
return false
33+
}
34+
|> Property.tryFinally (fun () -> cleanupCalled <- true)
35+
|> Property.reportBool
36+
37+
match report.Status with
38+
| Failed _ -> ()
39+
| _ -> failwith "Expected Failed status"
40+
41+
Expect.isTrue cleanupCalled
42+
43+
testCase "ignoreResult converts Property<'a> to Property<unit>" <| fun () ->
44+
let mutable testRan = false
45+
property {
46+
let! x = Gen.constant 42
47+
testRan <- true
48+
return shouldBe 42 x // Returns AssertionResult
49+
}
50+
|> Property.ignoreResult // Convert to Property<unit>
51+
|> Property.check
52+
53+
54+
testCase "ignoreResult preserves failures" <| fun () ->
55+
let report =
56+
property {
57+
let! x = Gen.constant 42
58+
return shouldBe 99 x // Will throw
59+
}
60+
|> Property.ignoreResult
61+
|> Property.report
62+
63+
match report.Status with
64+
| Failed _ -> ()
65+
| _ -> failwith "Expected Failed status"
66+
]

0 commit comments

Comments
 (0)