我们有一个大型的.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:在vtable初始化期间,C++模块加载失败。
---> System.IO.FileNotFoundException:无法加载文件或程序集'CSharpDll,Version = 8.80.0.0,Culture = neutral,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
在f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp中的.LanguageSupport.InitializeVtables(LanguageSupport *):行331
在f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp中的.LanguageSupport._Initialize(LanguageSupport *):行491
在f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp中的.LanguageSupport.Initialize(LanguageSupport *):第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中的.LanguageSupport.Initialize(LanguageSupport *):行712
在f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp中的.cctor()中:线754
---内部异常堆栈跟踪的结尾---
在System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode,IntPtr errorInfo)
在System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode)
在.DoCallBackInDefaultDomain(IntPtr function,Void * cookie)中的f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:线406
在.DefaultDomain.Initialize()中的f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:277行
在.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中
在f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp中的.LanguageSupport.Initialize(LanguageSupport *):行702
---内部异常堆栈跟踪的结尾---
在.ThrowNestedModuleLoadException(Exception innerException,Exception nestedException)在f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 184
在.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中的.LanguageSupport.Initialize(LanguageSupport *):行710
在f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp中的.cctor()中:线754
---内部异常堆栈跟踪的结尾---
这是我在Fusion Log中看到的(我已将DLL的名称更改为SomeDLL.dll,而不是原始名称):
*** Assembly Binder日志条目(2013年8月1日下午01:47:48)***
操作失败。
绑定(bind)结果: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下运行
---详细的错误日志如下。
===预绑定(bind)状态信息===
日志:用户= 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
调用程序集:(未知)。
===
日志:此绑定(bind)在默认的加载上下文中启动。
日志:使用应用程序配置文件:c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe.Config
日志:使用主机配置文件:
日志:使用C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config中的计算机配置文件。
日志:目前未将策略应用于引用(私有(private),自定义,部分或基于位置的程序集绑定(bind))。
日志:尝试下载新的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。
日志:所有探测URL尝试失败。
如您所见,问题在于AppBase是MyTester.exe所在的位置,而不是SomeDLL.dll所在的位置(与单元测试dll相同的位置)。几个DLL都会发生这种情况,包括上面异常(exception)中提到的两个DLL。
我还尝试使用一个更简单的单元测试项目(一个小型的VS2012解决方案,其中包含3个项目-一个C#项目引用一个C++/CLI项目,该C++/CLI项目引用另一个C#项目)来重现该问题,但是问题没有重现,并且可以完美地工作。如前所述,在升级到VS2012和.NET 4.5之前,单元测试还可以。
我能做些什么?
谢谢!
最佳答案
这似乎是.NET 4.5中的错误。
NUnit创建一个新的应用程序域来运行单元测试。如果单元测试程序集或其任何引用是混合模式程序集,则在某些情况下,它最终也会尝试在默认应用程序域中加载混合模式程序集的引用。
运行时必须先初始化混合模式程序集的非托管c++代码,然后才能在该程序集中执行其他任何操作。它通过自动编译的LanguageSupport类来实现此目的(此源代码随Visual Studio一起分发)。 LanguageSupport::Initialize
首先在NUnit创建的appdomain的上下文中在混合模式单元测试程序集的编译器生成的.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实际上可以成功加载单元测试程序集及其引用(假设您没有其他问题),但是默认appdomain中的第二语言支持初始化失败。
通过为混合模式程序集转储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) {}
};
解决方法:
<probing>
portion of the app.config file。 Object^
并强制转换为方法实现中的实际类型来实现此目的,这虽然很me脚,但是可以工作。