(これはとてつもなく長い質問です。これまでの調査から質問を分離しようとしたので、少し読みやすくなりました。)
MSTest.exe を使用して単体テストを実行しています。ときどき、次のテスト エラーが表示されます。
個別の単体テスト方法について: 「テストの実行中にエージェント プロセスが停止しました。」
テスト実行全体で:
バックグラウンド スレッドの 1 つが例外をスローしました: System.NullReferenceException: オブジェクト参照がオブジェクトのインスタンスに設定されていません。 System.Runtime.InteropServices.Marshal.ReleaseComObject (オブジェクト o) で System.Management.Instrumentation.MetaDataInfo.Dispose() で System.Management.Instrumentation.MetaDataInfo.Finalize() で
それで、ここで私がしなければならないと思うことは次のとおりです。MetaDataInfo でエラーの原因を追跡する必要がありますが、空白を描画しています。私の単体テスト スイートの実行には 30 分以上かかり、エラーは毎回発生するわけではないため、再現するのは困難です。
単体テストの実行でこの種の失敗を見た人はいますか? 特定のコンポーネントまで追跡できましたか?
編集:
テスト対象のコードは、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 クラスのすべての使用法を見つけることができました。(これは内部クラスなので、アセンブリを検索するだけで完全なリストになるはずです。) 使用される場所は 1 つだけです。
public static Guid GetMvid(Assembly assembly)
{
using (MetaDataInfo info = new MetaDataInfo(assembly))
{
return info.Mvid;
}
}
MetaDataInfo のすべての使用は適切に Disposed されているため、次のようになります。
- MetaDataInfo.importInterface が null でない場合:
- 静的メソッド GetMvid は MetaDataInfo.Mvid を返します
using
呼び出し MetaDataInfo.Dispose- COM オブジェクトのリークを破棄する
- Dispose は importInterface を null に設定します
- 呼び出しを破棄する GC.SuppressFinalize
- その後、GC が MetaDataInfo を収集するときに、ファイナライザーはスキップされます。
- .
- MetaDataInfo.importInterface が null の場合:
- 静的メソッド GetMvid は、MetaDataInfo.Mvid を呼び出す NullReferenceException を取得します。
- 例外が伝播する前に、
using
呼び出し MetaDataInfo.Dispose- 呼び出しを破棄する Marshal.ReleaseComObject
- Marshal.ReleaseComObject は NullReferenceException をスローします。
- 例外がスローされるため、Dispose は GC.SuppressFinalize を呼び出しません。
- 呼び出しを破棄する Marshal.ReleaseComObject
- 例外は、GetMvid の呼び出し元まで伝播します。
- その後、GC が MetaDataInfo を収集すると、Finalizer が実行されます。
- 通話を終了する
- 呼び出しを破棄する Marshal.ReleaseComObject
- Marshal.ReleaseComObject は NullReferenceException をスローし、それが GC まで伝搬され、アプリケーションが終了します。
- 呼び出しを破棄する Marshal.ReleaseComObject
- 通話を終了する
参考までに、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 は内部クラスであり、それを呼び出す唯一の API はAssembly.Locationの結果を渡すことにより、MetaDataInfo がマネージド アセンブリで作成されることを知っています。
ただし、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 ステートメントは逆であり、Finalizer からマネージド オブジェクトにアクセスするべきではありません。
を呼び出すことはなかったのでReleaseComObject
、COMオブジェクトをリークしていると前に言いました。.Net での COM オブジェクトの使用について詳しく調べましたが、それを正しく理解していれば、それは正しくありませんでした。COM オブジェクトは、Dispose() が呼び出されたときに解放されませんが、ガベージ コレクターがアクセスしたときに解放されます。管理対象オブジェクトであるランタイム呼び出し可能ラッパーを収集します。アンマネージ COM オブジェクトのラッパーですが、RCW は依然としてマネージ オブジェクトであり、「ファイナライザからマネージ オブジェクトにアクセスしない」という規則が引き続き適用されます。