18

相互に参照する C# プロジェクトと C++/CLI プロジェクトの両方を含む大規模な .NET ソリューションがあります。また、いくつかの単体テスト プロジェクトもあります。最近、Visual Studio 2010 & .NET 4.0 から Visual Studio 4.5 & .NET 4.5 にアップグレードしましたが、単体テストを実行しようとすると、テスト中に一部の DLL の読み込みに問題があるようです。

この問題は、単体テストが別の AppDomain で実行されるために発生するようです。ユニット テスト プロセス (たとえば、nunit-agent.exe) は、テスト プロジェクトの場所に設定された AppBase を使用して新しい AppDomain を作成しますが、Fusion Log によると、一部の DLL は、AppDomain の AppBase ではなく、AppBase として nunit の実行可能ファイルのディレクトリと共にロードされます。 .

新しい AppDomain を作成し、そこでテストを実行しようとする、より単純なシナリオで問題を再現することができました。これがどのように見えるかです(無実を保護するために、ユニットテストクラス、メソッド、およびdllの場所の名前を変更しました):

class Program
{
    static void Main(string[] args)
    {

        var setup = new AppDomainSetup {
            ApplicationBase = "C:\\DirectoryOfMyUnitTestDll\\"
        };

        AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
        ObjectHandle handle = Activator.CreateInstanceFrom(domain, typeof(TestRunner).Assembly.CodeBase, typeof(TestRunner).FullName);
        TestRunner runner = (TestRunner)handle.Unwrap();
        runner.Run();

        AppDomain.Unload(domain);
    }

}

