Skip to content

Commit 0eb85ba

Browse files
authored
Merge pull request #60 from messerli-informatik-ag/result-stack-trace
Added a valid stack trace to the exception in the Result monad
2 parents 1a80085 + d63e9ed commit 0eb85ba

File tree

6 files changed

+102
-2
lines changed

6 files changed

+102
-2
lines changed

Funcky.Test/FunctionalAssert.cs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Funcky.Test
2+
{
3+
internal class FunctionalAssert
4+
{
5+
public static void Unmatched()
6+
=> throw new UnmatchedException();
7+
8+
public static void Unmatched(string unmatchedCase)
9+
=> throw new UnmatchedException(unmatchedCase);
10+
}
11+
}

Funcky.Test/ResultTest.cs

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
using System;
1+
using System;
22
using System.IO;
3+
using System.Linq;
34
using Funcky.Monads;
45
using Xunit;
56
using static Funcky.Functional;
@@ -99,5 +100,49 @@ public static TheoryData<Result<int>, bool> GetResults()
99100
{ Result<int>.Ok(42), true },
100101
{ Result<int>.Error(new InvalidCastException()), false },
101102
};
103+
104+
[Fact]
105+
public void GivenAResultWithAnExceptionWeGetAStackTrace()
106+
{
107+
var arbitrayNumberOfStackFrames = 3;
108+
109+
InterestingStackTrace(arbitrayNumberOfStackFrames)
110+
.Match(
111+
ok: v => FunctionalAssert.Unmatched("ok"),
112+
error: e => Assert.NotNull(e.StackTrace));
113+
}
114+
115+
[Fact]
116+
public void GivenAResultWithAnExceptionTheStackTraceStartsInCreationMethod()
117+
{
118+
var arbitrayNumberOfStackFrames = 0;
119+
120+
InterestingStackTrace(arbitrayNumberOfStackFrames)
121+
.Match(
122+
ok: v => FunctionalAssert.Unmatched("ok"),
123+
error: IsInterestingStackTraceFirst);
124+
}
125+
126+
private void IsInterestingStackTraceFirst(Exception exception)
127+
{
128+
if (exception.StackTrace is { })
129+
{
130+
var lines = exception.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
131+
132+
Assert.StartsWith(" at Funcky.Test.ResultTest.InterestingStackTrace(Int32 n)", lines.First());
133+
}
134+
else
135+
{
136+
FunctionalAssert.Unmatched("else");
137+
}
138+
}
139+
140+
private Result<int> InterestingStackTrace(int n)
141+
=> n == 0
142+
? Result<int>.Error(new InvalidCastException())
143+
: Indirection(n - 1);
144+
145+
private Result<int> Indirection(int n)
146+
=> InterestingStackTrace(n);
102147
}
103148
}

Funcky.Test/UnmatchedException.cs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace Funcky.Test
4+
{
5+
internal class UnmatchedException : Exception
6+
{
7+
public UnmatchedException()
8+
: base("Wrong pattern matched.")
9+
{
10+
}
11+
12+
public UnmatchedException(string? unmatchedCase)
13+
: base($"Wrong pattern matched: the case '{unmatchedCase}' has been matched accidentally.")
14+
{
15+
}
16+
}
17+
}

Funcky/Monads/ExceptionUtilities.cs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Reflection;
4+
5+
namespace Funcky.Monads
6+
{
7+
internal static class ExceptionUtilities
8+
{
9+
private static readonly FieldInfo STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
10+
private static readonly Type TRACE_FORMAT_TI = Type.GetType("System.Diagnostics.StackTrace").GetNestedType("TraceFormat", BindingFlags.NonPublic);
11+
private static readonly MethodInfo TRACE_TO_STRING_MI = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { TRACE_FORMAT_TI }, null);
12+
13+
public static Exception SetStackTrace(this Exception target, StackTrace stack)
14+
{
15+
var getStackTraceString = TRACE_TO_STRING_MI.Invoke(stack, new object[] { Enum.GetValues(TRACE_FORMAT_TI).GetValue(0) });
16+
STACK_TRACE_STRING_FI.SetValue(target, getStackTraceString);
17+
return target;
18+
}
19+
}
20+
}

Funcky/Monads/Result.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using System;
2+
using System.Diagnostics;
23

34
namespace Funcky.Monads
45
{
56
public readonly struct Result<TValidResult>
67
{
8+
private const int SkipLowestStackFrame = 1;
9+
710
private readonly TValidResult _result;
811
private readonly Exception _error;
912

@@ -32,6 +35,8 @@ public static Result<TValidResult> Ok(TValidResult item)
3235

3336
public static Result<TValidResult> Error(Exception item)
3437
{
38+
ExceptionUtilities.SetStackTrace(item, new StackTrace(SkipLowestStackFrame, true));
39+
3540
return new Result<TValidResult>(item);
3641
}
3742

changelog.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@
1717
* Added overload for AndThen which flattens the Option
1818
* Add `Where` method to `Option<T>`, which allows filtering the `Option` by a predicate.
1919
* Add overload for `Option<T>.SelectMany` that takes only a selector.
20-
20+
21+
## Unpublished
22+
* `Exception` created by `Result` monad contains valid stack trace

0 commit comments

Comments
 (0)