18

我们有一个大型 .NET 解决方案,其中包含相互引用的 C# 和 C++/CLI 项目。我们还有几个单元测试项目。我们最近从 Visual Studio 2010 和 .NET 4.0 升级到 Visual Studio 4.5 和 .NET 4.5,现在当我们尝试运行单元测试时,在测试期间加载某些 DLL 似乎出现问题。

这个问题似乎是因为单元测试是在单独的 AppDomain 上执行的。单元测试过程(例如 nunit-agent.exe)创建了一个新的 AppDomain,其中 AppBase 设置为测试项目的位置,但根据 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(我将涉及的 DLL 的名称更改为 CPlusPlusDll 和 CSharpDll):

System.TypeInitializationException:“”的类型初始化程序引发了异常。
 ---> .ModuleLoadExceptionHandlerException:在导致 C++ 模块加载失败的主要异常之后发生了嵌套异常。
 ---> System.TypeInitializationException: '' 的类型初始化程序引发了异常。
 ---> .ModuleLoadException:C++ 模块在 vtable 初始化期间加载失败。
 ---> System.IO.FileNotFoundException:无法加载文件或程序集“CSharpDll,版本=8.80.0.0,文化=中性,PublicKeyToken=null”或其依赖项之一。该系统找不到指定的文件。
   在 ?A0xb992d574.??__E??_7CAppletAction@CPlusPlusDll@SomeNamespace@@6B@@@YMXXZ()
   在 _initterm_m((fnptr)* pfbegin, (fnptr)* pfend) 在 f:\dd\vctools\crt_bld\self_x86\crt\src\puremsilcode.cpp:line 219
   在 .LanguageSupport.InitializeVtables(LanguageSupport* ) 在 f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 331
   在 .LanguageSupport._Initialize(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
   --- 内部异常堆栈跟踪结束 ---
   在 f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 194 中的 .ThrowModuleLoadException(String errorMessage, Exception 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 错误代码,IntPtr 错误信息)
   在 System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 错误代码)
   在 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()
   在 .LanguageSupport.InitializeDefaultAppDomain(LanguageSupport* ) 在 f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 342
   在 .LanguageSupport._Initialize(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
   --- 内部异常堆栈跟踪结束 ---
   在 f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 184 中的 .ThrowNestedModuleLoadException(Exception innerException, Exception nestedException)
   在 .LanguageSupport.Cleanup(LanguageSupport* , Exception 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 Log 中看到的(我已将 DLL 的名称更改为 SomeDLL.dll 而不是原来的):

*** 装配活页夹日志条目 (8/1/2013 @ 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,Version=8.80.0.0,Culture=neutral,PublicKeyToken=null
 (完全指定)
日志:Appbase = file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/
日志:初始 PrivatePath = NULL
日志:动态基础 = NULL
日志:缓存基础 = NULL
日志:AppName = MyTester.exe
调用程序集:(未知)。
===
LOG:此绑定在默认加载上下文中开始。
日志:使用应用程序配置文件:c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe.Config
LOG:使用主机配置文件:
LOG:使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config 中的机器配置文件。
LOG:此时未将策略应用于引用(私有、自定义、部分或基于位置的程序集绑定)。
日志:尝试下载新的 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 是 MyTester.exe 所在的位置,而不是 SomeDLL.dll 所在的位置(与单元测试 dll 的位置相同)。这发生在几个 DLL 中,包括上面异常中提到的两个 DLL。

我还尝试使用更简单的单元测试项目(一个带有 3 个项目的小型 VS2012 解决方案 - 一个引用另一个 C# 项目的 C++/CLI 项目的 C# 项目)进行重现,但问题没有重现,并且运行良好。正如我之前提到的,在我们升级到 VS2012 和 .NET 4.5 之前,单元测试是可以的。

我能做些什么?谢谢!

4

1 回答 1

15

这似乎是 .NET 4.5 中的一个错误。

NUnit 创建一个新的应用程序域来运行单元测试。如果单元测试程序集或其任何引用是混合模式程序集,则在某些条件下,它最终也会尝试在默认应用程序域中加载混合模式程序集的引用。

运行时必须先初始化混合模式程序集的非托管 c++ 代码,然后才能在该程序集中执行任何其他操作。它通过自动编译的 LanguageSupport 类来实现这一点(其源代码与 Visual Studio 一起分发)。LanguageSupport::Initialize首先在混合模式单元测试程序集的编译器生成.module类的静态构造函数中运行,在 NUnit 创建的应用程序域的上下文中。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 中的第二个 LanguageSupport 初始化失败。

通过转储混合模式程序集的 IL,我发现一些非托管类具有自动生成的静态初始化程序方法 - 这些是在调用堆栈顶部的第二个 InitializeVtables 方法中调用的方法之一。经过一些试错编译后,我发现如果非托管类有一个构造函数和至少一个签名中带有 .NET 类型的虚拟方法,编译器将为该类发出一个静态初始化程序。

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 可执行文件的子目录中(我猜不太可能),您可以修改<probing>app.config 文件的部分
  • 您可以将 nunit 及其依赖项复制到单元测试目录,反之亦然
  • 您可以修改非托管 c++ 类中的虚拟方法,以排除对 NUnit 无法加载的类型的引用。您可以通过将自己限制Object^在方法实现中并将其强制转换为实际类型来做到这一点,这很蹩脚但有效。
  • 您可以使有问题的虚拟方法成为非虚拟方法
  • 您可以从非托管 c++ 类中删除构造函数
于 2013-08-28T23:28:42.223 回答