Как получить полезную трассировку стека при тестировании асинхронных рабочих процессов 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 или другой обработки исключений также может помочь.