Как получить полезную трассировку стека при тестировании асинхронных рабочих процессов F#

Я хотел бы протестировать следующий асинхронный рабочий процесс (с NUnit+FsUnit):

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

Я написал следующий тест для этого:

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

Так как Foo бросает, я получаю следующую трассировку стека в модуле модульного теста:

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

К сожалению, трассировка стека не сообщает мне, где возникло исключение. Он останавливается на RunSynchronously.

Где-то я слышал, что Async.Catch волшебным образом восстанавливает трассировку стека, поэтому я настроил свой тест:

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))

Теперь это уродливо, но, по крайней мере, дает полезную трассировку стека:

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 ExplorationTests.foo@71.Invoke(Unit unitVar) in ExplorationTests.fs: line 71
   at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@769.Invoke(AsyncParams`1 args)

На этот раз трассировка стека показывает, где именно произошла ошибка: ExplorationTests.foo@line 71

Есть ли способ избавиться от Async.Catch и соответствия между двумя вариантами, в то же время получая полезные трассировки стека? Есть ли лучший способ структурировать асинхронные тесты рабочего процесса?

2 ответа

Решение

Поскольку Async.Catch и повторное генерирование исключения кажутся единственным способом получить полезную трассировку стека, я придумал следующее:

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"

Примечание "ExceptionDispatchInfo.Capture(ex).Throw()". Это самый хороший способ перезапустить исключение, не повредив его трассировку стека (недостаток: доступно только после.NET 4.5).

Теперь я могу переписать тест "TestFooWithBetterStacktrace" следующим образом:

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

Тест выглядит намного лучше, перебрасывающий код не сосет (так же, как раньше), и я получаю полезные трассировки стека в тестере, когда что-то идет не так.

Цитируя некоторые электронные письма, которые я отправил Дону Сайму некоторое время назад:

Опыт отладки должен улучшиться, если вы попытаетесь установить "Catch First Chance Exceptions" в Debug -> Exceptions -> CLR Exceptions. Отключение "Просто мой код" также может помочь.

а также

Правильно. При использовании async { ... } вычисления не привязываются к стеку, поэтому в некоторых местах необходимо перебрасывать исключения, чтобы вернуть их в нужный поток.

Разумное использование Async.Catch или другой обработки исключений также может помочь.

Другие вопросы по тегам