Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a useful stacktrace when testing F# async workflows

I'd like to test the following async workflow (with NUnit+FsUnit):

let foo = async {
  failwith "oops"
  return 42
}

I wrote the following test for it:

let [<Test>] TestFoo () =
  foo
  |> Async.RunSynchronously
  |> should equal 42

Since foo throws I get the following stacktrace in the unit test runner:

System.Exception : oops
   at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously(CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously(FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
   at ExplorationTests.TestFoo() in ExplorationTests.fs: line 76

Unfortunately the stacktrace doesn't tell me where the exception was raised. It stops at RunSynchronously.

Somewhere I heard that Async.Catch magically restores the stacktrace, so I adjusted my test:

let [<Test>] TestFooWithBetterStacktrace () =
  foo
  |> Async.Catch
  |> Async.RunSynchronously
  |> fun x -> match x with 
              | Choice1Of2 x -> x |> should equal 42
              | Choice2Of2 ex -> raise (new System.Exception(null, ex))

Now this is ugly but at least it produces a useful stacktrace:

System.Exception : Exception of type 'System.Exception' was thrown.
  ----> System.Exception : oops
   at Microsoft.FSharp.Core.Operators.Raise(Exception exn)
   at ExplorationTests.TestFooWithBetterStacktrace() in ExplorationTests.fs: line 86
--Exception
   at Microsoft.FSharp.Core.Operators.FailWith(String message)
   at [email protected](Unit unitVar) in ExplorationTests.fs: line 71
   at [email protected](AsyncParams`1 args)

This time the stacktrace shows exactly where the error happend: ExplorationTests.foo@line 71

Is there a way to get rid of the Async.Catch and the matching between two choices while still getting useful stacktraces? Is there a better way to structure async workflow tests?

like image 566
stmax Avatar asked Aug 12 '13 16:08

stmax


1 Answers

Since Async.Catch and rethrowing the exception seem to be the only way to get a useful stacktrace I came up with the following:

type Async with
  static member Rethrow x =
    match x with 
      | Choice1Of2 x -> x
      | Choice2Of2 ex -> ExceptionDispatchInfo.Capture(ex).Throw()
                         failwith "nothing to return, but will never get here"

Note "ExceptionDispatchInfo.Capture(ex).Throw()". That's about the nicest way one can rethrow an exception without corrupting its stacktrace (downside: only available since .NET 4.5).

Now I can rewrite the test "TestFooWithBetterStacktrace" like that:

let [<Test>] TestFooWithBetterStacktrace () =
  foo
  |> Async.Catch
  |> Async.RunSynchronously
  |> Async.Rethrow
  |> should equal 42

The test looks much better, the rethrowing code doesn't suck (as much as before) and I get useful stacktraces in the test runner when something goes wrong.

like image 189
stmax Avatar answered Sep 28 '22 03:09

stmax