From 99bbd2c2c9cfb3b0fd2f7579e39b9e93866fe6af Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Wed, 19 Aug 2015 12:58:57 -0700 Subject: [PATCH] Remote execution from the UI The UI now executes the tests in a separate process. This removes the need to shadow copy and frees up the idea of re-running the tests without restart of the UI. --- xunit.runner.data/TestResultData.cs | 8 +- xunit.runner.worker/DiscoverUtil.cs | 2 +- xunit.runner.worker/RunUtil.cs | 1 + .../Converters/TestStateConverter.cs | 1 + xunit.runner.wpf/ITestUtil.cs | 24 +++++ .../Impl/RemoteTestUtil .BackgroundRunner.cs | 58 ++++++++++++ .../Impl/RemoteTestUtil .RunSession.cs | 74 +++++++++++++++ xunit.runner.wpf/Impl/RemoteTestUtil.cs | 21 ++++- xunit.runner.wpf/ViewModel/MainViewModel.cs | 91 +++---------------- .../ViewModel/TestCaseViewModel.cs | 1 + .../ViewModel/TestStateEventArgs.cs | 17 ---- xunit.runner.wpf/xunit.runner.wpf.csproj | 3 +- 12 files changed, 203 insertions(+), 98 deletions(-) create mode 100644 xunit.runner.wpf/Impl/RemoteTestUtil .BackgroundRunner.cs create mode 100644 xunit.runner.wpf/Impl/RemoteTestUtil .RunSession.cs delete mode 100644 xunit.runner.wpf/ViewModel/TestStateEventArgs.cs diff --git a/xunit.runner.data/TestResultData.cs b/xunit.runner.data/TestResultData.cs index c49cbe1..592962e 100644 --- a/xunit.runner.data/TestResultData.cs +++ b/xunit.runner.data/TestResultData.cs @@ -7,11 +7,17 @@ using System.Threading.Tasks; namespace xunit.runner.data { + /// + /// Note: More severe states are higher numbers. + /// + /// public enum TestState { + All = 0, + NotRun, Passed, + Skipped, Failed, - Skipped } public sealed class TestResultData diff --git a/xunit.runner.worker/DiscoverUtil.cs b/xunit.runner.worker/DiscoverUtil.cs index e442e4f..4203700 100644 --- a/xunit.runner.worker/DiscoverUtil.cs +++ b/xunit.runner.worker/DiscoverUtil.cs @@ -44,7 +44,7 @@ namespace xunit.runner.worker assemblyFileName: fileName, diagnosticMessageSink: new MessageVisitor(), shadowCopy: false)) - using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) + using (var writer = new BinaryWriter(stream, Constants.Encoding, leaveOpen: true)) using (var impl = new Impl(xunit, writer)) { xunit.Find(includeSourceInformation: false, messageSink: impl, discoveryOptions: TestFrameworkOptions.ForDiscovery()); diff --git a/xunit.runner.worker/RunUtil.cs b/xunit.runner.worker/RunUtil.cs index 61ef259..2457084 100644 --- a/xunit.runner.worker/RunUtil.cs +++ b/xunit.runner.worker/RunUtil.cs @@ -23,6 +23,7 @@ namespace xunit.runner.worker private void Process(string displayName, TestState state) { + Console.WriteLine($"{state} - {displayName}"); var result = new TestResultData(displayName, state); result.WriteTo(_writer); } diff --git a/xunit.runner.wpf/Converters/TestStateConverter.cs b/xunit.runner.wpf/Converters/TestStateConverter.cs index d267326..6727212 100644 --- a/xunit.runner.wpf/Converters/TestStateConverter.cs +++ b/xunit.runner.wpf/Converters/TestStateConverter.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Windows.Data; using System.Windows.Media; using System.Windows.Media.Imaging; +using xunit.runner.data; using xunit.runner.wpf.ViewModel; namespace xunit.runner.wpf.Converters diff --git a/xunit.runner.wpf/ITestUtil.cs b/xunit.runner.wpf/ITestUtil.cs index 82b0639..eaca6a5 100644 --- a/xunit.runner.wpf/ITestUtil.cs +++ b/xunit.runner.wpf/ITestUtil.cs @@ -4,6 +4,8 @@ using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Windows.Threading; +using xunit.runner.data; using xunit.runner.wpf.ViewModel; namespace xunit.runner.wpf @@ -14,5 +16,27 @@ namespace xunit.runner.wpf /// Discover the list of test cases which are available in the specified assembly. /// List Discover(string assemblyPath); + + /// + /// Begin a run of a unit test for the given assembly. + /// + ITestRunSession Run(Dispatcher dispatcher, string assemblyPath); + } + + internal sealed class TestResultEventArgs : EventArgs + { + internal readonly string TestCaseDisplayName; + internal readonly TestState TestState; + + internal TestResultEventArgs(string displayName, TestState state) + { + TestCaseDisplayName = displayName; + TestState = state; + } + } + + internal interface ITestRunSession + { + event EventHandler TestFinished; } } diff --git a/xunit.runner.wpf/Impl/RemoteTestUtil .BackgroundRunner.cs b/xunit.runner.wpf/Impl/RemoteTestUtil .BackgroundRunner.cs new file mode 100644 index 0000000..9ac17ec --- /dev/null +++ b/xunit.runner.wpf/Impl/RemoteTestUtil .BackgroundRunner.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using xunit.runner.data; +using xunit.runner.wpf.ViewModel; + +namespace xunit.runner.wpf.Impl +{ + internal partial class RemoteTestUtil + { + private sealed class BackgroundRunner + { + private readonly ConcurrentQueue _resultQueue; + private readonly BinaryReader _reader; + + internal BackgroundRunner(ConcurrentQueue resultQueue, BinaryReader reader) + { + _resultQueue = resultQueue; + _reader = reader; + } + + /// + /// This will be called on a background thread to read the results of the test from the + /// named pipe client stream. + /// + /// + internal Task GoOnBackground() + { + while (true) + { + TestResultData result; + try + { + result = TestResultData.ReadFrom(_reader); + } + catch + { + // Hacky way of detecting the stream being closed + break; + } + + _resultQueue.Enqueue(result); + } + + // Signal we are done + _resultQueue.Enqueue(null); + + return Task.FromResult(true); + } + } + } +} diff --git a/xunit.runner.wpf/Impl/RemoteTestUtil .RunSession.cs b/xunit.runner.wpf/Impl/RemoteTestUtil .RunSession.cs new file mode 100644 index 0000000..cc0ca7d --- /dev/null +++ b/xunit.runner.wpf/Impl/RemoteTestUtil .RunSession.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; +using xunit.runner.data; +using xunit.runner.wpf.ViewModel; + +namespace xunit.runner.wpf.Impl +{ + internal partial class RemoteTestUtil + { + private sealed class RunSession : ITestRunSession + { + private const int MaxResultPerTick = 100; + + private readonly Connection _connection; + private readonly ConcurrentQueue _resultQueue; + private readonly DispatcherTimer _timer; + private bool _closed; + private event EventHandler _testFinished; + + internal RunSession(Connection connection, Dispatcher dispatcher, ConcurrentQueue resultQueue) + { + _connection = connection; + _resultQueue = resultQueue; + _timer = new DispatcherTimer(TimeSpan.FromMilliseconds(100), DispatcherPriority.Normal, OnTimerTick, dispatcher); + } + + private void OnTimerTick(object sender, EventArgs e) + { + var i = 0; + TestResultData data; + while (i < MaxResultPerTick && _resultQueue.TryDequeue(out data)) + { + if (data == null) + { + Close(); + break; + } + + _testFinished?.Invoke(this, new TestResultEventArgs(data.TestCaseDisplayName, data.TestState)); + } + } + + internal void Close() + { + if (_closed) + { + return; + } + + _closed = true; + _timer.Stop(); + ((IDisposable)_connection).Dispose(); + } + + #region ITestRunSession + + event EventHandler ITestRunSession.TestFinished + { + add { _testFinished += value; } + remove { _testFinished -= value; } + } + + #endregion + } + } +} diff --git a/xunit.runner.wpf/Impl/RemoteTestUtil.cs b/xunit.runner.wpf/Impl/RemoteTestUtil.cs index 19afc5e..264fb71 100644 --- a/xunit.runner.wpf/Impl/RemoteTestUtil.cs +++ b/xunit.runner.wpf/Impl/RemoteTestUtil.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -6,12 +7,13 @@ using System.IO.Pipes; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Windows.Threading; using xunit.runner.data; using xunit.runner.wpf.ViewModel; namespace xunit.runner.wpf.Impl { - internal sealed class RemoteTestUtil : ITestUtil + internal sealed partial class RemoteTestUtil : ITestUtil { private sealed class Connection : IDisposable { @@ -78,7 +80,7 @@ namespace xunit.runner.wpf.Impl var list = new List(); using (var connection = StartWorkerProcess(Constants.ActionDiscover, assemblyPath)) - using (var reader = new BinaryReader(connection.Stream, Encoding.UTF8, leaveOpen: true)) + using (var reader = new BinaryReader(connection.Stream, Constants.Encoding, leaveOpen: true)) { try { @@ -97,6 +99,16 @@ namespace xunit.runner.wpf.Impl return list; } + private RunSession Run(Dispatcher dispatcher, string assemblyPath) + { + var connection = StartWorkerProcess(Constants.ActionRun, assemblyPath); + var queue = new ConcurrentQueue(); + var backgroundRunner = new BackgroundRunner(queue, new BinaryReader(connection.Stream, Constants.Encoding, leaveOpen: true)); + Task.Run(backgroundRunner.GoOnBackground); + + return new RunSession(connection, dispatcher, queue); + } + #region ITestUtil List ITestUtil.Discover(string assemblyPath) @@ -104,6 +116,11 @@ namespace xunit.runner.wpf.Impl return Discover(assemblyPath); } + ITestRunSession ITestUtil.Run(Dispatcher dispatcher, string assemblyPath) + { + return Run(dispatcher, assemblyPath); + } + #endregion } } diff --git a/xunit.runner.wpf/ViewModel/MainViewModel.cs b/xunit.runner.wpf/ViewModel/MainViewModel.cs index 4c16a98..e4bc08a 100644 --- a/xunit.runner.wpf/ViewModel/MainViewModel.cs +++ b/xunit.runner.wpf/ViewModel/MainViewModel.cs @@ -17,6 +17,7 @@ using System.IO.Pipes; using System.IO; using System.Text; using xunit.runner.data; +using System.Windows.Threading; namespace xunit.runner.wpf.ViewModel { @@ -203,44 +204,6 @@ namespace xunit.runner.wpf.ViewModel } } - private class TestRunVisitor : TestMessageVisitor - { - private readonly Func isCancelRequested; - private readonly IEnumerable testCases; - - public event EventHandler TestFinished; - - public TestRunVisitor(IEnumerable testCases, Func isCancelRequested) - { - this.testCases = testCases; - this.isCancelRequested = isCancelRequested; - } - - protected override bool Visit(ITestFailed testFailed) - { - var testCase = testCases.Single(tc => tc.DisplayName == testFailed.TestCase.DisplayName); - testCase.State = TestState.Failed; - TestFinished?.Invoke(this, TestStateEventArgs.Failed); - return !isCancelRequested(); - } - - protected override bool Visit(ITestPassed testPassed) - { - var testCase = testCases.Single(tc => tc.DisplayName == testPassed.TestCase.DisplayName); - testCase.State = TestState.Passed; - TestFinished?.Invoke(this, TestStateEventArgs.Passed); - return !isCancelRequested(); - } - - protected override bool Visit(ITestSkipped testSkipped) - { - var testCase = testCases.Single(tc => tc.DisplayName == testSkipped.TestCase.DisplayName); - testCase.State = TestState.Skipped; - TestFinished?.Invoke(this, TestStateEventArgs.Skipped); - return !isCancelRequested(); - } - } - private bool IsBusy { get { return isBusy; } @@ -270,7 +233,7 @@ namespace xunit.runner.wpf.ViewModel private bool CanExecuteRun() => !IsBusy && TestCases.Any(); - private async void OnExecuteRun() + private void OnExecuteRun() { try { @@ -280,7 +243,7 @@ namespace xunit.runner.wpf.ViewModel TestsFailed = 0; TestsSkipped = 0; CurrentRunState = TestState.NotRun; - await Task.Run(() => RunTestsInBackground()); + RunTests(); } catch (Exception ex) { @@ -293,37 +256,26 @@ namespace xunit.runner.wpf.ViewModel } } - private void RunTestsInBackground() + private void RunTests() { foreach (var tc in TestCases) { tc.State = TestState.NotRun; } - var selectedAssemblies = TestCases.ToLookup(tc => tc.AssemblyFileName); - using (AssemblyHelper.SubscribeResolve()) - { - foreach (var assembly in selectedAssemblies) - { - using (var xunit = new XunitFrontController( - assemblyFileName: assembly.Key, - useAppDomain: true, - shadowCopy: false, - diagnosticMessageSink: new DiagnosticMessageVisitor())) - using (var testRunVisitor = new TestRunVisitor(allTestCases, () => IsCancelRequested)) - { - testRunVisitor.TestFinished += TestRunVisitor_TestFinished; - xunit.RunTests(assembly.Select(tcvm => xunit.Deserialize(tcvm.TestCase)).ToArray(), testRunVisitor, TestFrameworkOptions.ForExecution()); - testRunVisitor.Finished.WaitOne(); - } - } - } + // Hacky way of using one assembly for now. Will expand later. + var assemblyPath = TestCases.Select(x => x.AssemblyFileName).First(); + var session = this.testUtil.Run(Dispatcher.CurrentDispatcher, assemblyPath); + session.TestFinished += OnTestFinished; } - private void TestRunVisitor_TestFinished(object sender, TestStateEventArgs e) + private void OnTestFinished(object sender, TestResultEventArgs e) { + var testCase = TestCases.Single(x => x.DisplayName == e.TestCaseDisplayName); + testCase.State = e.TestState; + TestsCompleted++; - switch (e.State) + switch (e.TestState) { case TestState.Passed: TestsPassed++; @@ -336,9 +288,9 @@ namespace xunit.runner.wpf.ViewModel break; } - if (e.State > CurrentRunState) + if (e.TestState > CurrentRunState) { - CurrentRunState = e.State; + CurrentRunState = e.TestState; } } @@ -379,19 +331,6 @@ namespace xunit.runner.wpf.ViewModel } } - /// - /// Note: More severe states are higher numbers. - /// - /// - public enum TestState - { - All = 0, - NotRun, - Passed, - Skipped, - Failed, - } - public class TestComparer : IComparer { public static TestComparer Instance { get; } = new TestComparer(); diff --git a/xunit.runner.wpf/ViewModel/TestCaseViewModel.cs b/xunit.runner.wpf/ViewModel/TestCaseViewModel.cs index 5a3e9c7..b83724f 100644 --- a/xunit.runner.wpf/ViewModel/TestCaseViewModel.cs +++ b/xunit.runner.wpf/ViewModel/TestCaseViewModel.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using xunit.runner.data; using Xunit.Abstractions; namespace xunit.runner.wpf.ViewModel diff --git a/xunit.runner.wpf/ViewModel/TestStateEventArgs.cs b/xunit.runner.wpf/ViewModel/TestStateEventArgs.cs deleted file mode 100644 index aaf1949..0000000 --- a/xunit.runner.wpf/ViewModel/TestStateEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace xunit.runner.wpf.ViewModel -{ - public class TestStateEventArgs : EventArgs - { - public static TestStateEventArgs Failed { get; } = new TestStateEventArgs(TestState.Failed); - public static TestStateEventArgs Passed { get; } = new TestStateEventArgs(TestState.Passed); - public static TestStateEventArgs Skipped { get; } = new TestStateEventArgs(TestState.Skipped); - private TestStateEventArgs(TestState state) - { - this.State = state; - } - - public TestState State { get; } - } -} \ No newline at end of file diff --git a/xunit.runner.wpf/xunit.runner.wpf.csproj b/xunit.runner.wpf/xunit.runner.wpf.csproj index ab60505..c0d145d 100644 --- a/xunit.runner.wpf/xunit.runner.wpf.csproj +++ b/xunit.runner.wpf/xunit.runner.wpf.csproj @@ -89,12 +89,13 @@ + + - MSBuild:Compile