diff options
author | Lluis Sanchez <llsan@microsoft.com> | 2017-10-02 17:20:25 +0300 |
---|---|---|
committer | Lluis Sanchez <llsan@microsoft.com> | 2017-10-02 17:20:25 +0300 |
commit | f04a0fdd5e85b3761362cb30bb3648fe524d56a0 (patch) | |
tree | a27b52b49ccb72fee7e77d1fb821b7dcd57fd337 /Testing/CoreTests | |
parent | 249e0be72ddc2821cfdf54e22b40e5fd38c8ef67 (diff) |
Fix some context switching issues
Make sure that the original sync context is restored after invoking on
a specific toolkit. Also added some unit tests.
Diffstat (limited to 'Testing/CoreTests')
-rw-r--r-- | Testing/CoreTests/ContextSwitchTests.cs | 295 | ||||
-rw-r--r-- | Testing/CoreTests/CoreTests.csproj | 47 | ||||
-rw-r--r-- | Testing/CoreTests/FakeToolkit.cs | 186 |
3 files changed, 528 insertions, 0 deletions
diff --git a/Testing/CoreTests/ContextSwitchTests.cs b/Testing/CoreTests/ContextSwitchTests.cs new file mode 100644 index 00000000..f63ecefc --- /dev/null +++ b/Testing/CoreTests/ContextSwitchTests.cs @@ -0,0 +1,295 @@ +// +// Test.cs +// +// Author: +// Lluis Sanchez <llsan@microsoft.com> +// +// Copyright (c) 2017 Microsoft +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using NUnit.Framework; +using System; +using Xwt; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace CoreTests +{ + [TestFixture ()] + public class ContextSwitchTests + { + Toolkit mainToolkit; + Toolkit secToolkit; + + [Test] + public void AsyncContextSwitch () + { + Exception error = null; + Application.UnhandledException += (object sender, ExceptionEventArgs e) => { + error = e.ErrorException; + // If somethig goes wrong, make sure the main loop is exited + Application.Exit (); + }; + + // Load the main toolkit + + Application.Initialize (typeof (MainFakeToolkit).AssemblyQualifiedName); + mainToolkit = Toolkit.CurrentEngine; + + // Load a second toolkit + + secToolkit = Toolkit.Load (typeof (SecondaryFakeToolkit).AssemblyQualifiedName); + + // The main toolkit is the default toolkit by default + + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + // Run the tests in the simulated ui loop + var task = AsyncContextSwitchLoop (); + + Application.Run (); + task.Wait (); + + if (error != null) { + Console.WriteLine (error); + throw error; + } + + Application.Dispose (); + } + + async Task AsyncContextSwitchLoop () + { + // We are in the main toolkit + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + bool callbackInvoked = false; + + var t = secToolkit.Invoke (async delegate { + + // Current engine should now be the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + + await Task.Delay (200); + + // Current engine should still the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + Console.WriteLine ("Done"); + callbackInvoked = true; + }); + + // Invocation on the second toolkit should not change the + // current engine + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + await Task.Delay (2000); + + // During the wait, code in the context of the secondary context + // has been called, but the main toolkit must have been restored + + Assert.IsTrue (callbackInvoked); + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + Application.Exit (); + } + + [Test] + public void AppInvokeCapturesToolkit () + { + Exception error = null; + Application.UnhandledException += (object sender, ExceptionEventArgs e) => { + error = e.ErrorException; + // If somethig goes wrong, make sure the main loop is exited + Application.Exit (); + }; + + // Load the main toolkit + Application.Initialize (typeof (MainFakeToolkit).AssemblyQualifiedName); + mainToolkit = Toolkit.CurrentEngine; + + // Load a second toolkit + secToolkit = Toolkit.Load (typeof (SecondaryFakeToolkit).AssemblyQualifiedName); + + // The main toolkit is the default toolkit by default + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + secToolkit.Invoke (delegate { + // Current engine should now be the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + Application.TimeoutInvoke (100, () => { + // Current toolkit should still be the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + Application.Exit (); + return false; + }); + }); + + Application.Run (); + + if (error != null) { + Console.WriteLine (error); + throw error; + } + + Application.Dispose (); + } + + [Test] + public void TaskWaitRestoresMainToolkit () + { + Exception error = null; + Application.UnhandledException += (object sender, ExceptionEventArgs e) => { + error = e.ErrorException; + // If somethig goes wrong, make sure the main loop is exited + Application.Exit (); + }; + + // Load the main toolkit + + Application.Initialize (typeof (MainFakeToolkit).AssemblyQualifiedName); + mainToolkit = Toolkit.CurrentEngine; + + // Load a second toolkit + + secToolkit = Toolkit.Load (typeof (SecondaryFakeToolkit).AssemblyQualifiedName); + + // We are in the main toolkit + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + bool noXwtCallback = false; + + // Enqueue an event that will be executed directly by the + // event loop, out of the XWT context + EventQueue.MainEventQueue.Enqueue (delegate { + // When outside the XWT context, the default toolkit should be + // the main toolkit + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + noXwtCallback = true; + return false; + }, TimeSpan.FromMilliseconds (50)); + + var t = secToolkit.Invoke (async delegate { + + // Current engine should now be the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + + Assert.IsFalse (noXwtCallback); + + await Task.Delay (200); + + // Out of band event was executed during the wait + Assert.IsTrue (noXwtCallback); + + // Current engine should still the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + Console.WriteLine ("Done"); + Application.Exit (); + }); + + // Invocation on the second toolkit should not change the + // current engine + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + Application.Run (); + + if (error != null) { + Console.WriteLine (error); + throw error; + } + + Application.Dispose (); + } + + + [Test] + public void EventLoopPumpInMainToolkit () + { + Exception error = null; + Application.UnhandledException += (object sender, ExceptionEventArgs e) => { + error = e.ErrorException; + // If somethig goes wrong, make sure the main loop is exited + Application.Exit (); + }; + + // Load the main toolkit + + Application.Initialize (typeof (MainFakeToolkit).AssemblyQualifiedName); + mainToolkit = Toolkit.CurrentEngine; + + // Load a second toolkit + + secToolkit = Toolkit.Load (typeof (SecondaryFakeToolkit).AssemblyQualifiedName); + + // We are in the main toolkit + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + bool noXwtCallback = false; + + // Enqueue an event that will be executed directly by the + // event loop, out of the XWT context + EventQueue.MainEventQueue.Enqueue (delegate { + // When outside the XWT context, the default toolkit should be + // the main toolkit + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + noXwtCallback = true; + }); + + secToolkit.Invoke (delegate { + + // Current engine should now be the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + + Assert.IsFalse (noXwtCallback); + + Application.MainLoop.DispatchPendingEvents (); + + // Out of band event must have been dispatched + Assert.IsTrue (noXwtCallback); + + // Current engine should still the second toolkit + Assert.AreSame (secToolkit, Toolkit.CurrentEngine); + Console.WriteLine ("Done"); + Application.Exit (); + }); + + // Invocation on the second toolkit should not change the + // current engine + Assert.AreEqual (mainToolkit, Toolkit.CurrentEngine); + + Application.Run (); + + if (error != null) { + Console.WriteLine (error); + throw error; + } + + Application.Dispose (); + } + } + + class MainFakeToolkit : FakeToolkit + { + } + + class SecondaryFakeToolkit : FakeToolkit + { + + } +} diff --git a/Testing/CoreTests/CoreTests.csproj b/Testing/CoreTests/CoreTests.csproj new file mode 100644 index 00000000..99839e81 --- /dev/null +++ b/Testing/CoreTests/CoreTests.csproj @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{F0C03C12-F08A-4378-958D-86DD4CFE966F}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>CoreTests</RootNamespace>
+ <AssemblyName>CoreTests</AssemblyName>
+ <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug</OutputPath>
+ <DefineConstants>DEBUG;</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release</OutputPath>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="nunit.framework">
+ <HintPath>..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ContextSwitchTests.cs" />
+ <Compile Include="FakeToolkit.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\Xwt\Xwt.csproj">
+ <Project>{92494904-35FA-4DC9-BDE9-3A3E87AC49D3}</Project>
+ <Name>Xwt</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project>
\ No newline at end of file diff --git a/Testing/CoreTests/FakeToolkit.cs b/Testing/CoreTests/FakeToolkit.cs new file mode 100644 index 00000000..344e5ba2 --- /dev/null +++ b/Testing/CoreTests/FakeToolkit.cs @@ -0,0 +1,186 @@ +// +// FakeToolkit.cs +// +// Author: +// Lluis Sanchez <llsan@microsoft.com> +// +// Copyright (c) 2017 Microsoft +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using Xwt; +using Xwt.Backends; +using System.Linq; + +namespace CoreTests +{ + public class FakeToolkit : ToolkitEngineBackend + { + public FakeToolkit () + { + } + + public override void InitializeApplication () + { + base.InitializeApplication (); + EventQueue.MainEventQueue.Reset (); + } + + public override void DispatchPendingEvents () + { + EventQueue.MainEventQueue.DispatchPendingEvents (false); + } + + public override void ExitApplication () + { + EventQueue.MainEventQueue.Exit (); + } + + public override IWindowFrameBackend GetBackendForWindow (object nativeWindow) + { + throw new NotImplementedException (); + } + + public override object GetNativeWidget (Widget w) + { + throw new NotImplementedException (); + } + + public override object GetNativeWindow (IWindowFrameBackend backend) + { + throw new NotImplementedException (); + } + + public override bool HasNativeParent (Widget w) + { + throw new NotImplementedException (); + } + + public override void InvokeAsync (Action action) + { + EventQueue.MainEventQueue.Enqueue (delegate { + ApplicationContext.InvokeUserCode (action); + }); + } + + public override void RunApplication () + { + EventQueue.MainEventQueue.DispatchPendingEvents (true); + } + + public override object TimerInvoke (Func<bool> action, TimeSpan timeSpan) + { + return EventQueue.MainEventQueue.Enqueue (delegate { + bool result = false; + ApplicationContext.InvokeUserCode (delegate { + result = action (); + }); + return result; + }, timeSpan); + } + + void ScheduleTimerInvoke (CancellationTokenSource cts, Func<bool> action, TimeSpan timeSpan) + { + Task.Delay (timeSpan, cts.Token).ContinueWith (t => { + InvokeAsync (delegate { + if (action ()) + ScheduleTimerInvoke (cts, action, timeSpan); + }); + DispatchPendingEvents (); + }, TaskContinuationOptions.NotOnCanceled); + } + + public override void CancelTimerInvoke (object id) + { + ((CancellationTokenSource)id).Cancel (); + } + } + + public class EventQueue + { + Queue<Action> events = new Queue<Action> (); + bool exit; + + public static EventQueue MainEventQueue { get; } = new EventQueue (); + + public void Reset () + { + exit = false; + } + + public void DispatchPendingEvents (bool keepWaiting) + { + do { + List<Action> list; + lock (events) { + list = events.ToList (); + events.Clear (); + } + + foreach (var e in list) + e (); + + if (!keepWaiting) + return; + + lock (events) { + if (events.Count == 0 && !exit) + Monitor.Wait (events); + } + } while (!exit); + } + + public void Exit () + { + lock (events) { + exit = true; + Monitor.PulseAll (events); + } + } + + public void Enqueue (Action action) + { + lock (events) { + events.Enqueue (action); + Monitor.PulseAll (events); + } + } + + public CancellationTokenSource Enqueue (Func<bool> action, TimeSpan timeSpan) + { + CancellationTokenSource cts = new CancellationTokenSource (); + ScheduleTimerInvoke (cts, action, timeSpan); + return cts; + } + + void ScheduleTimerInvoke (CancellationTokenSource cts, Func<bool> action, TimeSpan timeSpan) + { + Task.Delay (timeSpan, cts.Token).ContinueWith (t => { + Enqueue (delegate { + if (action ()) + ScheduleTimerInvoke (cts, action, timeSpan); + }); + }, TaskContinuationOptions.NotOnCanceled); + } + } +} |