2013-08-01 11 views
17

Abbiamo una soluzione .NET di grandi dimensioni con progetti C# e C++/CLI che fanno riferimento l'un l'altro. Abbiamo anche diversi progetti di test unitari. Di recente abbiamo eseguito l'aggiornamento da Visual Studio 2010 & .NET 4.0 a Visual Studio 4.5 & .NET 4.5 e ora, quando proviamo a eseguire i test delle unità, sembra che ci sia un problema durante il caricamento di alcune DLL durante il test.DLL caricate da AppplicationBase errato durante il tentativo di caricare DLL miste C# e C++/CLI in un nuovo AppDomain

Il problema sembra verificarsi perché il test dell'unità viene eseguito su un AppDomain separato. Il processo di test delle unità (ad esempio nunit-agent.exe) crea un nuovo AppDomain con AppBase impostato sull'ubicazione del progetto di test, ma in base al Fusion Log, alcune DLL sono caricate con la directory dell'eseguibile di nunit come AppBase invece dell'AppBase di AppDomain .

Sono riuscito a riprodurre il problema con uno scenario più semplice, che crea un nuovo AppDomain e tenta di eseguire il test lì. Ecco come appare (ho cambiato i nomi delle classi di unit test, i metodi e la posizione del dll per proteggere gli innocenti):

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); 
     } 
    } 
} 

questa è l'eccezione che ottengo quando si cerca di eseguire il test di unità. Come si può vedere, il problema si verifica la dll C++ viene inizializzato e cerca di caricare il C# dll (ho cambiato i nomi delle DLL coinvolti di CPlusPlusDll e CSharpDll):

 
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 --- 

Questo è quello che sto vedendo nella fusione Log (ho cambiato il nome della DLL per SomeDLL.dll al posto dell'originale):

 
*** 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. 

Come si può vedere, il problema è che l'AppBase è dove risiede MyTester.exe, invece di dove risiede SomeDLL.dll (che è la stessa posizione dell'unità dll test di unità). Questo accade per diverse DLL, incluse entrambe le DLL menzionate nell'eccezione sopra.

Ho anche provato a riprodurre con un progetto di test unitario più semplice (una piccola soluzione VS2012 con 3 progetti - un progetto C# che fa riferimento a un progetto C++/CLI che fa riferimento a un altro progetto C#), ma il problema non si è riprodotto e ha funzionato perfecty. Come ho detto prima, i test unitari erano ok prima di passare a VS2012 & .NET 4.5.

Cosa posso fare? Grazie!

+0

accade solo con il NUnit-TestRunner? Puoi anche riproporlo con MSTest? –

+0

Succede in NUnit, MSTest e anche nel programma Tester che ho scritto qui. –

+0

Questa offuscazione non aiuta noi ad aiutarti. Qual è la relazione tra "CSharpDll" e "SomeDLL"? –

risposta

11

Questo sembra essere un bug in .NET 4.5.

NUnit crea un nuovo dominio di app per eseguire i test di unità. Se l'assembly di test unitario o uno qualsiasi dei suoi riferimenti sono assiemi in modalità mista, finisce con il tentativo di caricare anche i riferimenti dell'assieme in modalità mista nel dominio dell'app predefinito, a determinate condizioni.

Il runtime deve inizializzare il codice C++ non gestito dell'assembly in modalità mista prima di eseguire qualsiasi altra operazione in quell'assembly. Lo fa tramite la classe LanguageSupport automaticamente compilata (il codice sorgente per questo è distribuito con Visual Studio). LanguageSupport::Initialize viene eseguito per la prima volta nel costruttore statico della classe .module generata dal compilatore dell'assembly test in modalità mista, nel contesto dell'appdominio creato da NUnit. LanguageSupport, a sua volta, reinnesca lo stesso costruttore statico nell'appadominio predefinito, che alla fine chiama nuovamente LanguageSupport::Initialize.Ecco lo stesso stack di chiamate dall'alto meno la roba gestione degli errori:

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*) 

il dominio di applicazione che NUnit crea in realtà è riuscire a caricare il gruppo di test di unità e le sue referenze (ammesso che non si dispone di altri problemi), ma il L'inizializzazione di 2nd LanguageSupport nell'appadominio predefinito non riesce.

dal dumping l'IL per l'assemblaggio modalità mista, ho scoperto che alcune delle classi non gestite avevano un metodo inizializzatore statico generato automaticamente - questi sono tra i metodi che vengono chiamati nel metodo InitializeVtables visto 2 ° dalla parte superiore della chiamata pila. Dopo alcuni tentativi di compilazione degli errori, ho scoperto che se la classe non gestita ha un costruttore e almeno un metodo virtuale con un tipo .NET nella firma, il compilatore emetterà un inizializzatore statico per la classe.

chiama queste funzioni di inizializzatore statico. Quando viene eseguito l'inizializzatore, apparentemente il CLR tenta di caricare i riferimenti contenenti i tipi importati trovati nelle firme dei metodi virtuali della classe non gestita. Poiché l'appadominio predefinito non ha gli assembly unit test ei suoi riferimenti nella base dell'applicazione, la chiamata fallisce e genera l'errore che vedi sopra.

Inoltre, l'errore (nell'app giocattolo che ho creato, comunque) si verifica solo se è in esecuzione anche un altro inizializzatore non vtable.

Ecco la parte rilevante della mia app:

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) {} 
}; 

Soluzioni alternative:

  • Se il test di unità sono in una sottodirectory del file eseguibile NUnit (improbabile, credo), è possibile modify the <probing> portion of the app.config file.
  • È possibile copiare nunit e le sue dipendenze nella directory di test dell'unità o viceversa
  • È possibile modificare i metodi virtuali nelle classi C++ non gestite per escludere riferimenti a tipi che NUnit non sarà in grado di caricare. Puoi farlo limitandoti a Object^ e passando al tipo effettivo nell'implementazione del metodo, che è piuttosto zoppo ma funziona.
  • È possibile effettuare il metodo virtuale in questione un non virtuale uno
  • È possibile rimuovere il costruttore dal C++ unamanaged classe
+0

Risposta molto dettagliata, signor Mullet. Grazie! –

+0

@Oliver Mellet, per "copy nunit e le sue dipendenze alla directory di test dell'unità", è possibile forzare VS a eseguire i test dalla directory di test dell'unità? Ho avuto successo solo eseguendo nunit-x86.exe dalla directory di test. – Amanduh

+0

[Come accade] (http://stackoverflow.com/questions/32493614/different-dependency-resolution-behavior-loading-assembly-in-default-appdomain-v), forzando NUnit a eseguire test nell'AppDomain predefinito (/ domain: None per NUnit 2.x) è un'altra soluzione, laddove applicabile. – rationull