简体   繁体   English

在创建Form之前,带有Application循环的NUnit测试会挂起

[英]NUnit test with Application loop in it hangs when Form is created before it

I have a few tests with WebBrowser control wrapped with MessageLoopWorker as described here: WebBrowser Control in a new thread 我对WebBrowser控件进行了一些测试,并用MessageLoopWorker包装,如下所述: 新线程中的WebBrowser控件

But when another test creates user control or form, the test freezes and never completes: 但是,当另一个测试创建用户控件或表单时,该测试将冻结并且永远不会完成:

    [Test]
    public async Task WorksFine()
    {
        await MessageLoopWorker.Run(async () => new {});
    }

    [Test]
    public async Task NeverCompletes()
    {
        using (new Form()) ;
        await MessageLoopWorker.Run(async () => new {});
    }

    // a helper class to start the message loop and execute an asynchronous task
    public static class MessageLoopWorker
    {
        public static async Task<object> Run(Func<object[], Task<object>> worker, params object[] args)
        {
            var tcs = new TaskCompletionSource<object>();

            var thread = new Thread(() =>
            {
                EventHandler idleHandler = null;

                idleHandler = async (s, e) =>
                {
                    // handle Application.Idle just once
                    Application.Idle -= idleHandler;

                    // return to the message loop
                    await Task.Yield();

                    // and continue asynchronously
                    // propogate the result or exception
                    try
                    {
                        var result = await worker(args);
                        tcs.SetResult(result);
                    }
                    catch (Exception ex)
                    {
                        tcs.SetException(ex);
                    }

                    // signal to exit the message loop
                    // Application.Run will exit at this point
                    Application.ExitThread();
                };

                // handle Application.Idle just once
                // to make sure we're inside the message loop
                // and SynchronizationContext has been correctly installed
                Application.Idle += idleHandler;
                Application.Run();
            });

            // set STA model for the new thread
            thread.SetApartmentState(ApartmentState.STA);

            // start the thread and await for the task
            thread.Start();
            try
            {
                return await tcs.Task;
            }
            finally
            {
                thread.Join();
            }
        }
    }

The code steps-in well except return await tcs.Task; 代码return await tcs.Task;很好,除了return await tcs.Task; never returns. 永远不会回来。

Wrapping new Form into the MessageLoopWorker.Run(...) seems to make it better, but it does not work with more complicated code, unfortunately. new Form包装到MessageLoopWorker.Run(...)中似乎使它更好,但是不幸的是,它不适用于更复杂的代码。 And I have quite a lot of other tests with forms and user controls that I would like to avoid wrapping into messageloopworker. 而且,我还有很多其他关于表单和用户控件的测试,希望避免将它们包装到messageloopworker中。

Maybe MessageLoopWorker can be fixed to avoid the interference with other tests? 也许可以固定MessageLoopWorker以避免与其他测试产生干扰?

Update: following the amazing answer by @Noseratio I've reset the synchronisation context before the MessageLoopWorker.Run call and it now works well. 更新:按照@Noseratio给出的令人惊奇的答案,我已经在MessageLoopWorker.Run调用之前重置了同步上下文,现在它运行良好。

More meaningful code: 更有意义的代码:

[Test]
public async Task BasicControlTests()
{
  var form = new CustomForm();
  form.Method1();
  Assert....
}

[Test]
public async Task BasicControlTests()
{
    var form = new CustomForm();
    form.Method1();
    Assert....
}

[Test]
public async Task WebBrowserExtensionTest()
{
    SynchronizationContext.SetSynchronizationContext(null);

    await MessageLoopWorker.Run(async () => {
        var browser = new WebBrowser();
        // subscribe on browser's events
        // do something with browser
        // assert the event order
    });
}

When running the tests without nulling the sync context WebBrowserExtensionTest blocks when it follows BasicControlTests. 如果运行测试时没有使同步上下文无效,则WebBrowserExtensionTest在遵循BasicControlTests时会阻塞。 With nulling it pass well. 使用null可以顺利通过。

Is it ok to keep it like this? 这样保留它可以吗?

I repro'ed this under MSTest, but I believe all of the below applies to NUnit equally well. 我在MSTest下对此进行了重现,但我相信以下所有内容同样适用于NUnit。

First off all, I understand this code might have been taken out of context, but as is, it doesn't seem to be very useful. 首先,我知道该代码可能已脱离上下文,但就目前而言,它似乎不是很有用。 Why would you want to create a form inside NeverCompletes , which runs on an random MSTest/NUnit thread, different from the thread spawned by MessageLoopWorker ? 为什么要在NeverCompletes内创建一个窗体,该窗体运行在与MessageLoopWorker生成的线程不同的随机MSTest / NUnit线程上?