public class TestRunner : MarshalByRefObject
{
    public void Run()
    {
        try
        {
            HtmlTransformerUnitTest test = new HtmlTransformerUnitTest();
            test.SetUp();
            test.Transform_HttpEquiv_Refresh_Timeout();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

これは、単体テストを実行しようとしたときに発生する例外です。ご覧のとおり、C++ dll が初期化され、C# dll をロードしようとすると問題が発生します (関連する DLL の名前を CPlusPlusDll および CSharpDll に変更しました)。

System.TypeInitializationException: '' の型初期化子が例外をスローしました。
 ---> .ModuleLoadExceptionHandlerException: ネストされた例外が、C++ モジュールの読み込みに失敗する原因となったプライマリ例外の後に発生しました。
 ---> System.TypeInitializationException: '' の型初期化子が例外をスローしました。
 ---> .ModuleLoadException: vtable の初期化中に C++ モジュールをロードできませんでした。
 ---> System.IO.FileNotFoundException: ファイルまたはアセンブリ 'CSharpDll, Version=8.80.0.0, Culture=neutral, PublicKeyToken=null' またはその依存関係の 1 つを読み込めませんでした。システムは、指定されたファイルを見つけることができません。
   ?A0xb992d574.??__E??_7CAppletAction@CPlusPlusDll@SomeNamespace@@6B@@@YMXXZ() で
   at _initterm_m((fnptr)* pfbegin, (fnptr)* pfend) in f:\dd\vctools\crt_bld\self_x86\crt\src\puremsilcode.cpp:line 219
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 331 の .LanguageSupport.InitializeVtables(LanguageSupport*) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 491 の .LanguageSupport._Initialize(LanguageSupport*) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 702 の .LanguageSupport.Initialize(LanguageSupport*) で
   --- 内部例外スタック トレースの終了 ---
   f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 194 の .ThrowModuleLoadException (文字列 errorMessage、例外 innerException) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 712 の .LanguageSupport.Initialize(LanguageSupport*) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 754 の .cctor() で
   --- 内部例外スタック トレースの終了 ---
   System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal (Int32 errorCode、IntPtr errorInfo) で
   System.Runtime.InteropServices.Marshal.ThrowExceptionForHR (Int32 errorCode) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 406 の .DoCallBackInDefaultDomain (IntPtr 関数、Void* cookie) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 277 の .DefaultDomain.Initialize() で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 342 の .LanguageSupport.InitializeDefaultAppDomain(LanguageSupport*) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 539 の .LanguageSupport._Initialize(LanguageSupport*) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 702 の .LanguageSupport.Initialize(LanguageSupport*) で
   --- 内部例外スタック トレースの終了 ---
   f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 184 の .ThrowNestedModuleLoadException (例外 innerException、例外 nestedException) で
   .LanguageSupport.Cleanup (LanguageSupport*、例外 innerException) で f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 662
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 710 の .LanguageSupport.Initialize(LanguageSupport*) で
   f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 754 の .cctor() で
   --- 内部例外スタック トレースの終了 ---

これは、Fusion ログに表示されているものです (DLL の名前を元の名前ではなく SomeDLL.dll に変更しました)。

*** アセンブリ バインダー ログ エントリ (2013 年 8 月 1 日 @ 01:47:48 PM) ***

操作に失敗しました。
バインド結果: hr = 0x80070002。システムは、指定されたファイルを見つけることができません。

アセンブリ マネージャーの読み込み元: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
実行可能ファイル c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe の下で実行
--- 詳細なエラー ログが続きます。

=== プレバインド状態情報 ===
ログ: ユーザー = WF-IL\yshany
ログ: DisplayName = SomeDLL、バージョン = 8.80.0.0、カルチャ = ニュートラル、PublicKeyToken = null
 (完全指定)
ログ: Appbase = file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/
ログ: 初期 PrivatePath = NULL
ログ: 動的ベース = NULL
ログ: キャッシュ ベース = NULL
ログ: AppName = MyTester.exe
呼び出しアセンブリ : (不明)。
===
ログ: このバインドはデフォルトのロード コンテキストで開始されます。
ログ: アプリケーション構成ファイルの使用: c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe.Config
ログ: ホスト構成ファイルの使用:
ログ: C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config のマシン構成ファイルを使用しています。
ログ: 現時点ではポリシーが参照に適用されていません (プライベート、カスタム、部分、または場所に基づくアセンブリ バインド)。
ログ: 新しい URL ファイルのダウンロードを試みています:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL.DLL.
ログ: 新しい URL ファイルのダウンロードを試みています:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL/SomeDLL.DLL.
ログ: 新しい URL ファイルのダウンロードを試みています:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL.EXE.
ログ: 新しい URL ファイルのダウンロードを試みています:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL/SomeDLL.EXE.
LOG: すべてのプローブ URL が試行され、失敗しました。

ご覧のとおり、問題は、AppBase が SomeDLL.dll が存在する場所 (単体テスト dll と同じ場所) ではなく、MyTester.exe が存在する場所であることです。これは、上記の例外に記載されている両方の DLL を含むいくつかの DLL で発生します。

また、より単純な単体テスト プロジェクト (3 つのプロジェクトを含む小さな VS2012 ソリューション - 別の C# プロジェクトを参照する C++/CLI プロジェクトを参照する C# プロジェクト) で再現しようとしましたが、問題は再現されず、完全に機能しました。前に述べたように、VS2012 & .NET 4.5 にアップグレードする前の単体テストは問題ありませんでした。

私に何ができる?ありがとう!

4

1 に答える 1

15

これは .NET 4.5 のバグのようです。

NUnit は、単体テストを実行するための新しいアプリ ドメインを作成します。単体テスト アセンブリまたはその参照のいずれかが混合モード アセンブリである場合、特定の条件下では、既定のアプリ ドメインでも混合モード アセンブリの参照を読み込もうとします。

ランタイムは、そのアセンブリで他の処理を行う前に、混合モード アセンブリのアンマネージ C++ コードを初期化する必要があります。これは、自動的にコンパイルされた LanguageSupport クラスを介して行われます (このソース コードは Visual Studio と共に配布されます)。NUnit によって作成された appdomain のコンテキストでLanguageSupport::Initialize、混合モードの単体テスト アセンブリのコンパイラによって生成されたクラスの静的コンストラクターで最初に実行されます。.module次に、LanguageSupport はデフォルトの appdomain で同じ静的コンストラクターを再トリガーし、最終的にLanguageSupport::Initialize再度呼び出します。上記と同じコール スタックからエラー処理を除いたものを次に示します。

   at _initterm_m((fnptr)* pfbegin, (fnptr)* pfend)
   at .LanguageSupport.InitializeVtables(LanguageSupport* )
   at .LanguageSupport._Initialize(LanguageSupport* )
   at .LanguageSupport.Initialize(LanguageSupport* )
   at .LanguageSupport.Initialize(LanguageSupport* )
   at .DoCallBackInDefaultDomain(IntPtr function, Void* cookie)
   at .LanguageSupport.InitializeDefaultAppDomain(LanguageSupport* )
   at .LanguageSupport._Initialize(LanguageSupport* )
   at .LanguageSupport.Initialize(LanguageSupport* )
   at .LanguageSupport.Initialize(LanguageSupport* )

NUnit が作成する appdomain は、実際には単体テスト アセンブリとその参照の読み込みに成功していますが (他に問題がない場合)、既定の appdomain での 2 番目の LanguageSupport 初期化は失敗しています。

混合モード アセンブリの IL をダンプすると、アンマネージ クラスの一部に静的イニシャライザ メソッドが自動的に生成されていることがわかりました。これらは、コール スタックの最上部から 2 番目にある InitializeVtables メソッドで呼び出されるメソッドの 1 つです。いくつかの試行錯誤のコンパイルの後、アンマネージ クラスにコンストラクターと、署名に .NET 型を含む少なくとも 1 つの仮想メソッドがある場合、コンパイラーはクラスの静的初期化子を発行することを発見しました。

LanguageSupport::InitializeVtablesこれらの静的初期化関数を呼び出します。イニシャライザが実行されると、アンマネージ クラスの仮想メソッドのシグネチャにあるインポートされた型を含む参照を CLR が読み込もうとするようです。既定の appdomain には単体テスト アセンブリとその参照がアプリ​​ケーション ベースにないため、呼び出しは失敗し、上記のエラーが生成されます。

さらに、(とにかく私が作成したおもちゃのアプリで) エラーが発生するのは、別の非 vtable 初期化子が実行されている場合のみです。

私のアプリの関連部分は次のとおりです。

class DomainDumper {
public:
   DomainDumper() {
      Console::WriteLine("Dumper called from appdomain {0}", 
         AppDomain::CurrentDomain->Id);
   }
};

// comment out this line and InitializeVtables succeeds in default appdomain
DomainDumper dumper;

class CppClassUsingManagedRef {
public:
   // comment out this line and the dynamic vtable initializer doesn't get created
   CppClassUsingManagedRef(){}

   virtual void VirtualMethodWithNoArgs() {}

   // comment out this line and the dynamic vtable initializer doesn't get created
   virtual void VirtualMethodWithImportedTypeRef(ReferredToClassB^ bref) {}

   void MethodWithImportedTypeRef(ReferredToClassB^ bref) {}
};

回避策:

  • 単体テストが NUnit 実行可能ファイルのサブディレクトリにある場合 (おそらくそうではないと思います)、app.config ファイルの一部を変更<probing>できます。
  • nunit とその依存関係を単体テスト ディレクトリにコピーしたり、その逆を行ったりできます。
  • アンマネージ C++ クラスの仮想メソッドを変更して、NUnit が読み込めない型への参照を除外できます。これを行うには、メソッドの実装で自分自身を制限Object^して実際の型にキャストします。これは非常に不十分ですが機能します。
  • 問題の仮想メソッドを非仮想メソッドにすることができます
  • アンマネージ C++ クラスからコンストラクターを削除できます。
于 2013-08-28T23:28:42.223 に答える