2013-08-01 34 views
17

我們有一個大型的.NET解決方案,它包含相互引用的C#和C++/CLI項目。 我們也有幾個單元測試項目。我們最近從Visual Studio 2010 & .NET 4.0升級到Visual Studio 4.5 & .NET 4.5,現在當我們嘗試運行單元測試時,似乎在測試過程中加載了一些DLL。嘗試在新的AppDomain中加載混合的C#和C++/CLI dll時從錯誤的AppplicationBase加載的DLL

由於單元測試是在單獨的AppDomain上執行的,因此會出現此問題。單元測試過程(例如nunit-agent.exe)會創建一個AppBase設置爲測試項目位置的新AppDomain,但根據Fusion Log,某些DLL會以nunit的可執行文件目錄作爲AppBase而不是AppDomain的AppBase 。

我已經設法用一個更簡單的方案來重現問題,它創建一個新的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(我改變涉及到CPlusPlusDll和CSharpDll的DLL文件的名稱):

 
System.TypeInitializationException: The type initializer for '' threw an exception. 
---> .ModuleLoadExceptionHandlerException: A nested exception occurred after the primary exception that caused the C++ module to fail to load. 
---> System.TypeInitializationException: The type initializer for '' threw an exception. 
---> .ModuleLoadException: The C++ module failed to load during vtable initialization. 
---> System.IO.FileNotFoundException: Could not load file or assembly 'CSharpDll, Version=8.80.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. 
    at [email protected]@[email protected]@[email protected]@@YMXXZ() 
    at _initterm_m((fnptr)* pfbegin, (fnptr)* pfend) in f:\dd\vctools\crt_bld\self_x86\crt\src\puremsilcode.cpp:line 219 
    at .LanguageSupport.InitializeVtables(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 331 
    at .LanguageSupport._Initialize(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 491 
    at .LanguageSupport.Initialize(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 702 
    --- End of inner exception stack trace --- 
    at .ThrowModuleLoadException(String errorMessage, Exception innerException) in f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 194 
    at .LanguageSupport.Initialize(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 712 
    at .cctor() in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 754 
    --- End of inner exception stack trace --- 
    at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo) 
    at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode) 
    at .DoCallBackInDefaultDomain(IntPtr function, Void* cookie) in f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 406 
    at .DefaultDomain.Initialize() in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 277 
    at .LanguageSupport.InitializeDefaultAppDomain(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 342 
    at .LanguageSupport._Initialize(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 539 
    at .LanguageSupport.Initialize(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 702 
    --- End of inner exception stack trace --- 
    at .ThrowNestedModuleLoadException(Exception innerException, Exception nestedException) in f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 184 
    at .LanguageSupport.Cleanup(LanguageSupport* , Exception innerException) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 662 
    at .LanguageSupport.Initialize(LanguageSupport*) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 710 
    at .cctor() in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 754 
    --- End of inner exception stack trace --- 

這是我所看到的在融合日誌(我已經改變了名的DLL來SomeDLL.dll而不是原來的):

 
*** Assembly Binder Log Entry (8/1/2013 @ 01:47:48 PM) *** 

The operation failed. 
Bind result: hr = 0x80070002. The system cannot find the file specified. 

Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll 
Running under executable c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe 
--- A detailed error log follows. 

=== Pre-bind state information === 
LOG: User = WF-IL\yshany 
LOG: DisplayName = SomeDLL, Version=8.80.0.0, Culture=neutral, PublicKeyToken=null 
(Fully-specified) 
LOG: Appbase = file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/ 
LOG: Initial PrivatePath = NULL 
LOG: Dynamic Base = NULL 
LOG: Cache Base = NULL 
LOG: AppName = MyTester.exe 
Calling assembly : (Unknown). 
=== 
LOG: This bind starts in default load context. 
LOG: Using application configuration file: c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe.Config 
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config. 
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind). 
LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL.DLL. 
LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL/SomeDLL.DLL. 
LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL.EXE. 
LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL/SomeDLL.EXE. 
LOG: All probing URLs attempted and failed. 

正如你所看到的,問題是,應用平臺就是MyTester.exe所在,而不是SomeDLL.dll所在的位置(與單元測試dll位置相同)。這發生在幾個DLL上,包括上面例外中提到的兩個DLL。我還嘗試使用一個更簡單的單元測試項目(一個包含3個項目的小型VS2012解決方案 - 一個引用另一個C#項目的C++/CLI項目的C#項目)重現,但問題沒有重現, perfecty。正如我之前提到的,在升級到VS2012 & .NET 4.5之前,單元測試是可以的。

我該怎麼辦? 謝謝!

+0

它只能和NUnit的-的TestRunner發生取出構造?你也可以用MSTest重新制作它嗎? –

+0

它發生在NUnit,MSTest以及我在這裏編寫的測試程序中。 –

+0

這種混淆並不能幫助我們幫助你。 「CSharpDll」和「SomeDLL」之間有什麼關係? –

回答

11

這似乎是.NET 4.5中的一個錯誤。

NUnit創建一個新的應用程序域來運行單元測試。如果單元測試程序集或其任何引用是混合模式程序集,則它會在特定條件下嘗試在默認應用程序域中加載混合模式程序集的引用。

運行時必須先初始化混合模式程序集的非託管C++代碼,然後再執行該程序集中的任何其他操作。它通過自動編譯的LanguageSupport類來完成它(源代碼隨Visual Studio一起分發)。 LanguageSupport::Initialize首先在NUnit創建的應用程序域的上下文中在混合模式單元測試程序集的編譯器生成的.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中第二語言支持初始化失敗。

通過爲混合模式程序集轉儲IL,我發現一些非託管類有一個自動生成的靜態初始化方法 - 這些方法是在調用頂部第二個InitializeVtables方法中調用的方法疊加。經過一些試驗和錯誤編譯之後,我發現如果非託管類有一個構造函數並且至少有一個具有.NET類型的虛擬方法,那麼編譯器將爲該類發出一個靜態初始化程序。

調用這些靜態初始化函數。初始化程序運行時,顯然會導致CLR嘗試加載包含在非託管類的虛擬方法的簽名中找到的導入類型的引用。由於默認appdomain在應用程序庫中沒有單元測試程序集及其引用,因此調用將失敗並生成您在上面看到的錯誤。

更重要的是,錯誤(在我製作的玩具應用程序中)只會在另一個非可變初始化器運行時纔會發生。

這裏是我的應用程序的相關部分:

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的可執行文件的子目錄(不太可能,我猜的),你可以modify the <probing> portion of the app.config file
  • 可以NUnit的和它的依賴複製到單元測試目錄,反之亦然
  • 您可以修改您的非託管C++類的虛方法來排除到類型NUnit的將無法加載引用。你可以通過將自己限制爲Object^並將其轉換爲方法實現中的實際類型來實現這一點,但這種方法很蹩腳,但很有效。
  • 您可以有問題的虛方法的非虛擬一個
  • 您可以從unamanaged C++類
+0

穆勒先生,非常詳細的答案。謝謝! –

+0

@Oliver Mellet,「複製nunit及其依賴到單元測試目錄」,是否可以強制VS從單元測試目錄運行測試?我只通過運行測試目錄中的nunit-x86.exe獲得了成功。 – Amanduh

+0

[發生](http://stackoverflow.com/questions/32493614/different-dependency-resolution-behavior-loading-assembly-in-default-appdomain-v),迫使NUnit在默認的AppDomain中運行測試(/domain:NUnit 2.x無)是適用的另一種解決方法。 – rationull