Skip to content

Commit 87e418b

Browse files
authored
Merge pull request #470 from hedgehogqa/tagged-journal
Tagged journal
2 parents 06a84ae + 60fad8d commit 87e418b

File tree

19 files changed

+330
-304
lines changed

19 files changed

+330
-304
lines changed

src/Hedgehog.Xunit/AutoGenConfig.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type %s{configType.Name} =
2222
let methodInfo =
2323
if methodInfo.IsGenericMethod then
2424
methodInfo.GetParameters()
25-
|> Array.map (_.ParameterType.IsGenericParameter)
25+
|> Array.map _.ParameterType.IsGenericParameter
2626
|> Array.zip configArgs
2727
|> Array.filter snd
2828
|> Array.map (fun (arg, _) -> arg.GetType())

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: 14 additions & 52 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,33 +117,6 @@ let dispose (o: obj) =
120117
| :? IDisposable as d -> d.Dispose()
121118
| _ -> ()
122119

123-
// ========================================
124-
// Value Formatting & Display
125-
// ========================================
126-
127-
let printValue (value: obj) : string =
128-
let prepareForPrinting (value: obj) : obj =
129-
if isNull value then
130-
value
131-
else
132-
let typeInfo = IntrospectionExtensions.GetTypeInfo(value.GetType())
133-
let isResizeArray = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() = typedefof<ResizeArray<_>>
134-
if isResizeArray then
135-
value :?> System.Collections.IEnumerable
136-
|> Seq.cast<obj>
137-
|> List.ofSeq
138-
:> obj
139-
else
140-
value
141-
142-
value |> prepareForPrinting |> sprintf "%A"
143-
144-
let formatParametersWithNames (parameters: ParameterInfo[]) (values: obj list) : string =
145-
Array.zip parameters (List.toArray values)
146-
|> Array.map (fun (param, value) ->
147-
$"%s{param.Name} = %s{printValue value}")
148-
|> String.concat Environment.NewLine
149-
150120
// ========================================
151121
// Configuration Helpers
152122
// ========================================
@@ -219,16 +189,8 @@ module private PropertyBuilder =
219189
else
220190
testMethod
221191

222-
try
223-
methodToInvoke.Invoke(testClassInstance, args |> Array.ofList)
224-
with
225-
| :? TargetInvocationException as tie when not (isNull tie.InnerException) ->
226-
// Unwrap reflection exception to show the actual user exception instead of TargetInvocationException.
227-
// We use ExceptionDispatchInfo.Capture().Throw() to preserve the original stack trace.
228-
// Note: This adds a "--- End of stack trace from previous location ---" marker
229-
// and appends additional frames as the exception propagates, which we filter out later.
230-
ExceptionDispatchInfo.Capture(tie.InnerException).Throw()
231-
failwith "unreachable"
192+
methodToInvoke.Invoke(testClassInstance, args |> Array.ofList)
193+
232194

233195
/// Creates a property based on the test method's return type
234196
let createProperty
@@ -243,23 +205,23 @@ module private PropertyBuilder =
243205
invokeTestMethod testMethod testClassInstance args
244206
finally
245207
List.iter dispose args
246-
with e ->
247-
// If the test method throws an exception, we need to handle it
248-
// For Property<_> return types, the exception will be caught by Property.map
249-
// For other return types, we need to wrap it in a failing property
250-
// We return a special marker that wrapReturnValue will recognize
251-
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
252214

253215
let createJournal args =
254-
let formattedParams = formatParametersWithNames parameters args
255-
Journal.singleton (fun () -> formattedParams)
216+
args
217+
|> Seq.zip parameters
218+
|> Seq.map (fun (param, value) -> fun () -> TestParameter (param.Name, value))
219+
|> Array.ofSeq // not sure if journal will do multiple enumerations
220+
|> Journal.ofSeq
256221

257222
let wrapWithExceptionHandling (result: obj) : Property<unit> =
258223
match result with
259-
| :? exn as e ->
260-
// Exception was thrown - create a failing property
261-
Property.counterexample (fun () -> string e)
262-
|> Property.bind (fun () -> Property.failure)
224+
| :? exn as e -> Property.exn e
263225
| _ -> wrapReturnValue result
264226

265227

src/Hedgehog.Xunit/Prelude.fs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ module Array =
2323
(first, middle, Some last)
2424

