2013-08-01 7 views
17

Mamy duże rozwiązanie .NET z projektami C# i C++/CLI, które się do siebie nawzajem odwołują. Mamy również kilka projektów testowania jednostek. Niedawno uaktualniliśmy Visual Studio 2010 z wersji & .NET 4.0 do Visual Studio 4.5 & .NET 4.5, a teraz, gdy próbujemy uruchomić testy jednostkowe, wydaje się, że wystąpił problem podczas ładowania niektórych bibliotek DLL podczas testu.DLLs załadowane z niewłaściwej AppplicationBase podczas próby załadowania bibliotek dll C# i C++/CLI w nowej AppDomain

Problem pojawia się, ponieważ testowanie jednostki odbywa się w oddzielnym AppDomain. Proces testowania jednostkowego (na przykład nunit-agent.exe) tworzy nową AppDomain z AppBase ustawioną na lokalizację projektu testowego, ale według dziennika Fusion niektóre biblioteki DLL są załadowane katalogiem wykonywalnym nunit jako AppBase zamiast AppBomain AppBase .

Udało mi się odtworzyć problem za pomocą prostszego scenariusza, który tworzy nową AppDomain i próbuje uruchomić tam test. Oto jak to wygląda (I zmienił nazwy klas testowych jednostki, metod i lokalizacji dll, aby chronić niewinnych):

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

Jest to wyjątek uzyskać, gdy próbuje wykonać test urządzenia. Jak widać, problem dzieje dll C++ jest inicjowany i próbuje załadować biblioteki dll C# (zmieniłem nazwy DLL zaangażowanych do CPlusPlusDll i 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 --- 

to co widzę w Fusion Log (zmieniłem nazwę pliku DLL do SomeDLL.dll zamiast oryginału):

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

Jak widać, problem jest taki, że AppBase gdzie MyTester.exe rezyduje, zamiast gdzie znajduje się plik SomeDLL.dll (który jest tą samą lokalizacją, co dll testowy jednostki). Dzieje się tak w przypadku kilku bibliotek DLL, w tym obu bibliotek DLL wymienionych w powyższym wyjątku.

Próbowałem również odtworzyć za pomocą prostszego projektu testu jednostkowego (małe rozwiązanie VS2012 z 3 projektami - projekt C# odwołujący się do projektu C++/CLI, który odwołuje się do innego projektu C#), ale problem nie powtarzał się i zadziałało perfekcja. Jak wspomniałem wcześniej, testy jednostkowe były w porządku, zanim zaktualizowaliśmy do VS2012 & .NET 4.5.

Co mogę zrobić? Dzięki!

+0

zdarza się tylko z nunit-TestRunner? Czy możesz również zaimportować go za pomocą MSTest? –

+0

Zdarza się w NUnit, MSTest, a także w programie Tester, który tutaj napisałem. –

+0

Ta obfuskacja nie pomaga nam. Jaki jest związek między "CSharpDll" i "SomeDLL"? –

Odpowiedz

11

Wygląda na to, że jest to błąd w .NET 4.5.

NUnit tworzy nową domenę aplikacji do uruchamiania testów jednostkowych. Jeśli zespół testowania jednostkowego lub dowolne jego odwołania są zespołami w trybie mieszanym, kończy się próba załadowania odwołań do zespołu trybu mieszanego również w domyślnej domenie aplikacji, pod pewnymi warunkami.

Środowisko wykonawcze musi zainicjować niezarządzany kod C++ zespołu trybu mieszanego, zanim zrobi coś innego w tym zestawie. Robi to poprzez automatycznie skompilowaną klasę LanguageSupport (kod źródłowy tego jest dystrybuowany za pomocą Visual Studio). LanguageSupport::Initialize jest uruchamiany po raz pierwszy w konstruktorze statycznym złożonej z kompilatora klasy .module złożonego z zestawu testów w kontekście tworzonej przez NUnit domeny app. LanguageSupport z kolei uruchamia ponownie ten sam konstruktor statyczny w domyślnym appdomain, który ponownie wywołuje LanguageSupport::Initialize.Oto ten sam stos wywołań od powyżej minus rzeczy obsługa błędów:

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

AppDomain że NUnit tworzy rzeczywiście udaje się ładowanie zestawu testów jednostkowych i jego odniesienia (zakładając, że nie ma innych problemów), ale Niepowodzenie inicjacji drugiego języka w domyślnym appdomain.

Po zrzuceniu IL dla zespołu mieszanego, stwierdziłem, że niektóre z klas niezarządzanych automatycznie wygenerowały metodę inicjalizatora statycznego - są to metody, które wywoływane są w metodzie InitializeVtables, która jest druga od początku wywołania stos. Po kilku próbach i kompilacjach błędów odkryłem, że jeśli klasa niezarządzana ma konstruktora i co najmniej jedną metodę wirtualną z typem .NET w podpisie, kompilator wyśle ​​statyczny inicjator dla tej klasy.

wywołuje te statyczne funkcje inicjalizacyjne. Podczas uruchamiania inicjalizatora prawdopodobnie powoduje to, że CLR próbuje załadować odwołania zawierające importowane typy znajdujące się w sygnaturach metod wirtualnych klasy niezarządzanej. Ponieważ domyślne appdomain nie ma zespołów testów jednostkowych i ich odniesień w bazie aplikacji, wywołanie kończy się niepowodzeniem i generuje błąd, który widzisz powyżej.

Co więcej, błąd (w aplikacji zabawkowej, którą zrobiłem, tak czy inaczej) wystąpi tylko wtedy, gdy uruchomi się inny inicjator, który nie jest obsługiwany.

Tutaj jest odpowiednia część mojego 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) {} 
}; 

Obejścia:

  • Jeśli testy jednostkowe są w podkatalogu wykonywalnego NUnit (mało prawdopodobne, chyba), można modify the <probing> portion of the app.config file.
  • Można kopiować nunit i jego zależności do katalogu testów jednostkowych, lub odwrotnie
  • Można modyfikować metody wirtualnej w niezarządzanych C++ klas w celu wykluczenia odniesień do rodzajów NUnit że nie będzie w stanie załadować. Możesz to zrobić ograniczając się do Object^ i rzutując na rzeczywisty typ w implementacji metody, która jest dość kulawa, ale działa.
  • Można dokonać sposób wirtualny w pytaniu nie-wirtualny
  • Można usunąć konstruktora z unamanaged klasa C++
+0

Bardzo szczegółowa odpowiedź, panie Mullet. Dzięki! –

+0

@Oliver Mellet, dla "skopiuj nunit i jego zależności do katalogu testów jednostkowych", czy można zmusić VS do uruchamiania testów z katalogu testów jednostkowych? Udało mi się tylko uruchomić program nunit-x86.exe z katalogu testowego. – Amanduh

+0

[Tak się składa] (http://stackoverflow.com/questions/32493614/different-dependency-resolution-behavior-loading-assembly-in-default-appdomain-v), zmuszając NUnit do uruchamiania testów w domyślnym AppDomain (/ domain: Brak dla NUnit 2.x) jest kolejnym obejściem tam, gdzie ma to zastosowanie. – rationull

Powiązane problemy