Anyhow, you're having a deadlock because using (new Form()) installs an instance of WindowsFormsSynchronizationContext on that original unit test thread. 无论如何,您陷入了僵局,因为using (new Form())在该原始单元测试线程上安装WindowsFormsSynchronizationContext的实例。 Check SynchronizationContext.Current after the using statement. using语句之后检查SynchronizationContext.Current Then, you facing a classic deadlock well explained by Stephen Cleary in his "Don't Block on Async Code" . 然后,您将面临一个经典的僵局,该僵局由Stephen Cleary在他的“不要阻止异步代码”中很好地解释。

Right, you don't block yourself but MSTest/NUnit does, because it is smart enough to recognize async Task signature of NeverCompletes method and then execute something like Task.Wait on the Task returned by it. 是的,您不会阻塞自己,但是MSTest / NUnit会阻塞,因为它足够聪明,可以识别NeverCompletes方法的async Task签名,然后Task.Wait返回的Task执行类似Task.Wait的操作。 Because the original unit test thread doesn't have a message loop and doesn't pump messages (unlike is expected by WindowsFormsSynchronizationContext ), the await continuation inside NeverCompletes never gets a chance to execute and Task.Wait is just hanging waiting. 因为原始的单元测试线程没有消息循环,也没有泵送消息(这与WindowsFormsSynchronizationContext所期望的不同),所以NeverCompletes内部的await延续永远不会执行, Task.Wait只是挂起了等待。

That said, MessageLoopWorker was only designed to create and run WinForms object inside the scope of the async method you pass to MessageLoopWorker.Run , and then be done. 就是说, MessageLoopWorker仅设计为传递给MessageLoopWorker.Run async方法范围内创建并运行WinForms对象,然后完成该操作。 Eg, the following wouldn't block: 例如,以下内容不会被阻止:

[TestMethod]
public async Task NeverCompletes()
{
    await MessageLoopWorker.Run(async (args) =>
    {
        using (new Form()) ;
        return Type.Missing;
    });
}

It was not designed to work with WinForms objects across multiple MessageLoopWorker.Run calls. 并非旨在与多个MessageLoopWorker.Run调用中的WinForms对象一起使用。 If that's what you need, you may want to look at my MessageLoopApartment from here , eg: 如果这是您需要的,则可能需要从这里查看我的MessageLoopApartment ,例如:

[TestMethod]
public async Task NeverCompletes()
{
    using (var apartment = new MessageLoopApartment())
    {
        // create a form inside MessageLoopApartment
        var form = apartment.Invoke(() => new Form {
            Width = 400, Height = 300, Left = 10, Top = 10, Visible = true });

        try
        {
            // await outside MessageLoopApartment's thread
            await Task.Delay(2000);

            await apartment.Run(async () =>
            {
                // this runs on MessageLoopApartment's STA thread 
                // which stays the same for the life time of 
                // this MessageLoopApartment instance

                form.Show();
                await Task.Delay(1000);
                form.BackColor = System.Drawing.Color.Green;
                await Task.Delay(2000);
                form.BackColor = System.Drawing.Color.Red;
                await Task.Delay(3000);

            }, CancellationToken.None);
        }
        finally
        {
            // dispose of WebBrowser inside MessageLoopApartment
            apartment.Invoke(() => form.Dispose());
        }
    }
}

Or, you can even use it across multiple unit test methods, if you're not concerned about potential coupling of tests, eg (MSTest): 或者,如果您不担心测试的潜在耦合,甚至可以跨多种单元测试方法使用它,例如(MSTest):

[TestClass]
public class MyTestClass
{
    static MessageLoopApartment s_apartment;

    [ClassInitialize]
    public static void TestClassSetup()
    {
        s_apartment = new MessageLoopApartment();
    }

    [ClassCleanup]
    public void TestClassCleanup()
    {
        s_apartment.Dispose();
    }

    // ...
}

Finally, neither MessageLoopWorker nor MessageLoopApartment were designed to work with WinForms object created on different threads (which is almost never a good idea anyway). 最后, MessageLoopWorkerMessageLoopApartment都没有设计为与在不同线程上创建的WinForms对象一起使用(无论如何这几乎不是一个好主意)。 You can have as many MessageLoopWorker / MessageLoopApartment instances as you like, but once a WinForm object has been created on the thread of a particular MessageLoopWorker / MessageLoopApartment instance, it should further be accessed and properly destroyed on the same thread only. 您可以根据需要拥有任意多个MessageLoopWorker / MessageLoopApartment实例,但是一旦在特定MessageLoopWorker / MessageLoopApartment实例的线程上创建了WinForm对象,则应进一步对其进行访问并仅在同一线程上对其进行适当销毁。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM