Skip to content

Commit df2f464

Browse files
committed
Format report for uniformly
1 parent a1ab097 commit df2f464

File tree

13 files changed

+232
-348
lines changed

13 files changed

+232
-348
lines changed

src/Hedgehog.Xunit/Exceptions.fs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@ namespace Hedgehog.Xunit
22

33
open Xunit.Sdk
44

5-
// This exists to make it clear to users that the exception is in the return of their test.
6-
// Raising System.Exception isn't descriptive enough.
7-
// Using Xunit.Assert.True could be confusing since it may resemble a user's assertion.
8-
type internal TestReturnedFalseException() =
9-
inherit System.Exception("Test returned `false`.")
10-
115
/// Exception for property test failures that produces clean output
126
type PropertyFailedException(message: string) =
137
inherit XunitException(message)

src/Hedgehog.Xunit/InternalLogic.fs

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ open Hedgehog.FSharp
55
open Hedgehog.Xunit
66
open System
77
open System.Reflection
8-
open System.Runtime.ExceptionServices
9-
open System.Threading
108
open System.Threading.Tasks
119

1210
// ========================================
@@ -110,7 +108,6 @@ let rec wrapReturnValue (x: obj) : Property<unit> =
110108

111109
| _ -> Property.success ()
112110

113-
114111
// ========================================
115112
// Resource Management
116113
// ========================================
@@ -120,18 +117,6 @@ let dispose (o: obj) =
120117
| :? IDisposable as d -> d.Dispose()
121118
| _ -> ()
122119

123-
// ========================================
124-
// Value Formatting & Display
125-
// ========================================
126-
127-
let printValue = Hedgehog.FSharp.ValueFormatting.printValue
128-
129-
let formatParametersWithNames (parameters: ParameterInfo[]) (values: obj list) : string =
130-
Array.zip parameters (List.toArray values)
131-
|> Array.map (fun (param, value) ->
132-
$"%s{param.Name} = %s{printValue value}")
133-
|> String.concat Environment.NewLine
134-
135120
// ========================================
136121
// Configuration Helpers
137122
// ========================================
@@ -204,16 +189,8 @@ module private PropertyBuilder =
204189
else
205190
testMethod
206191

207-
try
208-
methodToInvoke.Invoke(testClassInstance, args |> Array.ofList)
209-
with
210-
| :? TargetInvocationException as tie when not (isNull tie.InnerException) ->
211-
// Unwrap reflection exception to show the actual user exception instead of TargetInvocationException.
212-
// We use ExceptionDispatchInfo.Capture().Throw() to preserve the original stack trace.
213-
// Note: This adds a "--- End of stack trace from previous location ---" marker
214-
// and appends additional frames as the exception propagates, which we filter out later.
215-
ExceptionDispatchInfo.Capture(tie.InnerException).Throw()
216-
failwith "unreachable"
192+
methodToInvoke.Invoke(testClassInstance, args |> Array.ofList)
193+
217194

218195
/// Creates a property based on the test method's return type
219196
let createProperty
@@ -228,12 +205,12 @@ module private PropertyBuilder =
228205
invokeTestMethod testMethod testClassInstance args
229206
finally
230207
List.iter dispose args
231-
with e ->
232-
// If the test method throws an exception, we need to handle it
233-
// For Property<_> return types, the exception will be caught by Property.map
234-
// For other return types, we need to wrap it in a failing property
235-
// We return a special marker that wrapReturnValue will recognize
236-
box e
208+
with
209+
// Unwrap TargetInvocationException to get the actual exception.
210+
// It is safe to do it because invokeTestMethod uses reflection that adds this wrapper.
211+
| :? TargetInvocationException as e when not (isNull e.InnerException) ->
212+
box e.InnerException
213+
| e -> box e
237214

238215
let createJournal args =
239216
let parameterEntries =
@@ -245,10 +222,7 @@ module private PropertyBuilder =
245222

246223
let wrapWithExceptionHandling (result: obj) : Property<unit> =
247224
match result with
248-
| :? exn as e ->
249-
// Exception was thrown - create a failing property
250-
Property.counterexample (fun () -> string e)
251-
|> Property.bind (fun () -> Property.failure)
225+
| :? exn as e -> Property.exn e
252226
| _ -> wrapReturnValue result
253227

254228

src/Hedgehog.Xunit/ReportFormatter.fs

Lines changed: 1 addition & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -4,173 +4,8 @@ module internal ReportFormatter
44

55
open Hedgehog
66
open Hedgehog.Xunit
7-
open System.Text
8-
9-
// ========================================
10-
// Constants and Formatting
11-
// ========================================
12-
13-
let private indent = " " // 2 spaces to align with xUnit's output format
14-
let private printValue = Hedgehog.FSharp.ValueFormatting.printValue
15-
16-
// ========================================
17-
// Report Formatting
18-
// ========================================
19-
20-
/// Filters exception string to show only user code stack trace.
21-
/// When we rethrow using ExceptionDispatchInfo.Capture().Throw() to preserve the original stack trace,
22-
/// it adds a "--- End of stack trace from previous location ---" marker and appends Hedgehog's
23-
/// internal frames as the exception propagates. We remove everything from that marker onwards
24-
/// to show only the user's code in the test failure report.
25-
let private filterExceptionStackTrace (exceptionEntry: string) : string =
26-
match exceptionEntry.IndexOf("--- End of stack trace from previous location ---") with
27-
| -1 -> exceptionEntry // No marker found, return as-is
28-
| idx -> exceptionEntry.Substring(0, idx).TrimEnd()
29-
30-
// ========================================
31-
// Journal Entry Groups
32-
// ========================================
33-
34-
type private JournalEntryGroup =
35-
| ParametersGroup of (string * obj) list
36-
| GeneratedGroup of obj list
37-
| CounterexamplesGroup of string list
38-
| TextsGroup of string list
39-
| CancellationsGroup of string list
40-
| ExceptionsGroup of exn list
41-
42-
let private classifyJournalLine (line: JournalLine) : JournalEntryGroup =
43-
match line with
44-
| TestParameter (name, value) -> ParametersGroup [(name, value)]
45-
| GeneratedValue value -> GeneratedGroup [value]
46-
| Counterexample msg -> CounterexamplesGroup [msg]
47-
| Text msg -> TextsGroup [msg]
48-
| Cancellation msg -> CancellationsGroup [msg]
49-
| Exception exn -> ExceptionsGroup [exn]
50-
51-
let private groupKey (group: JournalEntryGroup) : int =
52-
match group with
53-
| ParametersGroup _ -> 0
54-
| GeneratedGroup _ -> 1
55-
| CounterexamplesGroup _ -> 2
56-
| TextsGroup _ -> 3
57-
| CancellationsGroup _ -> 4
58-
| ExceptionsGroup _ -> 5
59-
60-
let private mergeGroups (groups: JournalEntryGroup list) : JournalEntryGroup =
61-
match groups with
62-
| [] -> failwith "Cannot merge empty group list"
63-
| ParametersGroup _ :: _ ->
64-
groups |> List.collect (function ParametersGroup items -> items | _ -> []) |> ParametersGroup
65-
| GeneratedGroup _ :: _ ->
66-
groups |> List.collect (function GeneratedGroup items -> items | _ -> []) |> GeneratedGroup
67-
| CounterexamplesGroup _ :: _ ->
68-
groups |> List.collect (function CounterexamplesGroup items -> items | _ -> []) |> CounterexamplesGroup
69-
| TextsGroup _ :: _ ->
70-
groups |> List.collect (function TextsGroup items -> items | _ -> []) |> TextsGroup
71-
| CancellationsGroup _ :: _ ->
72-
groups |> List.collect (function CancellationsGroup items -> items | _ -> []) |> CancellationsGroup
73-
| ExceptionsGroup _ :: _ ->
74-
groups |> List.collect (function ExceptionsGroup items -> items | _ -> []) |> ExceptionsGroup
75-
76-
// ========================================
77-
// Group Rendering Functions
78-
// ========================================
79-
80-
let private renderParameters (sb: StringBuilder) (parameters: (string * obj) list) : unit =
81-
sb.AppendLine().AppendLine("Test parameters:") |> ignore
82-
parameters |> List.iter (fun (name, value) ->
83-
sb.AppendIndentedLine(indent, $"%s{name} = %s{printValue value}") |> ignore)
84-
85-
let private renderGenerated (sb: StringBuilder) (values: obj list) : unit =
86-
sb.AppendLine().AppendLine("Generated values:") |> ignore
87-
values |> List.iter (fun value ->
88-
sb.AppendIndentedLine(indent, printValue value) |> ignore)
89-
90-
let private renderCounterexamples (sb: StringBuilder) (messages: string list) : unit =
91-
sb.AppendLine().AppendLine("Counterexamples:") |> ignore
92-
messages |> List.iter (fun msg -> sb.AppendIndentedLine(indent, msg) |> ignore)
93-
94-
let private renderTexts (sb: StringBuilder) (messages: string list) : unit =
95-
sb.AppendLine() |> ignore
96-
messages |> List.iter (fun msg -> sb.AppendLine(msg) |> ignore)
97-
98-
let private renderCancellations (sb: StringBuilder) (messages: string list) : unit =
99-
sb.AppendLine() |> ignore
100-
messages |> List.iter (fun msg -> sb.AppendLine(msg) |> ignore)
101-
102-
let private renderExceptions (sb: StringBuilder) (exceptions: exn list) : unit =
103-
exceptions |> List.iter (fun exn ->
104-
let exceptionString = string (Exceptions.unwrap exn)
105-
let filteredEntry = filterExceptionStackTrace exceptionString
106-
sb.AppendLine().AppendLine("Actual exception:").AppendLine(filteredEntry) |> ignore)
107-
108-
let private formatFailureForXunit (failure: FailureData) (report: Report) : string =
109-
let sb = StringBuilder()
110-
111-
let renderTests (tests: int<tests>) =
112-
sprintf "%d test%s" (int tests) (if int tests = 1 then "" else "s")
113-
114-
let renderAndShrinks (shrinks: int<shrinks>) =
115-
if int shrinks = 0 then
116-
""
117-
else
118-
sprintf " and %d shrink%s" (int shrinks) (if int shrinks = 1 then "" else "s")
119-
120-
let renderAndDiscards (discards: int<discards>) =
121-
if int discards = 0 then
122-
""
123-
else
124-
sprintf " and %d discard%s" (int discards) (if int discards = 1 then "" else "s")
125-
126-
// Header
127-
sb.AppendIndentedLine(
128-
indent,
129-
sprintf
130-
"*** Failed! Falsifiable (after %s%s%s):"
131-
(renderTests report.Tests)
132-
(renderAndShrinks failure.Shrinks)
133-
(renderAndDiscards report.Discards)
134-
)
135-
|> ignore
136-
137-
// Recheck seed (if available)
138-
match failure.RecheckInfo with
139-
| Some recheckInfo ->
140-
let serialized = RecheckData.serialize recheckInfo.Data
141-
sb.AppendLine()
142-
.AppendLine("You can reproduce this failure with the following Recheck Seed:")
143-
.AppendIndentedLine(indent, $"\"%s{serialized}\"") |> ignore
144-
| None -> ()
145-
146-
// Evaluate journal entries and group consecutively by type
147-
let journalLines = Journal.eval failure.Journal
148-
149-
// Classify each journal line and group consecutive entries of the same type
150-
let groups =
151-
journalLines
152-
|> Seq.map classifyJournalLine
153-
|> Seq.groupConsecutiveBy groupKey
154-
|> List.map (fun (_, groupList) -> mergeGroups groupList)
155-
156-
// Render each group in order
157-
groups |> List.iter (fun group ->
158-
match group with
159-
| ParametersGroup parameters -> renderParameters sb parameters
160-
| GeneratedGroup values -> renderGenerated sb values
161-
| CounterexamplesGroup messages -> renderCounterexamples sb messages
162-
| TextsGroup messages -> renderTexts sb messages
163-
| CancellationsGroup messages -> renderCancellations sb messages
164-
| ExceptionsGroup exceptions -> renderExceptions sb exceptions)
165-
166-
sb.ToString()
167-
168-
let private formatReportForXunit (report: Report) : string =
169-
match report.Status with
170-
| Failed failure -> formatFailureForXunit failure report
171-
| _ -> Report.render report
1727

