c# unit-testing c++-cli .net-4.5 appdomain

DLL cargados desde AppplicationBase incorrecto al intentar cargar dlls mixtos de C#y C++/CLI en un nuevo dominio de aplicaciĆ³n



unit-testing c++-cli (1)

Esto parece ser un error en .NET 4.5.

NUnit crea un nuevo dominio de aplicación para ejecutar las pruebas unitarias. Si el conjunto de prueba de unidad o cualquiera de sus referencias son conjuntos de modo mixto, termina intentando cargar las referencias del conjunto de modo mixto en el dominio de aplicación predeterminado también, bajo ciertas condiciones.

El tiempo de ejecución debe inicializar el código c ++ no administrado del ensamblaje de modo mixto antes de hacer cualquier otra cosa en ese ensamblaje. Lo hace a través de la clase LanguageSupport compilada automáticamente (el código fuente para esto se distribuye con Visual Studio). LanguageSupport::Initialize se ejecuta primero en el constructor estático de la clase .module generada por compilador del ensamblado de prueba de unidad de modo mixto, en el contexto del dominio de aplicación creado por NUnit. A su vez, LanguageSupport vuelve a activar el mismo constructor estático en el dominio de aplicaciones predeterminado, que finalmente llama a LanguageSupport::Initialize nuevamente. Aquí está la misma pila de llamadas desde arriba, menos el manejo de errores:

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

El dominio de aplicaciones que crea NUnit realmente está logrando cargar el conjunto de prueba de la unidad y sus referencias (asumiendo que no tiene otros problemas), pero la inicialización de 2nd LanguageSupport en el dominio de aplicaciones predeterminado está fallando.

Al descargar el IL para el ensamblaje de modo mixto, descubrí que algunas de las clases no administradas tenían un método de inicialización estático generado automáticamente; estos son algunos de los métodos a los que se llama en el método InitializeVtables visto en segundo lugar desde la parte superior de la pila de llamadas. Después de algunas compilaciones de prueba y error, descubrí que si la clase no administrada tiene un constructor y al menos un método virtual con un tipo .NET en la firma, el compilador emitirá un inicializador estático para la clase.

LanguageSupport::InitializeVtables llama a estas funciones de inicializador estático. Cuando se ejecuta el inicializador, aparentemente está causando que el CLR intente cargar las referencias que contienen los tipos importados encontrados en las firmas de los métodos virtuales de la clase no administrada. Debido a que el dominio de aplicación predeterminado no tiene los ensamblajes de prueba de unidad y sus referencias en la base de la aplicación, la llamada falla y genera el error que se ve arriba.

Además, el error (en la aplicación de juguete que hice, de todos modos) solo ocurrirá si hay otro inicializador no vtable que también se ejecuta.

Aquí está la parte relevante de mi aplicación:

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

Soluciones:

  • Si sus pruebas de unidad se encuentran en un subdirectorio del ejecutable NUnit (improbable, supongo), puede modificar la parte <probing> del archivo app.config .
  • Puede copiar nunit y sus dependencias en el directorio de prueba de unidad, o viceversa
  • Puede modificar los métodos virtuales en sus clases de c ++ no administradas para excluir las referencias a los tipos que NUnit no podrá cargar. Puede hacer esto al limitarse a Object^ y convertir el tipo real en la implementación del método, que es bastante escaso pero funciona.
  • Puede hacer que el método virtual en cuestión no sea virtual.
  • Puede eliminar el constructor de la clase c ++ no administrada.

Tenemos una gran solución .NET con proyectos tanto en C # como en C ++ / CLI que se refieren entre sí. También tenemos varios proyectos de pruebas unitarias. Recientemente, hemos actualizado Visual Studio 2010 & .NET 4.0 a Visual Studio 4.5 & .NET 4.5, y ahora, cuando intentamos ejecutar las pruebas de unidad, parece que hay un problema al cargar algunas DLL durante la prueba.

El problema parece ocurrir porque las pruebas de unidad se realizan en un dominio de aplicación separado. El proceso de prueba de la unidad (por ejemplo, nunit-agent.exe) crea un nuevo dominio de aplicación con AppBase configurado en la ubicación del proyecto de prueba, pero de acuerdo con el Registro de Fusion, algunas DLL están cargadas con el directorio del ejecutable de nunit como AppBase en lugar de AppBase del dominio de la aplicación .

Logré reproducir el problema con un escenario más simple, que crea un nuevo dominio de aplicación e intenta ejecutar la prueba allí. Así es como se ve (cambié los nombres de las clases de prueba de unidad, los métodos y la ubicación de la dll para proteger a los inocentes):

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

Esta es la excepción que recibo cuando intento ejecutar la prueba de la unidad. Como puede ver, el problema ocurre: la dll de C ++ se inicializa e intenta cargar la dll de C # (cambié los nombres de las DLL involucradas a CPlusPlusDll y 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 ?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 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 ---

Esto es lo que veo en el Registro de Fusion (he cambiado el nombre de la DLL a SomeDLL.dll en lugar del original):

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

Como puede ver, el problema es que AppBase es donde reside MyTester.exe, en lugar de donde reside SomeDLL.dll (que es la misma ubicación que la unidad de prueba dll). Esto sucede con varias DLL, incluidas las dos DLL mencionadas en la excepción anterior.

También intenté reproducir con un proyecto de prueba unitaria más simple (una pequeña solución VS2012 con 3 proyectos: un proyecto C # que hace referencia a un proyecto C ++ / CLI que hace referencia a otro proyecto C #), pero el problema no se reprodujo y funcionó a la perfección. Como mencioné anteriormente, las pruebas de unidad estuvieron bien antes de actualizar a VS2012 y .NET 4.5.

¿Que puedo hacer? ¡Gracias!