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

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

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

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

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

Поскольку foo throws, я получаю следующую трассировку стека в модуле запуска модульных тестов:

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 [email protected](Unit unitVar) in ExplorationTests.fs: line 71
   at [email protected](AsyncParams`1 args)

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

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


person stmax    schedule 12.08.2013    source источник
comment
У меня была та же проблема, и Async.Catch был единственным обходным решением, которое я смог найти.   -  person Gustavo Guerra    schedule 12.08.2013
comment
Я написал Дону Сайму, который предположил, что это фундаментальное ограничение .NET и что Async.Catch — единственный вариант.   -  person John Palmer    schedule 13.08.2013
comment
@JohnPalmer звучит как ответ мне   -  person N_A    schedule 13.08.2013


Ответы (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

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

person stmax    schedule 13.08.2013

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

Опыт отладки должен улучшиться, если вы попробуете установить «Поймать исключения первого шанса» в «Отладка» -> «Исключения» -> «Исключения CLR». Отключение «Только мой код» также может помочь.

и

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

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

person John Palmer    schedule 13.08.2013
comment
привет, джон, цитата № 1, позволяющая разбивать исключения при первом шансе, на самом деле не очень помогает, так как я почти никогда не использую отладчик. мне нужны полезные трассировки стека в выводе средства запуска модульных тестов. цитата № 2 означает ли это, что я уже делаю это правильно во втором тесте (TestFooWithBetterStacktrace)? - person stmax; 13.08.2013