1738
let tryRaise (report: Report) : unit =
1749
match report.Status with
175-
| Failed _ -> report |> formatReportForXunit |> PropertyFailedException |> raise
10+
| Failed _ -> report |> Report.render |> PropertyFailedException |> raise
17611
| _ -> Report.tryRaise report

src/Hedgehog/Exceptions.fs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22
module Hedgehog.Exceptions
33

44
open System
5-
open System.Reflection
65

76
/// Recursively unwraps wrapper exceptions to get to the actual meaningful exception.
8-
/// Unwraps TargetInvocationException (from reflection) and single-inner AggregateException (from async/tasks).
7+
/// Unwraps single-inner AggregateException (from async/tasks).
98
let rec unwrap (e : exn) : exn =
109
#if FABLE_COMPILER
1110
e
1211
#else
1312
match e with
14-
| :? TargetInvocationException as tie when not (isNull tie.InnerException) ->
15-
unwrap tie.InnerException
1613
| :? AggregateException as ae when ae.InnerExceptions.Count = 1 ->
17-
unwrap ae.InnerExceptions.[0]
14+
unwrap ae.InnerExceptions[0]
1815
| _ -> e
1916
#endif

src/Hedgehog/Journal.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ namespace Hedgehog
22

33
/// Represents a single line in a property test journal with semantic meaning
44
type JournalLine =
5-
| TestParameter of name: string * value: obj // Individual xUnit test method parameter
5+
| TestParameter of name: string * value: obj // Individual test method parameter
66
| GeneratedValue of value: obj // forAll generated values (no name)
77
| Counterexample of message: string // Property.counterexample user messages
88
| Exception of exn: exn // Original exception, unwrap at render
99
| Cancellation of message: string // OperationCanceledException messages
10-
| Text of message: string // Future-proof escape hatch
10+
| Text of message: string // Plane text messages (info, etc.)
1111

