不同AppDomains中的自动测试,Console.Out断开连接(RemotingException)

时间:2018-01-08 18:38:39

标签: c# mstest appdomain marshalbyrefobject

我在Visual Studio 2015 UPD3上使用.NET 4.6.2继承了这个C#测试项目(但它最初是使用以前版本的.NET和VS创建的,可能是VS2012)。

某些测试必须在不同的 AppDomain 中执行,因为必须卸载SUT的DLL(AFAIK,AppDomains是在.NET中卸载程序集的唯一方法)。

测试基础架构使用Console.SetOut,因此即使从不同的AppDomain,VSTest.Console.exe也会收集对Console.WriteLine的调用,并且所有写入的行都将出现在输出TRX中(通常,调用Console来自不同AppDomains的.WriteLine没有出现在测试输出中。起初我认为这是一个错误,但可能是设计上只有从Microsoft创建的&#34; UnitTestAdapter <调用Console.WriteLine / strong>&#34; appdomain被重新路由到测试输出。)

代码

我试图尽可能地简化它。像往常一样,名字已经改变,以保护无辜和有罪。

////////////////////////AssemblyInitializer class:

using Microsoft.VisualStudio.TestTools.UnitTesting;


namespace DomTest
{
    [TestClass]
    public static class AssemblyInitializer
    {
        public const int DelayToSimulateWork = 6 * 60 * 1000;

        [AssemblyInitialize]
        public static void AssemblyInit(TestContext context)
        {
        }

        [AssemblyCleanup]
        public static void AssemblyCleanup()
        {
            StaticDomainManager.DestroyAppDomain();
        }
    }
}

////////////////////////AppDomainHelper class:

using System;
using System.IO;

namespace DomTest
{
    public class AppDomainHelper : IDisposable
    {
        private AppDomain _domain;

        public AppDomainHelper(string domainPrefix)
        {
            _domain = AppDomain.CreateDomain(
                domainPrefix + "_" + Guid.NewGuid(),
                null,
                AppDomain.CurrentDomain.SetupInformation);

            SetConsoleOut();
        }

        public T CreateInstance<T>() where T : MarshalByRefObject
        {
            Type type = typeof(T);
            return (T)_domain.CreateInstanceAndUnwrap(
                type.Assembly.FullName, type.FullName);
        }

        public void Dispose()
        {
            Console.WriteLine("AppDomainHelper.Dispose: Calling AppDomain.Unload for AppDomain: {0} [caller domain: {1}]",
                _domain.FriendlyName, AppDomain.CurrentDomain.FriendlyName);

            AppDomain.Unload(_domain);
            _domain = null;
        }

        public void SetConsoleOut()
        {
            var consoleOutSetter = CreateInstance<ConsoleOutSetter>();
            consoleOutSetter.Set(Console.Out);
        }

        private class ConsoleOutSetter : MarshalByRefObject
        {
            public void Set(TextWriter consoleOut)
            {
                Console.SetOut(consoleOut);
            }

            public override object InitializeLifetimeService()
            {
                return null;
            }
        }
    }
}

////////////////////////StaticDomainManager class:

using System;

namespace DomTest
{
    public class StaticDomainManager
    {
        private static AppDomainHelper _currentAppDomain;

        public static void DestroyAppDomain()
        {
            if (_currentAppDomain != null)
            {
                _currentAppDomain.SetConsoleOut();
                _currentAppDomain.Dispose();
                _currentAppDomain = null;
            }
        }

        public static void CreateNewAppDomain()
        {
            DestroyAppDomain();
            _currentAppDomain = new AppDomainHelper("SomeName");
        }

        public static void ExecuteInDomain<T>(Action<T> action) where T : MarshalByRefObject, IDisposable
        {
            _currentAppDomain.SetConsoleOut();

            using (T instance = _currentAppDomain.CreateInstance<T>())
            {
                action(instance);
            }
        }
    }
}

////////////////////////TestExecutorInAppDomainBase class:

using System;

namespace DomTest
{
    public class TestExecutorInAppDomainBase : MarshalByRefObject, IDisposable
    {
        public TestExecutorInAppDomainBase()
        {
            Console.WriteLine("TestExecutorInAppDomainBase: Calling some common INIT code in AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);

            // here: application-specific initialization that must be run in separate AppDomain
        }

        public void Dispose()
        {
            // The following call to Console.WriteLine throws RemotingException if a test
            // takes too long.
            Console.WriteLine("TestExecutorInAppDomainBase: Calling some common UNINIT code in AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);

            // here: application-specific uninitialization that must be run in separate AppDomain
        }

        public override object InitializeLifetimeService()
        {
            return null;
        }
    }
}

////////////////////////Test1 class (and imagine other classes like this):

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;

namespace DomTest
{
    [TestClass]
    public class Test1
    {
        [ClassInitialize]
        public static void ClassInitialize(TestContext testContext)
        {
            StaticDomainManager.CreateNewAppDomain();
        }

        [TestMethod]
        public void TestMethod1A()
        {
            RunTestInDomain((ltc) => ltc.TestAInAppDomain());
        }

        [TestMethod]
        public void TestMethod1B()
        {
            RunTestInDomain((ltc) => ltc.TestBInAppDomain());
        }

        private void RunTestInDomain(Action<LocalTestCode> action)
        {
            StaticDomainManager.ExecuteInDomain<LocalTestCode>(action);
        }

        private class LocalTestCode : TestExecutorInAppDomainBase
        {
            public LocalTestCode()
                : base()
            {
                // class-specific code here
            }

            public void TestAInAppDomain()
            {
                // class-specific code here
                Thread.Sleep(AssemblyInitializer.DelayToSimulateWork);
            }

            public void TestBInAppDomain()
            {
                // class-specific code here
                Thread.Sleep(AssemblyInitializer.DelayToSimulateWork);
            }
        }
    }
}

问题

所有MarshalByRefObject派生类重写 InitializeLifetimeService 以返回null,以避免对象断开连接和RemotingException。 查看相关问题(及其链接):

无论如何,有时候测试会因RemotingException 而失败,因为它们花费的时间太长(代码中的Sleep调用会模拟花费的时间)。

RemotingException发生在TestExecutorInAppDomainBase.Dispose中的Console.WriteLine调用中。

System.Runtime.Remoting.RemotingException: Object '/0d0a9080_ccb7_4db6_a605_6b57778cfb5b/gvcpwryuykjjcowto_nvpdl8_23.rem' has been disconnected or does not exist at the server

我认为问题是Console.Out的生命周期不受我的控制。 SyncTextWriterTextWriterMarshalByRefObject - 派生类,不会覆盖InitializeLifetimeService(或者说DotPeek说)。因此,如果测试时间过长,则会Console.Out断开连接并发生RemotingException

第一次尝试:Debug.WriteLine / Trace.WriteLine

我尝试用Console.WriteLineDebug.WriteLine替换Trace.WriteLine(在TestExecutorInAppDomainBase.Dispose中):现在测试不会失败,但这些行不会出现在输出TRX 。如果在不同的AppDomain中调用它们,VSTest.Console似乎不会收集Debug.WriteLineTrace.WriteLine的输出。

同样,这可能是设计中只有在&#34; UnitTestAdapter &#34;内生成的输出。 appdomain被重新路由到测试输出。

(临时)技巧

所以我决定改变这段代码:

    public static void ExecuteInDomain<T>(Action<T> action) where T : MarshalByRefObject, IDisposable
    {
        _currentAppDomain.SetConsoleOut();

        using (T instance = _currentAppDomain.CreateInstance<T>())
        {
            action(instance);
        }
    }

到此:

    public static void ExecuteInDomain<T>(Action<T> action) where T : MarshalByRefObject, IDisposable
    {
        _currentAppDomain.SetConsoleOut();

        using (T instance = _currentAppDomain.CreateInstance<T>())
        {
            action(instance);
            _currentAppDomain.SetConsoleOut();
        }
    }

我刚刚在using块结束之前添加了对SetConsoleOut的额外调用。 通过这种方式,Console.Out已更新&#34;在调用TestExecutorInAppDomainBase.Dispose之前,并且不会发生RemotingException。

但是我不喜欢这个&#34;解决方案&#34;因为它根本不是解决方案,它是一个快速的黑客攻击,只适用于代码的特定点,而其余的测试项目是一个发条炸弹,只是等待繁荣另一点。

问题

是否有一个很好的解决方案允许:

  • 避免使用RemotingException
  • AND捕获来自不同AppDomains的输出行
  • 并没有从头开始重新设计整个项目?

(thrid bullet是因为我考虑将自定义记录器传递给AppDomain,MarshalByRefObject派生并在我的控制下,但它将涉及更改所有目前调用的测试{{ 1}},它们是 很多 )。

0 个答案:

没有答案