Pet*_*ano 6 c# extension-methods nunit exception unobserved-exception
我有一个C#Extension方法,可以与任务一起使用,以确保抛出的任何异常都是在最低限度观察到的,以免崩溃托管进程.在.NET4.5中,行为略有改变,因此不会发生这种情况,但仍会触发未观察到的异常事件.我在这里的挑战是编写一个测试来证明扩展方法是有效的.我正在使用NUnit Test Framework,ReSharper是测试运行者.
我试过了:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(() =>
{
throw new NaiveTimeoutException();
return new DateTime?();
});
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
Run Code Online (Sandbox Code Playgroud)
测试总是失败的Assert.IsTrue.当我手动运行此测试时,在LINQPad之类的东西中,我得到了预期的wasUnobservedException回复行为true.
我猜测测试框架正在捕获异常并观察它,以至于TaskScheduler.UnobservedTaskException永远不会被触发.
我尝试修改代码如下:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(async () =>
{
await TaskEx.Delay(5000).WithTimeout(1000).Wait();
return new DateTime?();
});
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
Run Code Online (Sandbox Code Playgroud)
我在此代码中进行的尝试是在抛出异常之前使任务获得GC'd,以便终结器将看到未捕获的,未观察到的异常.然而,这导致了上述相同的故障.
事实上,是否存在某种由测试框架连接的异常处理程序?如果是这样,有办法吗?或者我只是把事情弄得一团糟,有更好/更容易/更清洁的方法吗?
我看到这种方法存在一些问题.
首先,有一个确定的竞争条件.当TaskEx.Run返回任务时,它只是将请求排队到线程池; 任务尚未完成.
其次,你遇到了一些垃圾收集器细节.在调试中编译时 - 实际上,无论编译器感觉如何 - 局部变量的生命周期(即res)都会扩展到方法的末尾.
考虑到这两个问题,我能够通过以下代码:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
Run Code Online (Sandbox Code Playgroud)
但是,仍然存在两个问题:
(技术上)仍有竞争条件.尽管UnobservedTaskException由于任务终结器而引发,但无法保证AFAIK 从任务终结器中引发它.目前,它似乎是,但在我看来,这似乎是一个非常不稳定的解决方案(考虑到限制终结器应该是多少).因此,在框架的未来版本中,我不会太惊讶地知道终结器只是将一个队列排队UnobservedTaskException到线程池而不是直接执行它.在这种情况下,您不能再依赖于事件在任务完成时处理的事实(上面的代码所做的隐式假设).
UnobservedTaskException在单元测试中修改全局状态()也存在问题.
考虑到这两个问题,我最终得到:
var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
if (!mre.WaitOne(10000))
Assert.Fail();
}
finally
{
TaskScheduler.UnobservedTaskException -= subscription;
}
Run Code Online (Sandbox Code Playgroud)
考虑到它的复杂性,它也通过但是值得怀疑.
小智 5
只是为@Stephen Cleary 的解决方案添加一个补充(请不要赞成我的答案)。
正如他之前提到的,在“调试”模式下编译时,局部变量的生命周期会延长到方法的末尾,因此建议的解决方案仅在代码以“发布”模式(未附加调试器)编译时有效。
如果你真的需要对这个行为进行单元测试(或者让它在“调试”模式下工作),你可以“欺骗”GC,将启动任务的代码放在一个动作(或本地函数)中。这样做,在调用操作后,任务将可供 GC 收集。
var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
Action runTask = () =>
{
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
};
runTask.Invoke();
GC.Collect();
GC.WaitForPendingFinalizers();
if (!mre.WaitOne(10000))
Assert.Fail();
}
finally
{
TaskScheduler.UnobservedTaskException -= subscription;
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
863 次 |
| 最近记录: |