2525
module Seq =
26-
let inline tryMin xs =
27-
if Seq.isEmpty xs then None else Some (Seq.min xs)
28-
2926
// https://github.com/dotnet/fsharp/blob/b9942004e8ba19bf73862b69b2d71151a98975ba/src/FSharp.Core/seqcore.fs#L172-L174
3027
let inline private checkNonNull argName arg =
3128
if isNull arg then
@@ -51,25 +48,3 @@ module internal Type =
5148
|> Seq.tryFind (fun attr -> attr :? 'T)
5249
|> Option.map (fun attr -> attr :?> 'T))
5350
|> Seq.toList
54-
55-
[<AutoOpen>]
56-
module StringBuilder =
57-
open System.Text
58-
59-
type StringBuilder with
60-
/// Appends each string in the sequence with indentation
61-
member this.AppendIndentedLine(indent: string, lines: #seq<string>) =
62-
lines |> Seq.iter (fun line -> this.Append(indent).AppendLine(line) |> ignore)
63-
this
64-
65-
/// Splits text into lines and appends each with indentation
66-
member this.AppendIndentedLine(indent: string, text: string) =
67-
let lines = text.Split([|'\n'; '\r'|], StringSplitOptions.None)
68-
this.AppendIndentedLine(indent, lines)
69-
70-
member this.AppendLines(lines: #seq<string>) =
71-
this.AppendJoin(Environment.NewLine, lines)
72-
73-
/// Returns the string content with trailing whitespace removed
74-
member this.ToStringTrimmed() =
75-
this.ToString().TrimEnd()

src/Hedgehog.Xunit/ReportFormatter.fs

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

55
open Hedgehog
66
open Hedgehog.Xunit
7-
open System
8-
open System.Text
9-
10-
// ========================================
11-
// Report Formatting
12-
// ========================================
13-
14-
/// Filters exception string to show only user code stack trace.
15-
/// When we rethrow using ExceptionDispatchInfo.Capture().Throw() to preserve the original stack trace,
16-
/// it adds a "--- End of stack trace from previous location ---" marker and appends Hedgehog's
17-
/// internal frames as the exception propagates. We remove everything from that marker onwards
18-
/// to show only the user's code in the test failure report.
19-
let private filterExceptionStackTrace (exceptionEntry: string) : string =
20-
match exceptionEntry.IndexOf("--- End of stack trace from previous location ---") with
21-
| -1 -> exceptionEntry // No marker found, return as-is
22-
| idx -> exceptionEntry.Substring(0, idx).TrimEnd()
23-
24-
let private formatFailureForXunit (failure: FailureData) (report: Report) : string =
25-
let sb = StringBuilder()
26-
let indent = " " // 2 spaces to align with xUnit's output format
27-
28-
let renderTests (tests: int<tests>) =
29-
sprintf "%d test%s" (int tests) (if int tests = 1 then "" else "s")
30-
31-
let renderAndShrinks (shrinks: int<shrinks>) =
32-
if int shrinks = 0 then
33-
""
34-
else
35-
sprintf " and %d shrink%s" (int shrinks) (if int shrinks = 1 then "" else "s")
36-
37-
let renderAndDiscards (discards: int<discards>) =
38-
if int discards = 0 then
39-
""
40-
else
41-
sprintf " and %d discard%s" (int discards) (if int discards = 1 then "" else "s")
42-
43-
// Header
44-
sb.AppendIndentedLine(
45-
indent,
46-
sprintf
47-
"*** Failed! Falsifiable (after %s%s%s):"
48-
(renderTests report.Tests)
49-
(renderAndShrinks failure.Shrinks)
50-
(renderAndDiscards report.Discards)
51-
)
52-
|> ignore
53-
54-
// Journal structure: first=parameters, middle=entries (optional), last=exception (always present on failure)
55-
let journalEntries = Journal.eval failure.Journal |> Array.ofSeq
56-
57-
let parametersEntry, entries, exceptionEntryOpt =
58-
Array.splitFirstMiddleLast journalEntries
59-
60-
// Parameters section
61-
sb.AppendLine() |> ignore
62-
63-
if String.IsNullOrWhiteSpace(parametersEntry) then
64-
sb.AppendLine("Test doesn't take parameters") |> ignore
65-
else
66-
sb.AppendLine("Input parameters:").AppendIndentedLine(indent, parametersEntry)
67-
|> ignore
68-
69-
// Middle entries section (user's debug info from Property.counterexample, etc.)
70-
if entries.Length > 0 then
71-
sb.AppendLine().AppendLines(entries) |> ignore
72-
73-
// Recheck seed (if available)
74-
match failure.RecheckInfo with
75-
| Some recheckInfo ->
76-
let serialized = RecheckData.serialize recheckInfo.Data
77-
sb.AppendLine().AppendLine($"Recheck seed: \"%s{serialized}\"") |> ignore
78-
| None -> ()
79-
80-
// Exception section (filtered to show only user code)
81-
match exceptionEntryOpt with
82-
| Some exceptionEntry ->
83-
let filteredEntry = filterExceptionStackTrace exceptionEntry
84-
85-
sb.AppendLine().AppendLine("Actual exception:").AppendLine(filteredEntry)
86-
|> ignore
87-
| None -> ()
88-
89-
sb.ToStringTrimmed()
90-
91-
let private formatReportForXunit (report: Report) : string =
92-
match report.Status with
93-
| Failed failure -> formatFailureForXunit failure report
94-
| _ -> Report.render report
957

968
let tryRaise (report: Report) : unit =
979
match report.Status with
98-
| Failed _ -> report |> formatReportForXunit |> PropertyFailedException |> raise
10+
| Failed _ -> report |> Report.render |> PropertyFailedException |> raise
9911
| _ -> 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/Hedgehog.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Failures are automatically simplified, giving developers coherent, intelligible
3030
<ItemGroup>
3131
<Compile Include="AutoOpen.fs" />
3232
<Compile Include="Exceptions.fs" />
33+
<Compile Include="ValueFormatting.fs" />
3334
<Compile Include="Numeric.fs" />
3435
<Compile Include="Seed.fs" />
3536
<Compile Include="Seq.fs" />

src/Hedgehog/Journal.fs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,46 @@
11
namespace Hedgehog
22

3+
/// Represents a single line in a property test journal with semantic meaning
4+
type JournalLine =
5+
| TestParameter of name: string * value: obj // Individual test method parameter
6+
| GeneratedValue of value: obj // forAll generated values (no name)
7+
| Counterexample of message: string // Property.counterexample user messages
8+
| Exception of exn: exn // Original exception, unwrap at render
9+
| Cancellation of message: string // OperationCanceledException messages
10+
| Text of message: string // Plane text messages (info, etc.)
11+
312
[<Struct>]
413
type Journal =
5-
| Journal of seq<unit -> string>
14+
| Journal of seq<unit -> JournalLine>
615

716
module Journal =
817

918
/// Creates a journal from a sequence of entries.
10-
let ofSeq (entries : seq<unit -> string>) : Journal =
19+
let ofSeq (entries : seq<unit -> JournalLine>) : Journal =
1120
Journal entries
1221

13-
/// Evaluates a single entry, returning it's message.
14-
let private evalEntry (f : unit -> string) : string =
22+
/// Evaluates a single entry, returning the journal line.
23+
let private evalEntry (f : unit -> JournalLine) : JournalLine =
1524
f()
1625

17-
/// Evaluates all entries in the journal, returning their messages.
18-
let eval (Journal entries : Journal) : seq<string> =
26+
/// Evaluates all entries in the journal, returning their journal lines.
27+
let eval (Journal entries : Journal) : seq<JournalLine> =
1928
Seq.map evalEntry entries
2029

2130
/// Represents a journal with no entries.
2231
let empty : Journal =
2332
ofSeq []
2433

25-
/// Creates a single entry journal from a given message.
34+
/// Creates a single entry journal from a given message as Text.
2635
let singletonMessage (message : string) : Journal =
27-
ofSeq [ fun () -> message ]
36+
ofSeq [ fun () -> Text message ]
2837

2938
/// Adds exception to the journal as a single entry.
3039
let exn (error: exn): Journal =
31-
singletonMessage (string (Exceptions.unwrap error))
40+
ofSeq [ fun () -> Exception error ]
3241

3342
/// Creates a single entry journal from a given entry.
34-
let singleton (entry : unit -> string) : Journal =
43+
let singleton (entry : unit -> JournalLine) : Journal =
3544
ofSeq [ entry ]
3645

3746
/// Creates a journal composed of entries from two journals.

0 commit comments

Comments
 (0)