(我知道,这是一个非常长的问题。到目前为止,我试图将这个问题与我的调查分开,所以它更容易阅读。)
我正在使用 MSTest.exe 运行我的单元测试。有时,我会看到此测试错误:
关于单个单元测试方法:“在测试运行时代理进程已停止。”
在整个测试运行中:
后台线程之一引发异常: System.NullReferenceException:对象引用未设置为对象的实例。 在 System.Runtime.InteropServices.Marshal.ReleaseComObject(对象 o) 在 System.Management.Instrumentation.MetaDataInfo.Dispose() 在 System.Management.Instrumentation.MetaDataInfo.Finalize()
所以,这就是我认为我需要做的事情:我需要找出导致 MetaDataInfo 错误的原因,但我正在画一个空白。我的单元测试套件运行需要半个多小时,而且错误不是每次都发生,所以很难让它重现。
有没有其他人在运行单元测试时看到过这种类型的失败?您是否能够将其追踪到特定组件?
编辑:
被测代码混合了 C#、C++/CLI 和一些非托管 C++ 代码。非托管 C++ 仅在 C++/CLI 中使用,从不直接从单元测试中使用。单元测试都是 C#。
被测代码将在独立的 Windows 服务中运行,因此 ASP.net 或类似的东西没有任何复杂性。在被测代码中,有线程启动和停止、网络通信和本地硬盘驱动器的文件 I/O。
到目前为止我的调查:
我花了一些时间在我的 Windows 7 机器上挖掘 System.Management 程序集的多个版本,并在我的 Windows 目录中的 System.Management 中找到了 MetaDataInfo 类。(Program Files\Reference Assemblies 下的版本要小得多,并且没有 MetaDataInfo 类。)
使用 Reflector 检查这个程序集,我发现 MetaDataInfo.Dispose() 中似乎有一个明显的错误:
// From class System.Management.Instrumentation.MetaDataInfo:
public void Dispose()
{
if (this.importInterface == null) // <---- Should be "!="
{
Marshal.ReleaseComObject(this.importInterface);
}
this.importInterface = null;
GC.SuppressFinalize(this);
}
向后使用此“if”语句,MetaDataInfo 将泄漏 COM 对象(如果存在),或者抛出 NullReferenceException(如果不存在)。我已经在 Microsoft Connect 上报告了这一点:https ://connect.microsoft.com/VisualStudio/feedback/details/779328/
使用反射器,我能够找到 MetaDataInfo 类的所有用途。(它是一个内部类,所以只搜索程序集应该是一个完整的列表。)它只有一个地方使用:
public static Guid GetMvid(Assembly assembly)
{
using (MetaDataInfo info = new MetaDataInfo(assembly))
{
return info.Mvid;
}
}
由于 MetaDataInfo 的所有使用都被正确处理,这就是正在发生的事情:
- 如果 MetaDataInfo.importInterface 不为空:
- 静态方法 GetMvid 返回 MetaDataInfo.Mvid
using
调用 MetaDataInfo.Dispose- Dispose 泄漏 COM 对象
- Dispose 将 importInterface 设置为 null
- Dispose 调用 GC.SuppressFinalize
- 稍后,当 GC 收集 MetaDataInfo 时,会跳过终结器。
- .
- 如果 MetaDataInfo.importInterface 为空:
- 静态方法 GetMvid 获取调用 MetaDataInfo.Mvid 的 NullReferenceException。
- 在异常向上传播之前,
using
调用 MetaDataInfo.Dispose- Dispose 调用 Marshal.ReleaseComObject
- Marshal.ReleaseComObject 引发 NullReferenceException。
- 因为抛出异常,Dispose 不会调用 GC.SuppressFinalize
- Dispose 调用 Marshal.ReleaseComObject
- 异常传播到 GetMvid 的调用者。
- 稍后,当 GC 收集到 MetaDataInfo 时,它会运行 Finalizer
- 完成调用 Dispose
- Dispose 调用 Marshal.ReleaseComObject
- Marshal.ReleaseComObject 抛出一个 NullReferenceException,它一直传播到 GC,应用程序被终止。
- Dispose 调用 Marshal.ReleaseComObject
- 完成调用 Dispose
对于它的价值,这里是来自 MetaDataInfo 的其余相关代码:
public MetaDataInfo(string assemblyName)
{
Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value);
// The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44"
IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser();
this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid);
Marshal.ReleaseComObject(o);
}
private void InitNameAndMvid()
{
if (this.name == null)
{
uint num;
StringBuilder szName = new StringBuilder {
Capacity = 0
};
this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
szName.Capacity = (int) num;
this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
this.name = szName.ToString();
}
}
public Guid Mvid
{
get
{
this.InitNameAndMvid();
return this.mvid;
}
}
编辑2:
我能够在 Microsoft 的 MetaDataInfo 类中重现该错误。但是,我的复制与我在这里看到的问题略有不同。
- 复制:我尝试在不是托管程序集的文件上创建 MetaDataInfo 对象。
importInterface
这会在初始化之前从构造函数中引发异常。 - 我对 MSTest 的问题:MetaDataInfo 是在某个托管程序集上构造的,并且在初始化
之前发生了一些事情使
importInterface
null 或退出构造函数。importInterface
- 我知道 MetaDataInfo 是在托管程序集上创建的,因为 MetaDataInfo 是一个内部类,并且唯一调用它的 API 是通过传递Assembly.Location的结果来实现的。
但是,在 Visual Studio 中重新创建问题意味着它为我将源下载到 MetaDataInfo。这是实际代码,以及原始开发人员的评论。
public void Dispose()
{
// We implement IDisposable on this class because the IMetaDataImport
// can be an expensive object to keep in memory.
if(importInterface == null)
Marshal.ReleaseComObject(importInterface);
importInterface = null;
GC.SuppressFinalize(this);
}
~MetaDataInfo()
{
Dispose();
}
原始代码证实了在反射器中看到的内容:if 语句是向后的,它们不应该从终结器访问托管对象。
我之前说过,因为它从来没有调用ReleaseComObject
,它正在泄漏 COM 对象。我阅读了更多关于 .Net 中使用 COM 对象的信息,如果我理解正确,那是不正确的:调用 Dispose() 时 COM 对象没有被释放,但是当垃圾收集器到达时它被释放收集运行时可调用包装器,它是一个托管对象。尽管它是一个非托管 COM 对象的包装器,但 RCW 仍然是一个托管对象,并且关于“不要从终结器访问托管对象”的规则仍然应该适用。