1212
[<Struct>]
1313
type Journal =

src/Hedgehog/Property.fs

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ module Property =
9090
let! result = Async.AwaitTask inputTask
9191
return (Journal.empty, Success result)
9292
with
93-
| :? System.OperationCanceledException ->
93+
| :? OperationCanceledException ->
9494
return (Journal.singleton (fun () -> Cancellation "Task was canceled"), Failure)
9595
| ex ->
9696
return (Journal.exn ex, Failure)
@@ -106,7 +106,7 @@ module Property =
106106
do! Async.AwaitTask inputTask
107107
return (Journal.empty, Success ())
108108
with
109-
| :? System.OperationCanceledException ->
109+
| :? OperationCanceledException ->
110110
return (Journal.singleton (fun () -> Cancellation "Task was canceled"), Failure)
111111
| ex ->
112112
return (Journal.exn ex, Failure)
@@ -135,6 +135,10 @@ module Property =
135135
let failure : Property<unit> =
136136
Failure |> ofOutcome
137137

138+
// A failed property with a given exception recorded in the journal.
139+
let exn (ex: exn) : Property<unit> =
140+
(Journal.exn ex, Failure) |> GenLazy.constant |> ofGen
141+
138142
/// A property that discards the current test case, causing a new one to be generated.
139143
/// Use sparingly to avoid "gave up" results.
140144
let discard : Property<unit> =
@@ -291,25 +295,6 @@ module Property =
291295
let falseToFailure p =
292296
p |> map (fun b -> if not b then raise (TestReturnedFalseException()))
293297

294-
let internal printValue value : string =
295-
// sprintf "%A" is not prepared for printing ResizeArray<_> (C# List<T>) so we prepare the value instead.
296-
let prepareForPrinting (value: obj) : obj =
297-
#if FABLE_COMPILER
298-
value
299-
#else
300-
if value = null then
301-
value
302-
else
303-
let t = value.GetType()
304-
let t = System.Reflection.IntrospectionExtensions.GetTypeInfo(t)
305-
let isList = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<ResizeArray<_>>
306-
if isList
307-
then value :?> System.Collections.IEnumerable |> Seq.cast<obj> |> List.ofSeq :> obj
308-
else value
309-
#endif
310-
311-
value |> prepareForPrinting |> sprintf "%A"
312-
313298
/// Creates a property that tests whether a condition holds for all values generated by the given generator.
314299
/// Generated values are automatically added to the test journal and will be shown if the test fails.
315300
/// This is the primary way to introduce generated test data into your properties.
@@ -610,7 +595,7 @@ module Property =
610595
Size = nextSize data.Size
611596
}
612597

613-
let! journal, outcome = PropertyResult.unwrapAsync (Tree.outcome result)
598+
let! _, outcome = PropertyResult.unwrapAsync (Tree.outcome result)
614599
match outcome with
615600
| Failure ->
616601
let! status = Shrinking.shrinkAsync args.Language data config.ShrinkLimit result

0 commit comments

Comments
 (0)