diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2f8f487..bb02267 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,7 +2,10 @@ variables: solution: 'src/Eto.Toolkit.sln' build.version: '0.1.0-ci-$(Build.BuildNumber)' build.configuration: 'Release' - build.arguments: /restore /t:Build;Pack /p:BuildVersion=$(build.version) /p:BuildBranch=$(Build.SourceBranch) + build.basearguments: '/v:minimal /p:BuildVersion=$(build.version) /p:BuildBranch=$(Build.SourceBranch)' + build.arguments: '/restore /t:Build;Pack $(build.basearguments)' + build.dotnetversion: 3.1.100 + build.xamarinversion: 6_6_0 trigger: - master @@ -17,8 +20,16 @@ jobs: steps: - checkout: self submodules: true - - script: sudo $AGENT_HOMEDIRECTORY/scripts/select-xamarin-sdk.sh 5_18_1 + + - script: sudo $AGENT_HOMEDIRECTORY/scripts/select-xamarin-sdk.sh $(build.xamarinversion) displayName: 'Select Xamarin SDK version' + + - task: UseDotNet@2 + displayName: Install .NET Core SDK + inputs: + packageType: sdk + version: $(build.dotnetversion) + - task: MSBuild@1 displayName: Restore, Build and Package inputs: @@ -26,6 +37,7 @@ jobs: platform: '$(build.platform)' configuration: '$(build.configuration)' msbuildArguments: '$(build.arguments)' + - task: PublishBuildArtifacts@1 displayName: Publish nupkg inputs: @@ -33,6 +45,23 @@ jobs: artifactName: nuget publishLocation: container + - task: CopyFiles@2 + displayName: Copy Eto.UnitTest.Desktop.app + inputs: + sourceFolder: artifacts/core/$(build.configuration)/net461 + contents: Eto.UnitTest.Desktop.app/** + targetFolder: $(Build.ArtifactStagingDirectory)/dmg + + - script: hdiutil create -fs HFS+ -srcfolder "$(Build.ArtifactStagingDirectory)/dmg" -volname "Eto.UnitTest.Desktop" "$(Build.ArtifactStagingDirectory)/Eto.UnitTest.Desktop.dmg" + displayName: 'Create Eto.UnitTest.Desktop.dmg' + + - task: PublishBuildArtifacts@1 + displayName: Publish Eto.UnitTest.Desktop for Mac + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory)/Eto.UnitTest.Desktop.dmg + artifactName: Eto.UnitTest + publishLocation: container + - job: Windows pool: vmImage: 'windows-2019' @@ -41,16 +70,45 @@ jobs: steps: - checkout: self submodules: true + + - task: UseDotNet@2 + displayName: Install .NET Core SDK + inputs: + packageType: sdk + version: $(build.dotnetversion) + - task: MSBuild@1 displayName: Restore, Build and Package inputs: solution: '$(solution)' platform: '$(build.platform)' configuration: '$(build.configuration)' - msbuildArguments: '$(build.arguments)' + msbuildArguments: '/t:Build;Pack $(build.arguments)' + + - task: MSBuild@1 + displayName: Publish Eto.UnitTest.Desktop + inputs: + solution: 'src/Eto.UnitTest.Desktop/Eto.UnitTest.Desktop.csproj' + platform: '$(build.platform)' + configuration: '$(build.configuration)' + msbuildArguments: '/t:Publish $(build.basearguments)' + - task: PublishBuildArtifacts@1 displayName: Publish nupkg inputs: pathtoPublish: artifacts/nuget/$(build.configuration) artifactName: nuget publishLocation: container + + - task: CopyFiles@2 + displayName: Copy Eto.UnitTest.Desktop + inputs: + sourceFolder: artifacts/core/Windows/$(build.configuration)/net461/publish + targetFolder: $(Build.ArtifactStagingDirectory)/Eto.UnitTest.Desktop/ + + - task: PublishBuildArtifacts@1 + displayName: Publish Eto.UnitTest.Desktop + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory) + artifactName: Eto.UnitTest + publishLocation: container diff --git a/src/Eto.Toolkit.sln b/src/Eto.Toolkit.sln index d3fb800..0e57218 100644 --- a/src/Eto.Toolkit.sln +++ b/src/Eto.Toolkit.sln @@ -42,6 +42,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{23803176 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Eto.HtmlRenderer.XamMac2", "Eto.HtmlRenderer.Mac\Eto.HtmlRenderer.XamMac2.csproj", "{B5D415B5-22D9-44C9-888A-26301E76AE99}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTest", "UnitTest", "{AEA85D35-82E6-4863-B54C-4B6EE89D9234}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Eto.UnitTest", "Eto.UnitTest\Eto.UnitTest.csproj", "{6E2E7041-9E81-4B7D-BE9D-A3796954871D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Eto.UnitTest.NUnit", "Eto.UnitTest.NUnit\Eto.UnitTest.NUnit.csproj", "{3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Eto.UnitTest.Xunit", "Eto.UnitTest.Xunit\Eto.UnitTest.Xunit.csproj", "{CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Eto.UnitTest.Desktop", "Eto.UnitTest.Desktop\Eto.UnitTest.Desktop.csproj", "{9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Mac = Debug|Mac @@ -140,6 +150,38 @@ Global {B5D415B5-22D9-44C9-888A-26301E76AE99}.Release|Mac.ActiveCfg = Release|Any CPU {B5D415B5-22D9-44C9-888A-26301E76AE99}.Release|Mac.Build.0 = Release|Any CPU {B5D415B5-22D9-44C9-888A-26301E76AE99}.Release|Windows.ActiveCfg = Release|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Debug|Mac.ActiveCfg = Debug|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Debug|Mac.Build.0 = Debug|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Debug|Windows.ActiveCfg = Debug|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Debug|Windows.Build.0 = Debug|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Release|Mac.ActiveCfg = Release|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Release|Mac.Build.0 = Release|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Release|Windows.ActiveCfg = Release|Any CPU + {6E2E7041-9E81-4B7D-BE9D-A3796954871D}.Release|Windows.Build.0 = Release|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Debug|Mac.ActiveCfg = Debug|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Debug|Mac.Build.0 = Debug|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Debug|Windows.ActiveCfg = Debug|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Debug|Windows.Build.0 = Debug|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Release|Mac.ActiveCfg = Release|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Release|Mac.Build.0 = Release|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Release|Windows.ActiveCfg = Release|Any CPU + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A}.Release|Windows.Build.0 = Release|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Debug|Mac.ActiveCfg = Debug|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Debug|Mac.Build.0 = Debug|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Debug|Windows.ActiveCfg = Debug|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Debug|Windows.Build.0 = Debug|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Release|Mac.ActiveCfg = Release|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Release|Mac.Build.0 = Release|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Release|Windows.ActiveCfg = Release|Any CPU + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE}.Release|Windows.Build.0 = Release|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Debug|Mac.ActiveCfg = Debug|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Debug|Mac.Build.0 = Debug|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Debug|Windows.ActiveCfg = Debug|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Debug|Windows.Build.0 = Debug|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Release|Mac.ActiveCfg = Release|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Release|Mac.Build.0 = Release|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Release|Windows.ActiveCfg = Release|Any CPU + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663}.Release|Windows.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -158,6 +200,10 @@ Global {364EDFB1-8C1A-46B1-A9F7-B9D7F9417E55} = {C5C9E1F4-B112-4BDE-A589-722E87D63962} {F9D593CA-8448-466D-BC47-A1753E3949FD} = {C5C9E1F4-B112-4BDE-A589-722E87D63962} {B5D415B5-22D9-44C9-888A-26301E76AE99} = {C5C9E1F4-B112-4BDE-A589-722E87D63962} + {6E2E7041-9E81-4B7D-BE9D-A3796954871D} = {AEA85D35-82E6-4863-B54C-4B6EE89D9234} + {3076C3F6-4313-47DD-8CB2-C1B1A617FC6A} = {AEA85D35-82E6-4863-B54C-4B6EE89D9234} + {CBCB2F2F-0BCF-4B0B-8AAC-85891FDDCEEE} = {AEA85D35-82E6-4863-B54C-4B6EE89D9234} + {9E9C86EE-7E14-4E98-93EA-6CC8F54DC663} = {AEA85D35-82E6-4863-B54C-4B6EE89D9234} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B290EE1E-3782-4C57-A127-FD078514CD8C} diff --git a/src/Eto.UnitTest.Desktop/App.config b/src/Eto.UnitTest.Desktop/App.config new file mode 100644 index 0000000..731f6de --- /dev/null +++ b/src/Eto.UnitTest.Desktop/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Eto.UnitTest.Desktop/AppDomainRunner.cs b/src/Eto.UnitTest.Desktop/AppDomainRunner.cs new file mode 100644 index 0000000..f569d5b --- /dev/null +++ b/src/Eto.UnitTest.Desktop/AppDomainRunner.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.UnitTest.App +{ + class AppDomainTestRunnerWrapper : MarshalByRefObject + { + ITestRunner _runner; + public bool IsRunning => _runner.IsRunning; + + public ITest Test => _runner.TestSuite; + + public event EventHandler Log; + public event EventHandler Progress; + public event EventHandler TestFinished; + public event EventHandler TestStarted; + public event EventHandler IsRunningChanged; + + public async Task> GetCategories(ITestFilter filter) + { + var categories = await _runner.GetCategories(filter); + return categories.ToList(); + } + + public Task GetTestCount(ITestFilter filter) + { + return _runner.GetTestCount(filter); + } + + public Task RunAsync(ITestFilter filter) + { + return _runner.RunAsync(filter); + } + + public void StopTests() => _runner.StopTests(); + + public void CreateRunner(Type type) + { + var runnerType = Activator.CreateInstance(type) as ITestRunnerType; + _runner = runnerType.CreateRunner(); + } + + public Task Load(ITestSource source) => _runner.Load(source); + } + + public class AppDomainTestRunner : ITestRunner, IDisposable + { + AppDomainTestRunnerWrapper _wrapper; + public bool IsRunning => _wrapper.IsRunning; + + public ITest TestSuite => _wrapper.Test; + + public event EventHandler Log; + public event EventHandler Progress; + public event EventHandler TestFinished; + public event EventHandler TestStarted; + public event EventHandler IsRunningChanged; + + public AppDomainTestRunner() + { + _wrapper = Domain.CreateInstanceFromAndUnwrap(typeof(AppDomainTestRunnerWrapper).Assembly.CodeBase, typeof(AppDomainTestRunnerWrapper).FullName) as AppDomainTestRunnerWrapper; + } + + public void Initialize(Type runnerType) + { + _wrapper.CreateRunner(runnerType); + } + + AppDomain _domain; + + AppDomain Domain + { + get + { + if (_domain != null) + return _domain; + + var cur = AppDomain.CurrentDomain; + var si = cur.SetupInformation; + //var path = AssemblyPath; + var setup = new AppDomainSetup + { + ApplicationName = si.ApplicationName ?? "Eto.UnitTest", + CachePath = si.CachePath, + ShadowCopyFiles = "true",//si.ShadowCopyFiles, + ShadowCopyDirectories = si.ShadowCopyDirectories, + PrivateBinPath = si.PrivateBinPath, + //DynamicBase = si.DynamicBase, + TargetFrameworkName = si.TargetFrameworkName, + ConfigurationFile = si.ConfigurationFile, + ApplicationBase = si.ApplicationBase, + LoaderOptimization = LoaderOptimization.MultiDomain,//= si.LoaderOptimization, + //SandboxInterop = si.SandboxInterop + }; + //setup.SetCompatibilitySwitches(new[] { "NetFx40_LegacySecurityPolicy" }); + _domain = AppDomain.CreateDomain("UnitTests");//, cur.Evidence, setup); + cur.DomainUnload += (sender, e) => UnloadDomain(); + return _domain; + } + } + + public Task> GetCategories(ITestFilter filter) + { + return _wrapper.GetCategories(filter); + } + + public Task GetTestCount(ITestFilter filter) + { + return _wrapper.GetTestCount(filter); + } + + public Task RunAsync(ITestFilter filter) + { + return _wrapper.RunAsync(filter); + } + + public void StopTests() + { + _wrapper.StopTests(); + } + + void UnloadDomain() + { + if (_domain == null) + return; + + AppDomain.Unload(_domain); + _domain = null; + } + + public void Dispose() => UnloadDomain(); + + public Task Load(ITestSource source) => _wrapper.Load(source); + } + +} diff --git a/src/Eto.UnitTest.Desktop/Eto.UnitTest.Desktop.csproj b/src/Eto.UnitTest.Desktop/Eto.UnitTest.Desktop.csproj new file mode 100644 index 0000000..c7e0b5e --- /dev/null +++ b/src/Eto.UnitTest.Desktop/Eto.UnitTest.Desktop.csproj @@ -0,0 +1,36 @@ + + + + + + WinExe + net461 + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Eto.UnitTest.Desktop/MainForm.cs b/src/Eto.UnitTest.Desktop/MainForm.cs new file mode 100644 index 0000000..912473a --- /dev/null +++ b/src/Eto.UnitTest.Desktop/MainForm.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Eto.Forms; +using Eto.UnitTest.UI; + +namespace Eto.UnitTest.App +{ + + public class MainForm : Form + { + public MainForm(IReadOnlyCollection assemblies) + { + var viewModel = new UnitTestViewModel(); + DataContext = viewModel; + + Title = "Eto Unit Test"; + + NUnit.NUnitTestRunnerType.Register(); + Xunit.XunitTestRunnerType.Register(); + + var unitTestPanel = new UnitTestPanel(true); + unitTestPanel.BindDataContext(c => c.Runner, (UnitTestViewModel m) => m.Runner); + + Content = unitTestPanel; + + CreateMenu(); + + if (assemblies?.Count > 0) + { + Application.Instance.AsyncInvoke(() => + { + foreach (var assembly in assemblies) + { + viewModel.OpenFile(this, assembly.FullName); + } + }); + } + + } + + void CreateMenu() + { + var openItem = new ButtonMenuItem { Text = "Open...", Shortcut = Application.Instance.CommonModifier | Keys.O }; + openItem.BindDataContext(c => c.Command, (UnitTestViewModel m) => m.OpenCommand); + openItem.CommandParameter = this; + + var clear = new ButtonMenuItem { Text = "Close tests" }; + clear.BindDataContext(c => c.Command, (UnitTestViewModel m) => m.ClearCommand); + + var showOutput = new CheckMenuItem { Text = "Show output" }; + showOutput.BindDataContext(c => c.Checked, (UnitTestViewModel m) => m.ShowOutput); + + var showOnlyFailed = new CheckMenuItem { Text = "Show only failed tests" }; + showOnlyFailed.BindDataContext(c => c.Checked, (UnitTestViewModel m) => m.ShowOnlyFailed); + + Menu = new MenuBar + { + Items = + { + new ButtonMenuItem { Text = "&File", Items = { openItem, clear } }, + new ButtonMenuItem { Text = "&View", Items = { showOutput, showOnlyFailed } } + } + }; + } + } +} diff --git a/src/Eto.UnitTest.Desktop/Program.cs b/src/Eto.UnitTest.Desktop/Program.cs new file mode 100644 index 0000000..c3c3529 --- /dev/null +++ b/src/Eto.UnitTest.Desktop/Program.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Eto.Forms; + +namespace Eto.UnitTest.App +{ + class Program + { + [STAThread] + static int Main(string[] args) + { + // options + var assemblyOption = new Option(new[] { "-a", "--assembly" }, "The assembly(ies) to test.") + { + Argument = new Argument { Arity = ArgumentArity.ZeroOrMore } + }; + + // Add them to the root command + var rootCommand = new RootCommand { Description = "Eto.UnitTest" }; + rootCommand.AddOption(assemblyOption); + + + var result = rootCommand.Parse(args); + FileInfo[] assemblies = null; + if (result.HasOption(assemblyOption)) + { + assemblies = result.FindResultFor(assemblyOption)?.GetValueOrDefault(); + } + + new Application().Run(new MainForm(assemblies)); + return 0; + } + } +} diff --git a/src/Eto.UnitTest.Desktop/UnitTestViewModel.cs b/src/Eto.UnitTest.Desktop/UnitTestViewModel.cs new file mode 100644 index 0000000..9f33e13 --- /dev/null +++ b/src/Eto.UnitTest.Desktop/UnitTestViewModel.cs @@ -0,0 +1,102 @@ +using System.ComponentModel; +using System.Reflection; +using System.Windows.Input; +using Eto.Forms; +using Eto.UnitTest.UI; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System; +using Eto.UnitTest.Runners; + +namespace Eto.UnitTest.App +{ + class UnitTestViewModel : INotifyPropertyChanged + { + ITestRunner _runner; + MultipleTestRunner _multipleTestRunner; + + public ICommand OpenCommand => new RelayCommand(OpenRunner); + + public ICommand ClearCommand => new RelayCommand(ClearRunner); + + public bool ShowOnlyFailed + { + get => (_runner as LoggingTestRunner)?.ShowOnlyFailed ?? false; + set + { + if (_runner is LoggingTestRunner runner) + runner.ShowOnlyFailed = value; + OnPropertyChanged(); + } + } + + public bool ShowOutput + { + get => (_runner as LoggingTestRunner)?.ShowOutput ?? false; + set + { + if (_runner is LoggingTestRunner runner) + runner.ShowOutput = value; + OnPropertyChanged(); + } + } + + private void ClearRunner() + { + _multipleTestRunner.Clear(); + Runner = null; + } + + public ITestRunner Runner + { + get => _runner; + set + { + _runner = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowOutput)); + OnPropertyChanged(nameof(ShowOnlyFailed)); + } + } + + void OnPropertyChanged([CallerMemberName] string memberName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memberName)); + } + + public event PropertyChangedEventHandler PropertyChanged; + + void OpenRunner(Control parent) + { + var open = new OpenFileDialog(); + open.CheckFileExists = true; + open.Filters.Add(new FileFilter("Assembly", ".dll")); + open.Filters.Add(new FileFilter("Executable", ".exe")); + open.CurrentFilterIndex = 0; + if (open.ShowDialog(parent) == DialogResult.Ok) + { + OpenFile(parent, open.FileName); + } + } + + public void OpenFile(Control parent, string fileName) + { + Application.Instance.Invoke(async () => + { + var source = new TestSource(fileName); + if (_multipleTestRunner == null) + _multipleTestRunner = new MultipleTestRunner(); + try + { + await _multipleTestRunner.Load(source); + + Runner = new LoggingTestRunner(_multipleTestRunner); + } + catch (Exception ex) + { + MessageBox.Show(parent, $"Error loading assembly: {ex.Message}", MessageBoxType.Information); + } + }); + } + } +} diff --git a/src/Eto.UnitTest.NUnit/Eto.UnitTest.NUnit.csproj b/src/Eto.UnitTest.NUnit/Eto.UnitTest.NUnit.csproj new file mode 100644 index 0000000..b3cc860 --- /dev/null +++ b/src/Eto.UnitTest.NUnit/Eto.UnitTest.NUnit.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + $(DefineConstants);INCLUDE_TESTS + + + + NUnit testing UI and utilities for Eto.Forms + Provides a control to use in Eto.Forms applications that can be used to display, filter, and run unit tests. + xunit;unit test;test;testing;tdd + + + + + + + + + + + diff --git a/src/Eto.UnitTest.NUnit/InvokeOnUIAttribute.cs b/src/Eto.UnitTest.NUnit/InvokeOnUIAttribute.cs new file mode 100644 index 0000000..2af87dd --- /dev/null +++ b/src/Eto.UnitTest.NUnit/InvokeOnUIAttribute.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.ExceptionServices; +using Eto.Forms; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using NUnit.Framework.Internal.Commands; + +namespace Eto.UnitTest.NUnit +{ + + /// + /// Runs the test on the UI thread + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class InvokeOnUIAttribute : Attribute, IWrapSetUpTearDown + { + /// + /// Wraps the specified command + /// + /// command to wrap + /// A new command that wraps the specified command in a UI invoke + public TestCommand Wrap(TestCommand command) => new RunOnUICommand(command); + + class RunOnUICommand : DelegatingTestCommand + { + public RunOnUICommand(TestCommand innerCommand) + : base(innerCommand) + { + } + + public override TestResult Execute(TestExecutionContext context) + { + Exception exception = null; + + var result = Application.Instance.Invoke(() => + { + try + { + context.EstablishExecutionEnvironment(); + return innerCommand.Execute(context); + } + catch (Exception ex) + { + exception = ex; + return null; + } + }); + + if (exception != null) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + + return result; + } + } + } +} diff --git a/src/Eto.UnitTest.NUnit/MyTests.cs b/src/Eto.UnitTest.NUnit/MyTests.cs new file mode 100644 index 0000000..70da54b --- /dev/null +++ b/src/Eto.UnitTest.NUnit/MyTests.cs @@ -0,0 +1,161 @@ +using System; +using NUnit.Framework; +using System.Threading; +using Eto.Forms; +using System.Threading.Tasks; + +#if INCLUDE_TESTS + +// Example tests for.. testing puproses + +[assembly: Parallelizable(ParallelScope.All)] + + +namespace Eto.UnitTest.NUnit.TheTests +{ + [TestFixture] + public class SomeTests + { + [Category("CategoryA")] + [Test] + public void Blar() + { + Thread.Sleep(1000); + } + + [Category("CategoryB")] + [Test] + public void ShouldNotPass() + { + Thread.Sleep(1000); + throw new InvalidOperationException("woo"); + } + } + + [TestFixture] + public class TestStatus + { + [Test] + public void TestShouldError() + { + Thread.Sleep(200); + throw new InvalidOperationException("this is a non-test exception"); + } + + [Test] + public void TestShouldFail() + { + Thread.Sleep(200); + Assert.AreEqual(10, 20); + } + + [Test] + public void TestShouldPass() + { + Thread.Sleep(200); + Assert.AreEqual(10, 10); + } + + [Test] + [Ignore("Ignore reason")] + public void TestShouldSkip() + { + Thread.Sleep(200); + throw new InvalidOperationException("boo"); + } + + [TestCase(3)] + [TestCase(5)] + [TestCase(6, Ignore = "Not working")] + [TestCase(8)] + public void TestTheory(int value) + { + Thread.Sleep(200); + Assert.True(value % 2 == 1); + } + } +} + +namespace Eto.UnitTest.NUnit.OtherTests +{ + [TestFixture] + public class SomeOtherTests + { + [Category("CategoryA")] + [Test] + public void Test1() + { + Thread.Sleep(1000); + } + + [Test] + public void Test2() + { + Thread.Sleep(1000); + throw new InvalidOperationException("woo"); + } + } + + [TestFixture] + public class MoreTests + { + [Test] + public void Test1() + { + Thread.Sleep(1000); + } + + [Test] + public void Test2() + { + Thread.Sleep(1000); + } + + [Test] + public void Test3() + { + Thread.Sleep(1000); + } + + [Test] + public void Test4() + { + Thread.Sleep(1000); + } + } + + [TestFixture] + public class UITests + { + [Test] + [InvokeOnUI] + public void RunThisOnUIThread() + { + Thread currentThread = Thread.CurrentThread; + Thread uiThread = null; + Application.Instance.Invoke(() => + { + uiThread = Thread.CurrentThread; + }); + + Assert.IsNotNull(uiThread, "#1. UI thread was not found"); + Assert.AreEqual(currentThread.ManagedThreadId, uiThread.ManagedThreadId, "#2. UI Thread should be the same as the test thread"); + } + + [Test] + public void RunThisOnTestThread() + { + Thread currentThread = Thread.CurrentThread; + Thread uiThread = null; + Application.Instance.Invoke(() => + { + uiThread = Thread.CurrentThread; + }); + + Assert.IsNotNull(uiThread, "#1. UI thread was not found"); + Assert.AreNotEqual(currentThread.ManagedThreadId, uiThread.ManagedThreadId, "#2. UI Thread should not be the same as the test thread"); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Eto.UnitTest.NUnit/NUnitTestRunner.cs b/src/Eto.UnitTest.NUnit/NUnitTestRunner.cs new file mode 100644 index 0000000..1e1328f --- /dev/null +++ b/src/Eto.UnitTest.NUnit/NUnitTestRunner.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Eto.UnitTest; +using Eto.UnitTest.Runners; +using nua = NUnit.Framework.Api; +using nui = NUnit.Framework.Interfaces; +using nuint = NUnit.Framework.Internal; + +[assembly: TestRunnerType(typeof(Eto.UnitTest.NUnit.NUnitTestRunnerType))] + +namespace Eto.UnitTest.NUnit +{ + public class NUnitTestRunnerType : TestRunnerType + { + public override string Name => "NUnit"; + + public static void Register() => TestRunnerType.Add(new NUnitTestRunnerType()); + + protected override string[] RequiredReferences => new[] { "nunit.framework" }; + } + + public class NUnitTestRunner : ProgressTestRunner, nui.ITestListener + { + TaskCompletionSource _tcs; + nua.ITestAssemblyBuilder _builder = new nua.DefaultTestAssemblyBuilder(); + nua.ITestAssemblyRunner _runner; + + public nua.ITestAssemblyRunner NUnitRunner => _runner; + + public override Task Load(ITestSource source) + { + var settings = new Dictionary(); + + _runner = new nua.NUnitTestAssemblyRunner(_builder); + _runner.Load(source.Assembly, settings); + return Task.CompletedTask; + } + + public override ITest TestSuite => new TestWrapper(_runner.ExploreTests(nuint.TestFilter.Empty)); + + public override Task> GetCategories(ITestFilter filter) + { + return Task.FromResult(TestSuite.GetChildren().Where(filter.Pass).SelectMany(r => r.Categories).Distinct()); + } + + public override Task GetTestCount(ITestFilter filter) + { + return Task.FromResult(TestSuite.GetChildren().Count(r => !r.IsSuite && filter.Pass(r))); + } + + protected override Task RunInternalAsync(ITestFilter filter) + { + _tcs = new TaskCompletionSource(); + new nuint.TestExecutionContext.AdhocContext().EstablishExecutionEnvironment(); + _runner.RunAsync(this, new TestFilterWrapper(filter)); + return _tcs.Task; + } + + public override void StopTests() + { + lock (this) + { + _runner.StopRun(true); + _tcs.SetResult(new TestResultWrapper(_runner.Result)); + } + } + + void nui.ITestListener.TestStarted(nui.ITest test) + { + OnTestStarted(new UnitTestTestEventArgs(new TestWrapper(test))); + } + + void nui.ITestListener.TestFinished(nui.ITestResult result) + { + var wrappedResult = new TestResultWrapper(result); + OnTestFinished(new UnitTestResultEventArgs(wrappedResult)); + + if (result.Test is nuint.TestAssembly) + { + _tcs.SetResult(new TestResultWrapper(_runner.Result)); + } + } + + void nui.ITestListener.TestOutput(nui.TestOutput output) + { + WriteLog(output.ToString()); + } + + void nui.ITestListener.SendMessage(nui.TestMessage message) + { + WriteLog(message.ToString()); + } + + } +} diff --git a/src/Eto.UnitTest.NUnit/TestAssertionWrapper.cs b/src/Eto.UnitTest.NUnit/TestAssertionWrapper.cs new file mode 100644 index 0000000..e32e6e9 --- /dev/null +++ b/src/Eto.UnitTest.NUnit/TestAssertionWrapper.cs @@ -0,0 +1,56 @@ +using System; +using NUnit.Framework.Interfaces; + +namespace Eto.UnitTest.NUnit +{ + class TestAssertion : ITestAssertion + { + public string Message { get; set; } + + public TestStatus Status { get; set; } + + public string StackTrace { get; set; } + + public object NativeObject => null; + } + + class TestAssertionWrapper : ITestAssertion + { + AssertionResult _assertion; + + public TestAssertionWrapper(AssertionResult assertion) + { + _assertion = assertion; + } + + public string Message => _assertion.Message; + + public TestStatus Status + { + get + { + switch (_assertion.Status) + { + case AssertionStatus.Error: + return TestStatus.Failed; + //return TestStatus.Error; + case AssertionStatus.Failed: + return TestStatus.Failed; + case AssertionStatus.Inconclusive: + return TestStatus.Inconclusive; + case AssertionStatus.Passed: + return TestStatus.Passed; + case AssertionStatus.Warning: + return TestStatus.Warning; + default: + throw new NotSupportedException(); + } + } + + } + + public string StackTrace => _assertion.StackTrace; + + public object NativeObject => _assertion; + } +} \ No newline at end of file diff --git a/src/Eto.UnitTest.NUnit/TestFilterWrapper.cs b/src/Eto.UnitTest.NUnit/TestFilterWrapper.cs new file mode 100644 index 0000000..a40a064 --- /dev/null +++ b/src/Eto.UnitTest.NUnit/TestFilterWrapper.cs @@ -0,0 +1,23 @@ +using System; +using nui = NUnit.Framework.Interfaces; + +namespace Eto.UnitTest.NUnit +{ + class TestFilterWrapper : nui.ITestFilter + { + ITestFilter _filter; + + public TestFilterWrapper(ITestFilter filter) + { + _filter = filter; + } + + public bool IsExplicitMatch(nui.ITest test) => _filter.IsExplicitMatch(new TestWrapper(test)); + + public bool Pass(nui.ITest test) => _filter.Pass(new TestWrapper(test)); + + public nui.TNode ToXml(bool recursive) => throw new NotImplementedException(); + + public nui.TNode AddToXml(nui.TNode parentNode, bool recursive) => throw new NotImplementedException(); + } +} diff --git a/src/Eto.UnitTest.NUnit/TestResultWrapper.cs b/src/Eto.UnitTest.NUnit/TestResultWrapper.cs new file mode 100644 index 0000000..6c9b322 --- /dev/null +++ b/src/Eto.UnitTest.NUnit/TestResultWrapper.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using nui = NUnit.Framework.Interfaces; + +namespace Eto.UnitTest.NUnit +{ + class TestResultWrapper : ITestResult + { + nui.ITestResult _result; + TestStatus? _status; + ITest _test; + + public TestResultWrapper(nui.ITestResult result) + { + _result = result; + } + + public int AssertCount => _result.AssertCount; + + public int FailCount => _result.FailCount; + + public int ErrorCount => 0; + + public int WarningCount => _result.WarningCount; + + public int PassCount => _result.PassCount; + + public int InconclusiveCount => _result.InconclusiveCount; + + public int SkipCount => _result.SkipCount; + + public TestStatus Status => _status ?? (_status = GetStatus()).Value; + + private TestStatus GetStatus() + { + switch (_result.ResultState.Status) + { + case nui.TestStatus.Failed: + return TestStatus.Failed; + case nui.TestStatus.Inconclusive: + return TestStatus.Inconclusive; + case nui.TestStatus.Passed: + return TestStatus.Passed; + case nui.TestStatus.Skipped: + return TestStatus.Skipped; + case nui.TestStatus.Warning: + return TestStatus.Warning; + default: + throw new NotSupportedException(); + } + } + + public ITest Test => _test ?? (_test = new TestWrapper(_result.Test)); + + public IEnumerable Children => _result.Children?.Select(r => new TestResultWrapper(r)); + + public TimeSpan Duration => TimeSpan.FromSeconds(_result.Duration); + + public DateTime EndTime => _result.EndTime; + + public bool HasChildren => _result.HasChildren; + + public string Message => _result.Message; + + public string Name => _result.Name; + + public string Output => _result.Output; + + public DateTime StartTime => _result.StartTime; + + public object NativeObject => _result; + + public IEnumerable Assersions + { + get + { + if (_result.AssertionResults.Count == 0) + return new[] { + new TestAssertion { + Message = _result.Message, + Status = (TestStatus)_result.ResultState.Status, + StackTrace = _result.StackTrace + } + }; + else + return _result.AssertionResults.Select(r => new TestAssertionWrapper(r)); + } + } + } +} diff --git a/src/Eto.UnitTest.NUnit/TestWrapper.cs b/src/Eto.UnitTest.NUnit/TestWrapper.cs new file mode 100644 index 0000000..37e6646 --- /dev/null +++ b/src/Eto.UnitTest.NUnit/TestWrapper.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using nui = NUnit.Framework.Interfaces; +using nuint = NUnit.Framework.Internal; + +namespace Eto.UnitTest.NUnit +{ + [Serializable] + class TestWrapper : ITest + { + readonly nui.ITest _test; + + public TestWrapper(nui.ITest test) + { + _test = test; + } + + protected TestWrapper(SerializationInfo info, StreamingContext context) + { + } + + public bool IsAssembly => _test is nuint.TestAssembly; + + public bool IsSuite => _test.IsSuite; + + public bool IsParameterized => _test is nuint.ParameterizedMethodSuite; + + public string Name => _test.Name; + + public ITest Parent => _test.Parent != null ? new TestWrapper(_test.Parent) : null; + + public bool HasChildren => _test.HasChildren; + + public IEnumerable Tests => _test.Tests.Select(r => new TestWrapper(r)); + + public IEnumerable Categories + { + get + { + var categories = _test.Properties["Category"]; + if (categories == null || categories.Count == 0) + return Enumerable.Empty(); + return categories.OfType(); + } + } + + public string FullName => _test.FullName; + + public object NativeObject => _test; + + public Type Type => _test.TypeInfo.Type; + + public Assembly Assembly => (_test is nuint.TestAssembly testAssembly) ? testAssembly.Assembly : _test.TypeInfo?.Assembly; + + public override int GetHashCode() => _test.GetHashCode(); + + public override bool Equals(object obj) + { + return obj is TestWrapper wrapper && wrapper._test.Equals(_test); + } + } +} diff --git a/src/Eto.UnitTest.Xunit/Eto.UnitTest.Xunit.csproj b/src/Eto.UnitTest.Xunit/Eto.UnitTest.Xunit.csproj new file mode 100644 index 0000000..745b9b4 --- /dev/null +++ b/src/Eto.UnitTest.Xunit/Eto.UnitTest.Xunit.csproj @@ -0,0 +1,24 @@ + + + + + + net461;netcoreapp2.0;netstandard2.0 + $(DefineConstants);INCLUDE_TESTS + True + + + + Xunit testing UI and utilities for Eto.Forms + Provides a control to use in Eto.Forms applications that can be used to display, filter, and run unit tests. + xunit;unit test;test;testing;tdd + + + + + + + + + + diff --git a/src/Eto.UnitTest.Xunit/MyTests.cs b/src/Eto.UnitTest.Xunit/MyTests.cs new file mode 100644 index 0000000..db9ec0c --- /dev/null +++ b/src/Eto.UnitTest.Xunit/MyTests.cs @@ -0,0 +1,119 @@ +using System; +using Xunit; +using System.Threading; + +#if INCLUDE_TESTS + +// Example tests for.. testing puproses + +namespace Eto.UnitTest.Xunit.TheTests +{ + public class SomeTests + { + [Trait("Category", "CategoryA")] + [Fact] + public void Blar() + { + Thread.Sleep(1000); + } + + [Trait("Category", "CategoryB")] + [Fact] + public void ShouldNotPass() + { + Thread.Sleep(1000); + throw new InvalidOperationException("woo"); + } + } + + public class TestStatus + { + [Fact] + public void TestShouldError() + { + Thread.Sleep(200); + throw new InvalidOperationException("this is a non-test exception"); + } + + [Fact] + public void TestShouldFail() + { + Thread.Sleep(200); + Assert.Equal(10, 20); + } + + [Fact] + public void TestShouldPass() + { + Thread.Sleep(200); + Assert.Equal(10, 10); + } + + [Fact(Skip = "Skip reason")] + public void TestShouldSkip() + { + Thread.Sleep(200); + throw new InvalidOperationException("boo"); + } + + [Theory] + [InlineData(3)] + [InlineData(5)] + [InlineData(6, Skip = "Not working")] + [InlineData(8)] + public void TestTheory(int value) + { + Thread.Sleep(200); + Assert.True(value % 2 == 1); + } + } +} + +namespace Eto.UnitTest.Xunit.OtherTests +{ + public class SomeOtherTests + { + [Trait("Category", "CategoryA")] + [Fact] + public void Test1() + { + Thread.Sleep(1000); + } + + [Fact] + public void Test2() + { + Thread.Sleep(1000); + throw new InvalidOperationException("woo"); + } + } + + public class MoreTests + { + [Fact] + public void Test1() + { + Thread.Sleep(1000); + } + + [Fact] + public void Test2() + { + Thread.Sleep(1000); + } + + [Fact] + public void Test3() + { + Thread.Sleep(1000); + } + + [Fact] + public void Test4() + { + Thread.Sleep(1000); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Eto.UnitTest.Xunit/TestCaseWrapper.cs b/src/Eto.UnitTest.Xunit/TestCaseWrapper.cs new file mode 100644 index 0000000..660f7aa --- /dev/null +++ b/src/Eto.UnitTest.Xunit/TestCaseWrapper.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using xua = Xunit.Abstractions; +using System.Linq; +using System.Reflection; + +namespace Eto.UnitTest.Xunit +{ + class TestCaseWrapper : ITest + { + readonly xua.ITestCase _test; + Type _type; + + public TestCaseWrapper(xua.ITestCase test) + { + _test = test; + } + + public List Children { get; set; } + + public bool IsSuite => false; + + public bool IsAssembly => false; + + public bool IsParameterized => false; + + public string Name => _test.TestMethod.Method.Name; + + public ITest Parent { get; set; } + + public bool HasChildren => Children?.Count > 0; + + public IEnumerable Tests => Children ?? Enumerable.Empty(); + + public IEnumerable Categories => new string[0]; + + public Type Type => _type ?? (_type = _test.TestMethod.TestClass.Class.ToRuntimeType()); + + public Assembly Assembly => Type.Assembly; + + public string FullName => _test.DisplayName;// _fullName ?? (_fullName = Parent != null ? $"{Parent.FullName}.{Name}" : Name); + + public object NativeObject => _test; + + public override int GetHashCode() => _test.UniqueID.GetHashCode(); + + public override bool Equals(object obj) + { + return obj is TestCaseWrapper wrapper && wrapper._test.UniqueID.Equals(_test.UniqueID); + } + } +} diff --git a/src/Eto.UnitTest.Xunit/TestCollectionWrapper.cs b/src/Eto.UnitTest.Xunit/TestCollectionWrapper.cs new file mode 100644 index 0000000..840993b --- /dev/null +++ b/src/Eto.UnitTest.Xunit/TestCollectionWrapper.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Reflection; + +namespace Eto.UnitTest.Xunit +{ + class TestCollectionWrapper : TestSuiteWrapper + { + public override bool IsAssembly => true; + + public TestCollectionWrapper(Assembly assembly) + { + Assembly = assembly; + Name = assembly.GetName().Name; + } + + public TestSuiteWrapper GetSuite(string fullName, bool isClass) + { + var names = fullName.Split('.'); + TestSuiteWrapper suite = this; + var len = isClass ? names.Length : names.Length - 1; + for (int i = 0; i < len; i++) + { + string name = names[i]; + var child = suite.Children.FirstOrDefault(r => r.Name == name) as TestSuiteWrapper; + if (child == null) + { + child = new TestSuiteWrapper { Name = name, Parent = suite, Assembly = Assembly }; + suite.Children.Add(child); + } + suite = child; + } + return suite; + } + } +} diff --git a/src/Eto.UnitTest.Xunit/TestResultWrapper.cs b/src/Eto.UnitTest.Xunit/TestResultWrapper.cs new file mode 100644 index 0000000..78e8777 --- /dev/null +++ b/src/Eto.UnitTest.Xunit/TestResultWrapper.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using xua = Xunit.Abstractions; +using System.Linq; + +namespace Eto.UnitTest.Xunit +{ + class TestAssertionWrapper : ITestAssertion + { + public string Message { get; set; } + + public TestStatus Status { get; set; } + + public string StackTrace { get; set; } + + public object NativeObject => null; + } + + class TestResultWrapper : ITestResult + { + List _testAssertions; + public TestResultWrapper(ITest test, TestStatus status) + { + Test = test; + Status = status; + } + + public TestResultWrapper(xua.ITestAssemblyFinished assemblyFinished) + { + FailCount = assemblyFinished.TestsFailed; + SkipCount = assemblyFinished.TestsSkipped; + PassCount = assemblyFinished.TestsRun - FailCount - SkipCount; + Status = FailCount > 0 ? TestStatus.Failed : SkipCount > 0 ? TestStatus.Skipped : TestStatus.Passed; + Duration = TimeSpan.FromSeconds((double)assemblyFinished.ExecutionTime); + } + + public TestResultWrapper(xua.ITestClassFinished classFinished, ITest test) + { + Test = test; + FailCount = classFinished.TestsFailed; + SkipCount = classFinished.TestsSkipped; + PassCount = classFinished.TestsRun - FailCount - SkipCount; + Status = FailCount > 0 ? TestStatus.Failed : SkipCount > 0 ? TestStatus.Skipped : TestStatus.Passed; + Duration = TimeSpan.FromSeconds((double)classFinished.ExecutionTime); + NativeObject = classFinished; + } + + public TestResultWrapper(xua.ITestCollectionFinished collectionFinished, ITest test) + { + Test = test; + FailCount = collectionFinished.TestsFailed; + SkipCount = collectionFinished.TestsSkipped; + PassCount = collectionFinished.TestsRun - FailCount - SkipCount; + Status = FailCount > 0 ? TestStatus.Failed : SkipCount > 0 ? TestStatus.Skipped : TestStatus.Passed; + Duration = TimeSpan.FromSeconds((double)collectionFinished.ExecutionTime); + NativeObject = collectionFinished; + } + public TestResultWrapper(xua.ITestFailed failed, ITest test) + { + Test = test; + FailCount = 1; + Set(failed); + Status = SetFailureInfo(failed); + //if (Status == TestStatus.Error) + // ErrorCount = 1; + } + public TestResultWrapper(xua.ITestSkipped skipped, ITest test) + { + Test = test; + SkipCount = 1; + Status = TestStatus.Skipped; + Set(skipped); + } + + public TestResultWrapper(xua.ITestPassed passed, ITest test) + { + Test = test; + PassCount = 1; + Status = TestStatus.Passed; + Set(passed); + } + + void Set(xua.ITestResultMessage message) + { + NativeObject = message; + Duration = TimeSpan.FromSeconds((double)message.ExecutionTime); + Output = message.Output; + } + + TestStatus SetFailureInfo(xua.IFailureInformation failure) + { + var status = TestStatus.Failed; + _testAssertions = new List(); + for (int i = 0; i < failure.Messages.Length; i++) + { + var assertion = new TestAssertionWrapper(); + var exceptionType = failure.ExceptionTypes[i]; + TestStatus assertionStatus = TestStatus.Failed; + /* + if (!exceptionType.StartsWith("Xunit.Sdk", StringComparison.Ordinal)) + assertionStatus = TestStatus.Error; + */ + + status = (TestStatus)Math.Max((int)assertionStatus, (int)status); + assertion.Message = failure.Messages[i]; + assertion.StackTrace = failure.StackTraces[i]; + assertion.Status = assertionStatus; + _testAssertions.Add(assertion); + } + return status; + } + + public int AssertCount { get; set; } + + public int FailCount { get; set; } + + public int ErrorCount { get; set; } + + public int WarningCount { get; set; } + + public int PassCount { get; set; } + + public int InconclusiveCount { get; set; } + + public int SkipCount { get; set; } + + public TestStatus Status { get; set; } + + public ITest Test { get; set; } + + public IEnumerable Assersions => _testAssertions ?? Enumerable.Empty(); + + public IEnumerable Children { get; set; } = Enumerable.Empty(); + + public TimeSpan Duration { get; set; } + + public DateTime EndTime { get; set; } + + public bool HasChildren { get; set; } + + public string Message { get; set; } + + public string Name { get; set; } + + public string Output { get; set; } + + public DateTime StartTime { get; set; } + + public object NativeObject { get; set; } + } +} diff --git a/src/Eto.UnitTest.Xunit/TestSuiteWrapper.cs b/src/Eto.UnitTest.Xunit/TestSuiteWrapper.cs new file mode 100644 index 0000000..2e3a388 --- /dev/null +++ b/src/Eto.UnitTest.Xunit/TestSuiteWrapper.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Eto.UnitTest.Xunit +{ + class TestSuiteWrapper : ITest + { + string _fullName; + public List Children { get; } = new List(); + + public virtual bool IsSuite => true; + + public virtual bool IsAssembly => false; + + public bool IsParameterized { get; set; } + + public string Name { get; set; } + + public ITest Parent { get; set; } + + public bool HasChildren => true; + + public IEnumerable Tests => Children; + + public IEnumerable Categories => Enumerable.Empty(); + + public List RunningTests { get; } = new List(); + + public Type Type => null; + + public Assembly Assembly { get; set; } + + public string FullName => _fullName ?? (_fullName = Parent != null ? $"{Parent.FullName}.{Name}" : Name); + + public object NativeObject => null; + } +} diff --git a/src/Eto.UnitTest.Xunit/XunitTestRunner.cs b/src/Eto.UnitTest.Xunit/XunitTestRunner.cs new file mode 100644 index 0000000..622413e --- /dev/null +++ b/src/Eto.UnitTest.Xunit/XunitTestRunner.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using xua = Xunit.Abstractions; +using xu = Xunit; +using Eto.UnitTest; +using System.Linq; +using System.Reflection; +using Eto.Forms; +using Eto.UnitTest.Runners; + +namespace Eto.UnitTest.Xunit +{ + public class XunitTestRunner : ProgressTestRunner, xu.IMessageSinkWithTypes + { + xu.XunitFrontController controller; + TaskCompletionSource _tcs; + TaskCompletionSource _tcsRun; + TaskCompletionSource _tcsFilter; + TestCollectionWrapper _testWrapper; + ITestSource _source; + Assembly _assembly; + ITestFilter _filter; + bool _cancel; + HashSet _testSuiteStarted; + Dictionary _testSuiteStatus; + + List _testsToRun = new List(); + + public override ITest TestSuite => _testWrapper; + + public override Task> GetCategories(ITestFilter filter) + { + return Task.FromResult(Enumerable.Empty()); + } + + public override Task GetTestCount(ITestFilter filter) + { + _tcsFilter = new TaskCompletionSource(); + _filter = filter; + messageHandler = FilterTestCases; + _testsToRun.Clear(); + var discoveryOptions = xu.TestFrameworkOptions.ForDiscovery(); + controller.Find(true, this, discoveryOptions); + return _tcsFilter.Task; + } + + public override Task Load(ITestSource source) + { + _source = source; + _tcs = new TaskCompletionSource(); + controller = new xu.XunitFrontController(xu.AppDomainSupport.Denied, source.Assembly.Location, diagnosticMessageSink: xu.MessageSinkAdapter.Wrap(this)); + _assembly = source.Assembly; + _testWrapper = new TestCollectionWrapper(_assembly); + + var discoveryOptions = xu.TestFrameworkOptions.ForDiscovery(); + messageHandler = DiscoverMessageHandler; + controller.Find(true, this, discoveryOptions); + return _tcs.Task; + } + + void DiscoverMessageHandler(xua.IMessageSinkMessage message) + { + try + { + if (message is xua.ITestCaseDiscoveryMessage testCaseDiscovery) + { + Wrap(testCaseDiscovery.TestCase, true); + } + else if (message is xua.IDiscoveryCompleteMessage discoveryComplete) + { + _tcs.SetResult(true); + } + else if (message is xua.IErrorMessage errorMessage) + { + Console.WriteLine($"Error ocurred {errorMessage.ToString()}"); + } + else + { + Console.WriteLine($"Unhandled message of type {message.GetType()}"); + } + } + catch (Exception ex) + { + _tcs.SetException(ex); + } + } + + public override void StopTests() + { + _cancel = true; + } + + Action messageHandler; + + protected override async Task RunInternalAsync(ITestFilter filter) + { + _tcsRun = new TaskCompletionSource(); + + var config = new xu.TestAssemblyConfiguration(); + + _cancel = false; + _testsToRun.Clear(); + _testSuiteStarted = new HashSet(); + _testSuiteStatus = new Dictionary(); + messageHandler = FilterTestCases; + _filter = filter; + _tcsFilter = new TaskCompletionSource(); + controller.Find(false, this, xu.TestFrameworkOptions.ForDiscovery(config)); + await _tcsFilter.Task; + + messageHandler = RunMessageHandler; + controller.RunTests(_testsToRun, this, xu.TestFrameworkOptions.ForExecution(config)); + + return await _tcsRun.Task; + } + + private void FilterTestCases(xua.IMessageSinkMessage message) + { + try + { + if (message is xua.ITestCaseDiscoveryMessage testDiscovered) + { + if (_filter?.Pass(Wrap(testDiscovered.TestCase)) != false) + _testsToRun.Add(testDiscovered.TestCase); + } + else if (message is xua.IDiscoveryCompleteMessage discoveryComplete) + { + _tcsFilter.SetResult(_testsToRun.Count); + } + else if (message is xua.IErrorMessage errorMessage) + { + Console.WriteLine($"Error ocurred {errorMessage.ToString()}"); + } + else + { + Console.WriteLine($"Unhandled message of type {message.GetType()}"); + } + } + catch (Exception ex) + { + _tcsFilter?.SetException(ex); + _tcsRun?.SetException(ex); + _tcs?.SetException(ex); + } + } + + + TestCaseWrapper Wrap(xua.ITestCase test, bool addToSuite = false) + { + var suite = _testWrapper.GetSuite(test.DisplayName, false); + var testWrapper = new TestCaseWrapper(test) { Parent = suite }; + if (addToSuite) + suite.Children.Add(testWrapper); + return testWrapper; + } + + TestSuiteWrapper Wrap(xua.ITestClass test) + { + return _testWrapper.GetSuite(test.Class.Name, true); + } + + void SetStatus(TestSuiteWrapper suite, TestStatus status) + { + if (suite == null) + return; + if (_testSuiteStatus.TryGetValue(suite, out var currentStatus)) + { + status = (TestStatus)Math.Max((int)currentStatus, (int)status); + } + _testSuiteStatus[suite] = status; + if (_testSuiteStarted.Contains(suite) && suite.RunningTests.Count == 0) + { + _testSuiteStarted.Remove(suite); + var parent = suite.Parent as TestSuiteWrapper; + if (parent != null) + { + parent.RunningTests.Remove(suite); + } + OnTestFinished(new UnitTestResultEventArgs(new TestResultWrapper(suite, status))); + SetStatus(parent, status); + } + } + + bool SetStarted(TestSuiteWrapper suite) + { + if (suite == null) + return false; + if (!_testSuiteStarted.Contains(suite)) + { + _testSuiteStarted.Add(suite); + var parent = suite.Parent as TestSuiteWrapper; + if (parent != null) + { + SetStarted(parent); + parent.RunningTests.Add(suite); + } + OnTestStarted(new UnitTestTestEventArgs(suite)); + return true; + } + return false; + } + + void RunMessageHandler(xua.IMessageSinkMessage message) + { + try + { + if (message is xua.ITestAssemblyFinished assemblyFinished) + { + _tcsRun.SetResult(new TestResultWrapper(assemblyFinished)); + } + else if (message is xua.ITestClassStarting classStarting) + { + var suite = Wrap(classStarting.TestClass); + SetStarted(suite); + OnTestStarted(new UnitTestTestEventArgs(suite)); + } + else if (message is xua.ITestClassFinished classFinished) + { + var suite = Wrap(classFinished.TestClass); + var result = new TestResultWrapper(classFinished, suite); + SetStatus(suite, result.Status); + OnTestFinished(new UnitTestResultEventArgs(result)); + } + else if (message is xua.ITestStarting testStarting) + { + OnTestStarted(new UnitTestTestEventArgs(Wrap(testStarting.TestCase))); + } + else if (message is xua.ITestFailed testFailed) + { + OnTestFinished(new UnitTestResultEventArgs(new TestResultWrapper(testFailed, Wrap(testFailed.TestCase)))); + } + else if (message is xua.ITestPassed testPassed) + { + OnTestFinished(new UnitTestResultEventArgs(new TestResultWrapper(testPassed, Wrap(testPassed.TestCase)))); + } + else if (message is xua.ITestSkipped testSkipped) + { + OnTestFinished(new UnitTestResultEventArgs(new TestResultWrapper(testSkipped, Wrap(testSkipped.TestCase)))); + } + else if (message is xua.IErrorMessage errorMessage) + { + Console.WriteLine($"Error ocurred {errorMessage.ToString()}"); + } + else + { + Console.WriteLine($"Unhandled message of type {message.GetType()}"); + } + } + catch (Exception ex) + { + _tcsRun.SetException(ex); + } + } + + bool xu.IMessageSinkWithTypes.OnMessageWithTypes(xua.IMessageSinkMessage message, HashSet messageTypes) + { + messageHandler?.Invoke(message); + return !_cancel; + } + } +} diff --git a/src/Eto.UnitTest.Xunit/XunitTestRunnerType.cs b/src/Eto.UnitTest.Xunit/XunitTestRunnerType.cs new file mode 100644 index 0000000..c68792c --- /dev/null +++ b/src/Eto.UnitTest.Xunit/XunitTestRunnerType.cs @@ -0,0 +1,16 @@ +using Eto.UnitTest; + +[assembly: TestRunnerType(typeof(Eto.UnitTest.Xunit.XunitTestRunnerType))] + + +namespace Eto.UnitTest.Xunit +{ + public class XunitTestRunnerType : TestRunnerType + { + public override string Name => "Xunit"; + + protected override string[] RequiredReferences => new[] { "xunit.core" }; + + public static void Register() => TestRunnerType.Add(new XunitTestRunnerType()); + } +} diff --git a/src/Eto.UnitTest/Eto.UnitTest.csproj b/src/Eto.UnitTest/Eto.UnitTest.csproj new file mode 100644 index 0000000..2f336ac --- /dev/null +++ b/src/Eto.UnitTest/Eto.UnitTest.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + + + Unit testing UI and utilities for Eto.Forms + Provides a control to use in Eto.Forms applications that can be used to display, filter, and run unit tests. + unit test;test;testing;tdd + + + + + + + + + + + + + + diff --git a/src/Eto.UnitTest/Filters/AndFilter.cs b/src/Eto.UnitTest/Filters/AndFilter.cs new file mode 100644 index 0000000..8ad3371 --- /dev/null +++ b/src/Eto.UnitTest/Filters/AndFilter.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Eto.UnitTest +{ + class AndFilter : ITestFilter + { + public List Filters { get; } + + public AndFilter() + { + Filters = new List(); + } + + public AndFilter(params ITestFilter[] filters) + { + Filters = filters.ToList(); + } + + public AndFilter(IEnumerable filters) + { + Filters = filters.ToList(); + } + + public bool IsExplicitMatch(ITest test) + { + for (int i = 0; i < Filters.Count; i++) + { + if (!Filters[i].IsExplicitMatch(test)) + return false; + } + return true; + } + + public bool Pass(ITest test) + { + for (int i = 0; i < Filters.Count; i++) + { + if (!Filters[i].Pass(test)) + return false; + } + return true; + } + } +} diff --git a/src/Eto.UnitTest/Filters/BaseFilter.cs b/src/Eto.UnitTest/Filters/BaseFilter.cs new file mode 100644 index 0000000..1a21579 --- /dev/null +++ b/src/Eto.UnitTest/Filters/BaseFilter.cs @@ -0,0 +1,33 @@ +using System.Linq; + +namespace Eto.UnitTest +{ + abstract class BaseFilter : ITestFilter + { + public bool IsExplicitMatch(ITest test) => Matches(test); + + public bool ChildCanMatch { get; set; } = true; + public bool ParentCanMatch { get; set; } = true; + + public bool Pass(ITest test) + { + var matches = Matches(test); + + if (test.IsSuite) + { + if (ChildCanMatch) + matches |= test.GetChildren().Any(Matches); + } + else if (ParentCanMatch) + matches |= test.GetParents().Any(ParentMatch); + + return matches; + } + + protected bool ParentMatch(ITest test) => Matches(test, true); + + protected bool Matches(ITest test) => Matches(test, false); + + protected abstract bool Matches(ITest test, bool parent); + } +} diff --git a/src/Eto.UnitTest/Filters/CategoryFilter.cs b/src/Eto.UnitTest/Filters/CategoryFilter.cs new file mode 100644 index 0000000..cac64ef --- /dev/null +++ b/src/Eto.UnitTest/Filters/CategoryFilter.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Eto.UnitTest +{ + class CategoryFilter : BaseFilter + { + public List Categories { get; } + + public CategoryFilter() + { + Categories = new List(); + } + + public CategoryFilter(IEnumerable categories) + { + Categories = categories.ToList(); + } + + public bool MatchAll { get; set; } + + public bool AllowNone { get; set; } + + protected override bool Matches(ITest test, bool parent) + { + var categories = test.Categories.ToList(); + if (categories == null || categories.Count == 0) + return Categories.Count == 0; + + return MatchAll ? Categories.All(categories.Contains) : Categories.Any(categories.Contains); + } + } +} diff --git a/src/Eto.UnitTest/Filters/EmptyFilter.cs b/src/Eto.UnitTest/Filters/EmptyFilter.cs new file mode 100644 index 0000000..01615ee --- /dev/null +++ b/src/Eto.UnitTest/Filters/EmptyFilter.cs @@ -0,0 +1,9 @@ +namespace Eto.UnitTest +{ + class EmptyFilter : ITestFilter + { + public bool IsExplicitMatch(ITest test) => true; + + public bool Pass(ITest test) => true; + } +} diff --git a/src/Eto.UnitTest/Filters/KeywordFilter.cs b/src/Eto.UnitTest/Filters/KeywordFilter.cs new file mode 100644 index 0000000..7b4e9ce --- /dev/null +++ b/src/Eto.UnitTest/Filters/KeywordFilter.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Eto.UnitTest +{ + class KeywordFilter : BaseFilter + { + string keywords; + string[][] keywordTokens; + + string[] SplitMatches(string value, string regex) => Regex.Matches(value, regex).OfType().Select(r => r.Value).ToArray(); + + /// + /// Gets or sets the keyword string to search for. + /// + /// + /// Supports: + /// - '-' prefix to exclude keyword + /// - Quotes for literal matches e.g. "my test" + /// - Multiple keywords separated by whitespace + /// + /// The keywords. + public string Keywords + { + get => keywords; + set + { + keywords = value; + if (string.IsNullOrWhiteSpace(value)) + keywordTokens = null; + else + { + var searches = SplitMatches(value, @"([-]?""[^""]*"")|((?<=[\s]|^)[^""\s]+(?=[\s]|$))"); + keywordTokens = searches + .Select(s => SplitMatches(s, @"(^-)|((?<=^-?"").+(?=""$))|([A-Z][^A-Z]*[^A-Z""]?)|((?= 0; + } + } +} diff --git a/src/Eto.UnitTest/Filters/NotFilter.cs b/src/Eto.UnitTest/Filters/NotFilter.cs new file mode 100644 index 0000000..e13df67 --- /dev/null +++ b/src/Eto.UnitTest/Filters/NotFilter.cs @@ -0,0 +1,20 @@ +namespace Eto.UnitTest +{ + class NotFilter : ITestFilter + { + public ITestFilter Filter { get; set; } + + public NotFilter(ITestFilter filter) + { + Filter = filter; + } + + public NotFilter() + { + } + + public bool IsExplicitMatch(ITest test) => Filter?.IsExplicitMatch(test) != true; + + public bool Pass(ITest test) => Filter?.Pass(test) != true; + } +} diff --git a/src/Eto.UnitTest/Filters/NotRunFilter.cs b/src/Eto.UnitTest/Filters/NotRunFilter.cs new file mode 100644 index 0000000..ace5e85 --- /dev/null +++ b/src/Eto.UnitTest/Filters/NotRunFilter.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; + +namespace Eto.UnitTest +{ + class NotRunFilter : ITestFilter + { + Func LookupResult { get; } + + public NotRunFilter(Func lookupResult) + { + LookupResult = lookupResult; + } + + public override string ToString() => "Not Run"; + + public bool Pass(ITest test) + { + if (test.IsSuite) + return test.GetChildren(false).Any(Pass); + return IsExplicitMatch(test); + } + + public bool IsExplicitMatch(ITest test) => LookupResult(test) == null; + + } +} diff --git a/src/Eto.UnitTest/Filters/OrFilter.cs b/src/Eto.UnitTest/Filters/OrFilter.cs new file mode 100644 index 0000000..163730a --- /dev/null +++ b/src/Eto.UnitTest/Filters/OrFilter.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Eto.UnitTest +{ + class OrFilter : ITestFilter + { + public List Filters { get; } + + public OrFilter() + { + Filters = new List(); + } + + public OrFilter(params ITestFilter[] filters) + { + Filters = filters.ToList(); + } + + public OrFilter(IEnumerable filters) + { + Filters = filters.ToList(); + } + + public bool IsExplicitMatch(ITest test) + { + for (int i = 0; i < Filters.Count; i++) + { + if (Filters[i].IsExplicitMatch(test)) + return true; + } + return false; + } + + public bool Pass(ITest test) + { + for (int i = 0; i < Filters.Count; i++) + { + if (Filters[i].Pass(test)) + return true; + } + return false; + } + + } +} diff --git a/src/Eto.UnitTest/Filters/SingleTestFilter.cs b/src/Eto.UnitTest/Filters/SingleTestFilter.cs new file mode 100644 index 0000000..e06102a --- /dev/null +++ b/src/Eto.UnitTest/Filters/SingleTestFilter.cs @@ -0,0 +1,54 @@ +using System.Reflection; + +namespace Eto.UnitTest +{ + class SingleTestFilter : ITestFilter + { + public ITest Test { get; set; } + + public Assembly Assembly { get; set; } + + public bool IsExplicitMatch(ITest test) + { + if (Assembly != null) + { + if (!test.IsSuite && test.Assembly != Assembly) + return false; + if (test.IsAssembly && test.Assembly != Assembly) + return false; + } + return test.FullName == Test.FullName; + } + + public bool Pass(ITest test) + { + if (Assembly != null) + { + if (!test.IsSuite && test.Assembly != Assembly) + return false; + + if (test.IsAssembly && test.Assembly != Assembly) + return false; + } + + + var parent = Test; + // check if it is a parent of the test + while (parent != null) + { + if (test.FullName == parent.FullName) + return true; + parent = parent.Parent; + } + // execute all children of the test + parent = test; + while (parent != null) + { + if (parent.FullName == Test.FullName) + return true; + parent = parent.Parent; + } + return false; + } + } +} diff --git a/src/Eto.UnitTest/Filters/StatusFilter.cs b/src/Eto.UnitTest/Filters/StatusFilter.cs new file mode 100644 index 0000000..625e4e9 --- /dev/null +++ b/src/Eto.UnitTest/Filters/StatusFilter.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; + +namespace Eto.UnitTest +{ + class StatusFilter : ITestFilter + { + Func _lookupResult; + + public string Name => Status.ToString(); + + public TestStatus Status { get; } + + public StatusFilter(Func lookupResult, TestStatus status) + { + Status = status; + _lookupResult = lookupResult; + } + + public override string ToString() => Name; + + public bool Pass(ITest test) + { + if (test.IsSuite) + return test.GetChildren(false).Any(Pass); + return IsExplicitMatch(test); + } + + public bool IsExplicitMatch(ITest test) => _lookupResult(test)?.Status == Status; + + } +} diff --git a/src/Eto.UnitTest/Filters/TestFilter.cs b/src/Eto.UnitTest/Filters/TestFilter.cs new file mode 100644 index 0000000..7a218d3 --- /dev/null +++ b/src/Eto.UnitTest/Filters/TestFilter.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Collections; + +namespace Eto.UnitTest +{ + + public static class TestFilter + { + public static ITestFilter Empty { get; } = new EmptyFilter(); + } +} diff --git a/src/Eto.UnitTest/INativeObject.cs b/src/Eto.UnitTest/INativeObject.cs new file mode 100644 index 0000000..cf80401 --- /dev/null +++ b/src/Eto.UnitTest/INativeObject.cs @@ -0,0 +1,7 @@ +namespace Eto.UnitTest +{ + public interface INativeObject + { + object NativeObject { get; } + } +} diff --git a/src/Eto.UnitTest/ITest.cs b/src/Eto.UnitTest/ITest.cs new file mode 100644 index 0000000..e875d27 --- /dev/null +++ b/src/Eto.UnitTest/ITest.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace Eto.UnitTest +{ + public interface ITest : INativeObject + { + bool IsSuite { get; } + bool IsAssembly { get; } + bool IsParameterized { get; } + string Name { get; } + ITest Parent { get; } + bool HasChildren { get; } + IEnumerable Tests { get; } + IEnumerable Categories { get; } + Type Type { get; } + Assembly Assembly { get; } + string FullName { get; } + } +} diff --git a/src/Eto.UnitTest/ITestCategory.cs b/src/Eto.UnitTest/ITestCategory.cs new file mode 100644 index 0000000..d3077b5 --- /dev/null +++ b/src/Eto.UnitTest/ITestCategory.cs @@ -0,0 +1,7 @@ +namespace Eto.UnitTest +{ + public interface ITestCategory + { + string Name { get; } + } +} diff --git a/src/Eto.UnitTest/ITestFilter.cs b/src/Eto.UnitTest/ITestFilter.cs new file mode 100644 index 0000000..0943d05 --- /dev/null +++ b/src/Eto.UnitTest/ITestFilter.cs @@ -0,0 +1,8 @@ +namespace Eto.UnitTest +{ + public interface ITestFilter + { + bool IsExplicitMatch(ITest test); + bool Pass(ITest test); + } +} diff --git a/src/Eto.UnitTest/ITestResult.cs b/src/Eto.UnitTest/ITestResult.cs new file mode 100644 index 0000000..2aa69f5 --- /dev/null +++ b/src/Eto.UnitTest/ITestResult.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Eto.UnitTest +{ + public interface ITestAssertion : INativeObject + { + string Message { get; } + TestStatus Status { get; } + string StackTrace { get; } + } + + public interface ITestResult : INativeObject + { + int AssertCount { get; } + //int ErrorCount { get; } + int FailCount { get; } + int WarningCount { get; } + int PassCount { get; } + int InconclusiveCount { get; } + int SkipCount { get; } + TestStatus Status { get; } + ITest Test { get; } + IEnumerable Assersions { get; } + IEnumerable Children { get; } + TimeSpan Duration { get; } + DateTime EndTime { get; } + bool HasChildren { get; } + string Message { get; } + string Name { get; } + string Output { get; } + DateTime StartTime { get; } + } +} diff --git a/src/Eto.UnitTest/ITestRunner.cs b/src/Eto.UnitTest/ITestRunner.cs new file mode 100644 index 0000000..9e86ba8 --- /dev/null +++ b/src/Eto.UnitTest/ITestRunner.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Eto.UnitTest +{ + public interface ITestRunner + { + bool IsRunning { get; } + ITest TestSuite { get; } + + event EventHandler Log; + event EventHandler Progress; + event EventHandler TestFinished; + event EventHandler TestStarted; + event EventHandler IsRunningChanged; + + void StopTests(); + + Task Load(ITestSource source); + + Task GetTestCount(ITestFilter filter); + Task> GetCategories(ITestFilter filter); + Task RunAsync(ITestFilter filter); + } +} diff --git a/src/Eto.UnitTest/ITestRunnerType.cs b/src/Eto.UnitTest/ITestRunnerType.cs new file mode 100644 index 0000000..e6a4934 --- /dev/null +++ b/src/Eto.UnitTest/ITestRunnerType.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Eto.UnitTest +{ + [AttributeUsage(AttributeTargets.Assembly)] + public class TestRunnerTypeAttribute : Attribute + { + ITestRunnerType _runnerType; + public Type Type { get; } + + public ITestRunnerType RunnerType => _runnerType ?? (_runnerType = (ITestRunnerType)Activator.CreateInstance(Type)); + + public TestRunnerTypeAttribute(Type type) + { + Type = type; + } + } + + public abstract class TestRunnerType : ITestRunnerType + where T : ITestRunner, new() + { + public virtual string Name { get; } + + static bool HasReference(Assembly assembly, string referenceName) + { + foreach (var reference in assembly.GetReferencedAssemblies()) + { + if (string.Equals(reference.Name, referenceName, StringComparison.Ordinal)) + return true; + } + return false; + } + + protected virtual string[] RequiredReferences => new string[0]; + + public virtual bool CanExecute(ITestSource source) + { + var assembly = source.GetReflectionAssembly(); + foreach (var reference in RequiredReferences) + { + if (!HasReference(assembly, reference)) + return false; + } + return true; + } + + public ITestRunner CreateRunner() => new T(); + } + + + public static class TestRunnerType + { + static List types; + + public static void AddAssembly(AssemblyName assemblyName) + { + AddAssembly(Assembly.Load(assemblyName)); + } + + public static void Add(ITestRunnerType type) + { + if (types == null) + types = new List(); + types.Add(type); + } + + public static void AddAssembly(Assembly assembly) + { + if (types == null) + types = new List(); + types.AddRange(assembly.GetCustomAttributes().Select(r => r.RunnerType)); + } + + public static ITestRunnerType Find(ITestSource source) + { + if (types == null) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + AddAssembly(assembly); + } + } + + foreach (var type in types) + { + if (type.CanExecute(source)) + { + return type; + } + } + return null; + } + } + + public interface ITestRunnerType + { + string Name { get; } + bool CanExecute(ITestSource source); + ITestRunner CreateRunner(); + } +} diff --git a/src/Eto.UnitTest/MultipleTest.cs b/src/Eto.UnitTest/MultipleTest.cs new file mode 100644 index 0000000..470e5ca --- /dev/null +++ b/src/Eto.UnitTest/MultipleTest.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Eto.UnitTest +{ + class MultipleTest : ITest + { + List _tests; + public MultipleTest(IEnumerable tests) + { + _tests = tests.ToList(); + } + + public bool IsSuite => true; + + public bool IsAssembly => false; + + public bool IsParameterized => false; + + public string Name => null; + + public ITest Parent => null; + + public bool HasChildren => true; + + public IEnumerable Tests => _tests; + + public IEnumerable Categories => _tests.SelectMany(r => r.Categories).Distinct(); + + public Type Type => null; + + public Assembly Assembly => null; + + public string FullName => null; + + public object NativeObject => null; + } +} diff --git a/src/Eto.UnitTest/MultipleTestResult.cs b/src/Eto.UnitTest/MultipleTestResult.cs new file mode 100644 index 0000000..32fbefc --- /dev/null +++ b/src/Eto.UnitTest/MultipleTestResult.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Eto.UnitTest +{ + public class MultipleTestResult : ITestResult + { + public List Results { get; private set; } + + public MultipleTestResult(ITest test) + { + Results = new List(); + Test = test; + } + + public int AssertCount => Results.Sum(r => r.AssertCount); + + public IEnumerable Children => Results.SelectMany(r => r.Children); + + public TimeSpan Duration => TimeSpan.FromTicks(Results.Sum(r => r.Duration.Ticks)); + + public DateTime EndTime => Results.Max(r => r.EndTime); + + public int FailCount => Results.Sum(r => r.FailCount); + + //public int ErrorCount => Results.Sum(r => r.ErrorCount); + + public string FullName => string.Empty; + + public bool HasChildren => Results.Any(r => r.HasChildren); + + public int InconclusiveCount => Results.Sum(r => r.InconclusiveCount); + + public string Message => string.Join("\n", Results.Select(r => r.Message)); + + public string Name => string.Join(", ", Results.Select(r => r.Name)); + + public string Output => string.Join("\n", Results.Select(r => r.Output)); + + public int PassCount => Results.Sum(r => r.PassCount); + + public int SkipCount => Results.Sum(r => r.SkipCount); + + public DateTime StartTime => Results.Min(r => r.StartTime); + + public ITest Test { get; } + + public int WarningCount => Results.Sum(r => r.WarningCount); + + public TestStatus Status => Results.Max(r => r.Status); + + public object NativeObject => null; + + public IEnumerable Assersions => Results.SelectMany(r => r.Assersions); + } +} diff --git a/src/Eto.UnitTest/Runners/BaseTestRunner.cs b/src/Eto.UnitTest/Runners/BaseTestRunner.cs new file mode 100644 index 0000000..99436f9 --- /dev/null +++ b/src/Eto.UnitTest/Runners/BaseTestRunner.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Eto.UnitTest.Runners +{ + public abstract class BaseTestRunner : ITestRunner, IDisposable + { + public abstract bool IsRunning { get; } + + public abstract ITest TestSuite { get; } + + public event EventHandler Log; + public event EventHandler Progress; + public event EventHandler TestFinished; + public event EventHandler TestStarted; + public event EventHandler IsRunningChanged; + + protected virtual void OnLog(UnitTestLogEventArgs e) => Log?.Invoke(this, e); + protected virtual void OnProgress(UnitTestProgressEventArgs e) => Progress?.Invoke(this, e); + protected virtual void OnTestFinished(UnitTestResultEventArgs e) => TestFinished?.Invoke(this, e); + protected virtual void OnTestStarted(UnitTestTestEventArgs e) => TestStarted?.Invoke(this, e); + protected virtual void OnIsRunningChanged(EventArgs e) => IsRunningChanged?.Invoke(this, e); + + public abstract Task> GetCategories(ITestFilter filter); + public abstract Task GetTestCount(ITestFilter filter); + public abstract Task Load(ITestSource source); + public abstract Task RunAsync(ITestFilter filter); + public abstract void StopTests(); + + protected void WriteLog(string message) => OnLog(new UnitTestLogEventArgs(message)); + + public void Dispose() => Dispose(true); + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/Eto.UnitTest/Runners/LoggingTestRunner.cs b/src/Eto.UnitTest/Runners/LoggingTestRunner.cs new file mode 100644 index 0000000..c1278ab --- /dev/null +++ b/src/Eto.UnitTest/Runners/LoggingTestRunner.cs @@ -0,0 +1,135 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace Eto.UnitTest.Runners +{ + public class LoggingTestRunner : WrappedTestRunner + { + string lastTestStarted; + object lastTestLock = new object(); + public bool ShowOutput { get; set; } + public bool ShowOnlyFailed { get; set; } = true; + public bool ShowTotalAtStart { get; set; } + + + public LoggingTestRunner(ITestRunner inner) + : base(inner) + { + } + + protected override void OnTestStarted(UnitTestTestEventArgs e) + { + base.OnTestStarted(e); + var test = e.Test; + + if (!ShowOnlyFailed && !test.IsSuite) + { + lock (lastTestLock) + { + lastTestStarted = test.FullName; + WriteLog(test.FullName); + } + } + } + + void WriteTest(ITest test) + { + var currentTest = test.FullName; + if (lastTestStarted != currentTest) + { + WriteLog(currentTest); + lastTestStarted = currentTest; + } + } + + protected override void OnTestFinished(UnitTestResultEventArgs e) + { + base.OnTestFinished(e); + var result = e.Result; + + if (!result.Test.IsSuite) + { + lock (lastTestLock) + { + + if (ShowOutput && !string.IsNullOrEmpty(result.Output)) + { + WriteTest(result.Test); + WriteLog(result.Output); + } + + if (result.Status != TestStatus.Passed && result.Status != TestStatus.Skipped) + { + WriteTest(result.Test); + var assertions = result.Assersions; + if (assertions != null) + { + foreach (var assertion in assertions) + { + if (assertion.Status == TestStatus.Passed) + continue; + if (!string.IsNullOrEmpty(assertion.StackTrace)) + WriteLog($"{assertion.Status}: {assertion.Message}\n{assertion.StackTrace}"); + else + WriteLog($"{assertion.Status}: {assertion.Message}"); + } + } + else + { + WriteLog($"{result.Status}: {result.Message}"); + } + } + } + } + } + + public override async Task RunAsync(ITestFilter filter) + { + WriteLog("Starting tests..."); + + if (ShowTotalAtStart) + { + var testCount = await GetTestCount(filter); + + WriteLog($"Total test count: {testCount}"); + } + + ITestResult result; + try + { + result = await base.RunAsync(filter); + } + catch (Exception ex) + { + WriteLog($"Error running tests: {ex}"); + return null; + } + + WriteLog(result.FailCount > 0 ? "FAILED" : "PASSED"); + var sb = new StringBuilder(); + void AppendText(string str) + { + if (sb.Length > 0) + sb.Append(", "); + sb.Append(str); + } + if (result.PassCount > 0) + AppendText($"Passed: {result.PassCount}"); + if (result.FailCount > 0) + AppendText($"Failed: {result.FailCount}"); + if (result.SkipCount > 0) + AppendText($"Skipped: {result.FailCount}"); + //if (result.ErrorCount > 0) + // AppendText($"Errors: {result.ErrorCount}"); + if (result.WarningCount > 0) + AppendText($"Warnings: {result.WarningCount}"); + if (result.InconclusiveCount > 0) + AppendText($"Inconclusive: {result.InconclusiveCount}"); + + WriteLog($"\t{sb}"); + WriteLog($"\tDuration: {result.Duration}"); + return result; + } + } +} diff --git a/src/Eto.UnitTest/Runners/MultipleTestRunner.cs b/src/Eto.UnitTest/Runners/MultipleTestRunner.cs new file mode 100644 index 0000000..83357fa --- /dev/null +++ b/src/Eto.UnitTest/Runners/MultipleTestRunner.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.UnitTest.Runners +{ + public class MultipleTestRunner : ProgressTestRunner + { + ITestRunner _currentRunner; + Queue _runnersToTest; + ITestFilter _testFilter; + + List Runners { get; } = new List(); + + public override async Task> GetCategories(ITestFilter filter) + { + var categoryList = await Task.WhenAll(Runners.Select(r => r.GetCategories(filter))); + return categoryList.SelectMany(r => r).Distinct(); + } + + public override async Task GetTestCount(ITestFilter filter) + { + var countList = await Task.WhenAll(Runners.Select(r => r.GetTestCount(filter))); + return countList.Sum(); + } + MultipleTest _testSuite; + + public override ITest TestSuite => _testSuite; + + public override void StopTests() + { + lock (this) + { + if (IsRunning) + { + WriteLog("Stopping tests..."); + _runnersToTest?.Clear(); + _currentRunner?.StopTests(); + } + } + } + + public override async Task Load(ITestSource source) + { + var runnerType = TestRunnerType.Find(source); + if (runnerType != null) + { + var runner = runnerType.CreateRunner(); + await runner.Load(source); + Runners.Add(runner); + } + _testSuite = new MultipleTest(Runners.Select(r => r.TestSuite)); + } + + public void Add(ITestRunner runner) + { + Runners.Add(runner); + _testSuite = new MultipleTest(Runners.Select(r => r.TestSuite)); + } + + public void Clear() + { + Runners.Clear(); + _testSuite = null; + } + + protected override async Task RunInternalAsync(ITestFilter filter) + { + var results = new MultipleTestResult(_testSuite); + _testFilter = filter; + _runnersToTest = new Queue(Runners.Where(r => _testFilter.Pass((r.TestSuite)))); + + OnTestStarted(new UnitTestTestEventArgs(_testSuite)); + try + { + while (_runnersToTest.Count > 0) + { + lock (this) + { + _currentRunner = _runnersToTest.Dequeue(); + _currentRunner.Log += currentRunner_Log; + _currentRunner.Progress += currentRunner_Progress; + _currentRunner.TestStarted += currentRunner_TestStarted; + _currentRunner.TestFinished += currentRunner_TestFinished; + } + var result = await _currentRunner.RunAsync(filter); + results.Results.Add(result); + + _currentRunner.Log -= currentRunner_Log; + _currentRunner.Progress -= currentRunner_Progress; + _currentRunner.TestStarted -= currentRunner_TestStarted; + _currentRunner.TestFinished -= currentRunner_TestFinished; + } + OnTestFinished(new UnitTestResultEventArgs(results)); + + } + finally + { + _currentRunner = null; + } + return results; + + } + + private void currentRunner_TestFinished(object sender, UnitTestResultEventArgs e) + { + OnTestFinished(e); + } + + private void currentRunner_TestStarted(object sender, UnitTestTestEventArgs e) + { + OnTestStarted(e); + } + + private void currentRunner_Progress(object sender, UnitTestProgressEventArgs e) + { + OnProgress(e); + } + + private void currentRunner_Log(object sender, UnitTestLogEventArgs e) + { + //OnLog(e); + } + } +} diff --git a/src/Eto.UnitTest/Runners/ProgressTestRunner.cs b/src/Eto.UnitTest/Runners/ProgressTestRunner.cs new file mode 100644 index 0000000..6a65b2f --- /dev/null +++ b/src/Eto.UnitTest/Runners/ProgressTestRunner.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +namespace Eto.UnitTest.Runners +{ + public abstract class ProgressTestRunner : BaseTestRunner, IDisposable + { + UnitTestProgressEventArgs _progressArgs; + + bool _isRunning; + public override bool IsRunning => _isRunning; + + void SetIsRunning(bool value) + { + if (_isRunning != value) + { + _isRunning = value; + OnIsRunningChanged(EventArgs.Empty); + } + } + + protected override void OnTestFinished(UnitTestResultEventArgs e) + { + base.OnTestFinished(e); + var result = e.Result; + + if (!result.Test.IsSuite) + { + _progressArgs.AddResult(result); + OnProgress(_progressArgs); + } + } + + public override sealed async Task RunAsync(ITestFilter filter) + { + SetIsRunning(true); + try + { + var testCount = await GetTestCount(filter); + _progressArgs = new UnitTestProgressEventArgs(testCount); + return await RunInternalAsync(filter); + } + finally + { + SetIsRunning(false); + } + } + + protected abstract Task RunInternalAsync(ITestFilter filter); + } +} diff --git a/src/Eto.UnitTest/Runners/WrappedTestRunner.cs b/src/Eto.UnitTest/Runners/WrappedTestRunner.cs new file mode 100644 index 0000000..468122b --- /dev/null +++ b/src/Eto.UnitTest/Runners/WrappedTestRunner.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Eto.UnitTest.Runners +{ + public class WrappedTestRunner : BaseTestRunner + { + ITestRunner _inner; + public override bool IsRunning => _inner.IsRunning; + + public override ITest TestSuite => _inner.TestSuite; + + public WrappedTestRunner(ITestRunner inner) + { + _inner = inner; + _inner.Log += _inner_Log; + _inner.Progress += _inner_Progress; + _inner.IsRunningChanged += _inner_IsRunningChanged; + _inner.TestFinished += _inner_TestFinished; + _inner.TestStarted += _inner_TestStarted; + } + + private void _inner_TestStarted(object sender, UnitTestTestEventArgs e) => OnTestStarted(e); + private void _inner_TestFinished(object sender, UnitTestResultEventArgs e) => OnTestFinished(e); + private void _inner_IsRunningChanged(object sender, EventArgs e) => OnIsRunningChanged(e); + private void _inner_Progress(object sender, UnitTestProgressEventArgs e) => OnProgress(e); + private void _inner_Log(object sender, UnitTestLogEventArgs e) => OnLog(e); + + public override Task> GetCategories(ITestFilter filter) => _inner.GetCategories(filter); + public override Task GetTestCount(ITestFilter filter) => _inner.GetTestCount(filter); + public override Task Load(ITestSource source) => _inner.Load(source); + public override Task RunAsync(ITestFilter filter) => _inner.RunAsync(filter); + public override void StopTests() => _inner.StopTests(); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing && _inner is IDisposable disposable) + disposable.Dispose(); + } + } +} diff --git a/src/Eto.UnitTest/TestHelpers.cs b/src/Eto.UnitTest/TestHelpers.cs new file mode 100644 index 0000000..a67333f --- /dev/null +++ b/src/Eto.UnitTest/TestHelpers.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Eto.UnitTest +{ + public static class TestHelpers + { + public static IEnumerable GetChildren(this ITest test) => GetChildren(test, true); + + public static IEnumerable GetChildren(this ITest test, bool recursive) + { + if (test.HasChildren) + { + foreach (var child in test.Tests) + { + yield return child; + if (recursive) + { + foreach (var childTest in GetChildren(child, recursive)) + yield return childTest; + } + } + } + } + + public static IEnumerable GetParents(this ITest test) + { + while (test != null) + { + yield return test; + test = test.Parent; + } + } + } +} diff --git a/src/Eto.UnitTest/TestSource.cs b/src/Eto.UnitTest/TestSource.cs new file mode 100644 index 0000000..5bbc1ce --- /dev/null +++ b/src/Eto.UnitTest/TestSource.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Eto.UnitTest +{ + public interface ITestSource + { + Assembly Assembly { get; } + Assembly GetReflectionAssembly(); + } + + [Serializable] + public class TestSource : ITestSource + { + [NonSerialized] + Assembly _reflectionAssembly; + Assembly _assembly; + + public string Path { get; } + public Assembly Assembly => _assembly ?? Assembly.LoadFrom(Path); + public TestSource(string path) + { + Path = path; + } + + public TestSource(Assembly assembly) + { + _assembly = assembly; + Path = assembly.Location; + } + + public Assembly GetReflectionAssembly() => _assembly ?? _reflectionAssembly ?? (_reflectionAssembly = Assembly.ReflectionOnlyLoad(File.ReadAllBytes(Path))); + + public static implicit operator TestSource(Assembly assembly) => new TestSource(assembly); + + public static implicit operator TestSource(string assemblyName) => new TestSource(assemblyName); + } +} diff --git a/src/Eto.UnitTest/TestStatus.cs b/src/Eto.UnitTest/TestStatus.cs new file mode 100644 index 0000000..ed4b119 --- /dev/null +++ b/src/Eto.UnitTest/TestStatus.cs @@ -0,0 +1,12 @@ +namespace Eto.UnitTest +{ + public enum TestStatus + { + Inconclusive = 0, + Skipped = 1, + Passed = 2, + Warning = 3, + Failed = 4, + //Error = 5 + } +} diff --git a/src/Eto.UnitTest/UI/AsyncQueue.cs b/src/Eto.UnitTest/UI/AsyncQueue.cs new file mode 100644 index 0000000..f215991 --- /dev/null +++ b/src/Eto.UnitTest/UI/AsyncQueue.cs @@ -0,0 +1,88 @@ +using System; +using Eto.Forms; +using System.Collections.Generic; + +namespace Eto.UnitTest.UI +{ + class AsyncQueue + { + List actions = new List(); + Dictionary namedActions = new Dictionary(); + UITimer timer; + double delay = 0.2; + bool isQueued; + + public double Delay + { + get => delay; + set + { + delay = value; + if (timer != null) + timer.Interval = delay; + } + } + + public void Add(string name, Action action) + { + lock (this) + { + namedActions[name] = action; + Start(); + } + } + + public void Add(Action action) + { + lock (this) + { + actions.Add(action); + Start(); + } + } + + void Start() + { + if (!isQueued) + { + isQueued = true; + /** + Application.Instance.AsyncInvoke(FlushQueue); + /**/ + Application.Instance.AsyncInvoke(StartTimer); + /**/ + } + } + + void StartTimer() + { + if (timer == null) + { + timer = new UITimer { Interval = delay }; + timer.Elapsed += Timer_Elapsed; + } + timer.Start(); + } + + void Timer_Elapsed(object sender, EventArgs e) => FlushQueue(); + + void FlushQueue() + { + List actionList; + lock (this) + { + actionList = actions; + actionList.AddRange(namedActions.Values); + namedActions.Clear(); + actions = new List(); + isQueued = false; + timer?.Stop(); + } + + foreach (var action in actionList) + { + action(); + } + } + } +} diff --git a/src/Eto.UnitTest/UI/BindingExtensions.cs b/src/Eto.UnitTest/UI/BindingExtensions.cs new file mode 100644 index 0000000..e35f916 --- /dev/null +++ b/src/Eto.UnitTest/UI/BindingExtensions.cs @@ -0,0 +1,17 @@ +using Eto.Forms; + +namespace Eto.UnitTest.UI +{ + static class BindingExtensions + { + public static IndirectBinding Invoke(this IndirectBinding binding) + { + return new DelegateBinding( + m => Application.Instance.Invoke(() => binding.GetValue(m)), + (m, val) => Application.Instance.Invoke(() => binding.SetValue(m, val)), + addChangeEvent: (m, ev) => binding.AddValueChangedHandler(m, ev), + removeChangeEvent: binding.RemoveValueChangedHandler + ); + } + } +} diff --git a/src/Eto.UnitTest/UI/BindingHelpers.cs b/src/Eto.UnitTest/UI/BindingHelpers.cs new file mode 100644 index 0000000..4c0648e --- /dev/null +++ b/src/Eto.UnitTest/UI/BindingHelpers.cs @@ -0,0 +1,32 @@ +using Eto.Forms; +using System.Collections.Generic; +using System.Linq; + +namespace Eto.UnitTest.UI +{ + static class BindingHelpers + { + internal interface ICastItems + where T : IBindable + { + BindableBinding> To(); + } + + class CastItemsHelper : ICastItems + where T : IBindable + { + public BindableBinding> Binding { get; set; } + + public BindableBinding> To() + { + return Binding.Convert(source => source.Cast(), list => list.Cast()); + } + } + + internal static ICastItems CastItems(this BindableBinding> binding) + where T : Eto.Forms.IBindable + { + return new CastItemsHelper { Binding = binding }; + } + } +} diff --git a/src/Eto.UnitTest/UI/UnitTestPanel.cs b/src/Eto.UnitTest/UI/UnitTestPanel.cs new file mode 100644 index 0000000..4fd3c30 --- /dev/null +++ b/src/Eto.UnitTest/UI/UnitTestPanel.cs @@ -0,0 +1,695 @@ +using System; +using System.Reflection; +using Eto.Forms; +using System.Collections.Generic; +using System.Threading.Tasks; +using Eto.Drawing; +using System.Linq; +using System.IO; +using System.Text; +using System.Collections.Concurrent; +using System.ComponentModel; +using Eto.UnitTest.Runners; + +namespace Eto.UnitTest.UI +{ + public class UnitTestPanel : Panel, INotifyPropertyChanged + { + ITestRunner _runner; + TreeGridView _tree; + Button _startButton; + Button _stopButton; + Control _filterControls; + SearchBox _search; + TextArea _log; + UnitTestProgressBar _progress; + Label _testCountLabel; + UITimer _timer; + Panel _customFilterControls; + ITestFilter _customFilter; + Dictionary _testMap; + Dictionary _stateImages = new Dictionary(); + Image _notRunStateImage; + Image _runningStateImage; + ConcurrentDictionary _lastResultMap = new ConcurrentDictionary(); + ConcurrentDictionary> _allResultsMap = new ConcurrentDictionary>(); + AsyncQueue _asyncQueue = new AsyncQueue(); + IEnumerable _statusFilters = Enumerable.Empty(); + IEnumerable _includeCategories = Enumerable.Empty(); + IEnumerable _excludeCategories = Enumerable.Empty(); + IList _availableCategories; + + public event EventHandler Log; + public event PropertyChangedEventHandler PropertyChanged; + + public new Control Content + { + get => _customFilterControls.Content; + set => _customFilterControls.Content = value; + } + + public ITestFilter CustomFilter + { + get => _customFilter; + set + { + _customFilter = value; + if (Loaded) + PopulateTree(); + } + } + + /// + /// Gets or sets a value indicating whether to merge nodes with only a single child into its parent. + /// + /// true to merge single nodes; otherwise, false. + public bool MergeSingleNodes { get; set; } = true; + + public ITestRunner Runner + { + get => _runner; + set + { + if (_runner != null) + { + _runner.Log -= Runner_Log; + _runner.Progress -= Runner_Progress; + _runner.TestFinished -= Runner_TestFinished; + _runner.TestStarted -= Runner_TestStarted; + _runner.IsRunningChanged -= Runner_IsRunningChanged; + } + _runner = value; + if (_runner != null) + { + _runner.Log += Runner_Log; + _runner.Progress += Runner_Progress; + _runner.TestFinished += Runner_TestFinished; + _runner.TestStarted += Runner_TestStarted; + _runner.IsRunningChanged += Runner_IsRunningChanged; + base.Content.DataContext = _runner; + } + PopulateTree(); + + } + } + + IEnumerable GetOptionalFilters() + { + foreach (var value in Enum.GetValues(typeof(TestStatus)).Cast()) + { + yield return new StatusFilter(LookupResult, value); + } + + yield return new NotRunFilter(LookupResult); + } + + ITestResult LookupResult(ITest test) + { + if (!_lastResultMap.TryGetValue(test, out var result)) + return null; + return result; + } + + IEnumerable StatusFilters + { + get => _statusFilters; + set + { + _statusFilters = value.ToList(); + PopulateTree(); + } + } + + IEnumerable IncludeCategories + { + get => _includeCategories; + set + { + _includeCategories = value.ToList(); + PopulateTree(); + } + } + + IEnumerable ExcludeCategories + { + get => _excludeCategories; + set + { + _excludeCategories = value.ToList(); + PopulateTree(); + } + } + + IEnumerable AvailableCategories + { + get => _availableCategories; + set + { + if (ReferenceEquals(value, _availableCategories)) + return; + var newCategories = value?.ToList(); + if (newCategories == null || _availableCategories?.SequenceEqual(newCategories) != true) + { + _availableCategories = newCategories; + OnPropertyChanged(nameof(AvailableCategories)); + OnPropertyChanged(nameof(HasCategories)); + } + } + } + + bool HasCategories => _availableCategories?.Count > 0; + + void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + + public UnitTestPanel(bool showLog = true) + { + _customFilterControls = new Panel(); + + _progress = new UnitTestProgressBar(); + + _timer = new UITimer(); + _timer.Interval = 0.5; + _timer.Elapsed += (sender, e) => PerformSearch(); + + _testCountLabel = new Label { + VerticalAlignment = VerticalAlignment.Center + }; + + _startButton = new Button { Text = "Start" }; + _startButton.Click += async (s, e) => await RunTests(); + _startButton.BindDataContext(c => c.Enabled, Binding.Property((ITestRunner r) => r.IsRunning).ToBool(false, true, false), DualBindingMode.OneWay); + + _stopButton = new Button { Text = "Stop", Enabled = false }; + _stopButton.Click += (s, e) => Runner?.StopTests(); + + _search = new SearchBox(); + _search.PlaceholderText = "Filter(s)"; + _search.Focus(); + _search.KeyDown += (sender, e) => + { + if (e.KeyData == Keys.Enter) + { + _startButton.PerformClick(); + e.Handled = true; + } + }; + _search.TextChanged += (sender, e) => _timer.Start(); + + + _tree = new TreeGridView { ShowHeader = false, Size = new Size(400, -1) }; + _tree.Columns.Add(new GridColumn + { + DataCell = new ImageTextCell + { + TextBinding = Binding.Property((UnitTestItem m) => m.Text), + ImageBinding = Binding.Property((UnitTestItem m) => m.Image) + } + }); + + _tree.Activated += async (sender, e) => + { + if (Runner.IsRunning) + return; + var item = (UnitTestItem)_tree.SelectedItem; + if (item != null) + { + var filter = item.Filter; + if (filter != null) + { + await RunTests(filter); + } + } + }; + + var showOutputCheckBox = new CheckBox { Text = "Show Output" }; + showOutputCheckBox.CheckedBinding.BindDataContext((LoggingTestRunner r) => r.ShowOutput); + showOutputCheckBox.BindDataContext(r => r.Visible, Binding.Delegate((LoggingTestRunner r) => r != null)); + + var buttons = new TableLayout + { + Padding = new Padding(10, 0), + Spacing = new Size(5, 5), + Rows = { new TableRow(_startButton, _stopButton, showOutputCheckBox, null, _testCountLabel) } + }; + + var statusChecks = new CheckBoxList + { + Spacing = new Size(2, 2), + Orientation = Orientation.Horizontal, + DataStore = GetOptionalFilters() + }; + statusChecks.SelectedValuesBinding.CastItems().To().Bind(this, c => c.StatusFilters); + + var includeChecks = new CheckBoxList + { + Spacing = new Size(2, 2), + }; + includeChecks.Bind(c => c.DataStore, this, c => c.AvailableCategories); + includeChecks.SelectedValuesBinding.CastItems().To().Bind(this, c => c.IncludeCategories); + includeChecks.Bind(c => c.Visible, this, c => c.HasCategories); + + var includeLabel = new Label { Text = "Include" }; + includeLabel.Bind(c => c.Visible, this, c => c.HasCategories); + + var excludeChecks = new CheckBoxList + { + Spacing = new Size(2, 2), + }; + excludeChecks.Bind(c => c.DataStore, this, c => c.AvailableCategories); + excludeChecks.SelectedValuesBinding.CastItems().To().Bind(this, c => c.ExcludeCategories); + excludeChecks.Bind(c => c.Visible, this, c => c.HasCategories); + + var excludeLabel = new Label { Text = "Exclude" }; + excludeLabel.Bind(c => c.Visible, this, c => c.HasCategories); + + _filterControls = new TableLayout + { + Spacing = new Size(5, 5), + Rows = { + new TableRow("Show", statusChecks, null), + new TableRow(includeLabel, includeChecks), + new TableRow(excludeLabel, excludeChecks) + } + }; + + var allFilters = new Panel + { + Padding = new Padding(10, 0), + Content = new Scrollable + { + Border = BorderType.None, + Content = new TableLayout { Rows = { _filterControls, _customFilterControls } } + } + }; + + if (showLog) + { + Size = new Size(950, 600); + _log = new TextArea { Size = new Size(400, 300), ReadOnly = true, Wrap = false }; + + base.Content = new Splitter + { + FixedPanel = SplitterFixedPanel.None, + + Panel1 = new TableLayout + { + Padding = new Padding(0, 10, 0, 0), + Spacing = new Size(5, 5), + Rows = { allFilters, _search, _tree } + }, + + Panel2 = new TableLayout + { + Padding = new Padding(0, 10, 0, 0), + Spacing = new Size(5, 5), + Rows = { buttons, _progress, _log } + } + }; + } + else + { + Size = new Size(400, 400); + base.Content = new TableLayout + { + Padding = new Padding(0, 10, 0, 0), + Spacing = new Size(5, 5), + Rows = { buttons, allFilters, _search, _progress, _tree } + }; + } + } + + private void PerformSearch() + { + _timer.Stop(); + PopulateTree(); + } + + List logQueue = new List(); + + void Runner_IsRunningChanged(object sender, EventArgs e) + { + var running = Runner.IsRunning; + Application.Instance.Invoke(() => + { + _startButton.Enabled = !running; + _stopButton.Enabled = running; + _search.ReadOnly = running; + _filterControls.Enabled = !running; + + if (!running && StatusFilters.Any()) + PopulateTree(); + }); + } + + void Runner_Log(object sender, UnitTestLogEventArgs e) + { + lock (logQueue) + { + logQueue.Add(e); + } + _asyncQueue.Add("log", () => + { + List logQueueCopy; + lock (logQueue) + { + logQueueCopy = logQueue; + logQueue = new List(); + } + var sb = new StringBuilder(); + + foreach (var logEvent in logQueueCopy) + { + if (_log != null) + { + sb.AppendLine(logEvent.Message); + } + Log?.Invoke(this, logEvent); + } + + _log?.Append(sb.ToString(), true); + }); + } + + void WriteLog(string text) => _log?.Append(text + "\n", true); + + void Runner_Progress(object sender, UnitTestProgressEventArgs e) + { + var progressAmount = e.TestCaseCount > 0 ? (float)e.CompletedCount / e.TestCaseCount : 0; + var color = e.FailCount > 0 ? Colors.Red : e.WarningCount > 0 ? Colors.Yellow : Colors.Green; + _asyncQueue.Add("progress", () => + { + _progress.Progress = progressAmount; + _progress.Color = color; + }); + } + + + void Runner_TestStarted(object sender, UnitTestTestEventArgs e) + { + var test = e.Test; + if (_testMap.TryGetValue(test, out var treeItem)) + { + if (_lastResultMap.ContainsKey(test)) + _lastResultMap.TryRemove(test, out var result); + _asyncQueue.Add(() => + { + treeItem.Image = RunningStateImage; + _tree.ReloadItem(treeItem, false); + }); + } + } + + IList GetAllResults(ITest test) + { + if (_allResultsMap.TryGetValue(test, out var list)) + return list; + + list = new List(); + if (_allResultsMap.TryAdd(test, list)) + return list; + + if (_allResultsMap.TryGetValue(test, out list)) + return list; + + throw new InvalidOperationException($"All results does not have an entry for {test.FullName}"); + } + + + + void Runner_TestFinished(object sender, UnitTestResultEventArgs e) + { + var test = e.Result.Test; + var result = e.Result; + if (_testMap.TryGetValue(test, out var treeItem)) + { + _lastResultMap[test] = result; + GetAllResults(test).Add(result); + + _asyncQueue.Add(() => + { + treeItem.Image = GetStateImage(result); + _tree.ReloadItem(treeItem, false); + }); + } + } + + Image NotRunStateImage => _notRunStateImage ?? (_notRunStateImage = CreateImage(Colors.Silver, Colors.Black, null)); + + Image RunningStateImage => _runningStateImage ?? (_runningStateImage = CreateImage(Colors.Blue, Colors.White, "↻")); + + Image GetStateImage(ITestResult result) => result != null ? GetStateImage(result.Status) : NotRunStateImage; + + Image GetStateImage(TestStatus status) + { + if (_stateImages.TryGetValue(status, out var image)) + return image; + image = CreateImage(status); + _stateImages[status] = image; + return image; + } + + + Image CreateImage(TestStatus status) + { + switch (status) + { + case TestStatus.Warning: + return CreateImage(Colors.Yellow, Colors.Black, "!"); + case TestStatus.Failed: + return CreateImage(Colors.Red, (g, b) => + { + var offset = 10; + var pen = new Pen(Colors.White, 4); + g.DrawLine(pen, offset, offset, b.Width - offset, b.Height - offset); + g.DrawLine(pen, b.Width - offset, offset, offset, b.Height - offset); + }); + case TestStatus.Inconclusive: + return CreateImage(Colors.Yellow, new Color(Colors.Black, 0.8f), "?"); + case TestStatus.Skipped: + return CreateImage(Colors.Yellow, new Color(Colors.Black, 0.8f), "⤸"); + case TestStatus.Passed: + return CreateImage(Colors.Green, Colors.White, "✓"); + //case TestStatus.Error: + // return CreateImage(Colors.Red, Colors.White, "!"); + default: + throw new NotSupportedException(); + } + } + + static Image CreateImage(Color color, Action draw) + { + var bmp = new Bitmap(32, 32, PixelFormat.Format32bppRgba); + using (var g = new Graphics(bmp)) + { + var r = new RectangleF(Point.Empty, bmp.Size); + r.Inflate(-1, -1); + g.FillEllipse(color, r); + draw?.Invoke(g, bmp); + } + return bmp.WithSize(16, 16); + } + + static Image CreateImage(Color color, Color textcolor, string text) + { + return CreateImage(color, (g, b) => + { + var r = new RectangleF(Point.Empty, b.Size); + r.Inflate(-1, -1); + if (text != null) + { + var font = SystemFonts.Default(SystemFonts.Default().Size * 2); + var size = g.MeasureString(font, text); + var location = r.Location + (PointF)(r.Size - size) / 2; + g.DrawText(font, textcolor, location, text); + } + }); + } + + async Task RunTests(ITestFilter filter = null) + { + if (!_startButton.Enabled) + return; + _startButton.Enabled = false; + _progress.Progress = 0; + _progress.Color = Colors.Green; + if (_log != null) + _log.Text = string.Empty; + + // run asynchronously so UI is responsive + await Runner.RunAsync(CreateFilter(filter)); + } + + ITestFilter CreateFilter(ITestFilter testFilter = null) + { + var filters = GetFilters(testFilter).ToList(); + if (filters.Count > 1) + return new AndFilter(filters); + + if (filters.Count == 0) + return TestFilter.Empty; + + return filters[0]; + } + + IEnumerable GetFilters(ITestFilter testFilter) + { + if (_customFilter != null) + yield return _customFilter; + + if (IncludeCategories.Any()) + yield return new CategoryFilter(IncludeCategories); + + if (ExcludeCategories.Any()) + yield return new NotFilter(new CategoryFilter(ExcludeCategories) { ChildCanMatch = false }); + + if (testFilter != null) + yield return testFilter; + + if (StatusFilters.Any()) + yield return new OrFilter(StatusFilters.OfType()); + + if (!string.IsNullOrWhiteSpace(_search.Text)) + yield return new KeywordFilter { Keywords = _search.Text }; + } + + protected override void OnLoadComplete(EventArgs e) + { + base.OnLoadComplete(e); + try + { + if (_tree != null) + PopulateTree(); + } + catch (Exception ex) + { + _log?.Append($"Error populating tree\n{ex}", true); + } + } + + public void Refresh() + { + PopulateTree(); + } + + void PopulateTree() + { + if (Runner == null) + { + Application.Instance.Invoke(() => + { + AvailableCategories = null; + _testCountLabel.Text = null; + _testMap = null; + _tree.DataStore = null; + }); + return; + } + var filter = CreateFilter(); + var categories = AvailableCategories; + Task.Run(async () => + { + var runner = Runner; + if (runner == null) + return; + var tests = runner.TestSuite; + if (tests == null) + return; + + var map = new Dictionary(); + + // always show all categories + if (categories == null) + categories = (await runner.GetCategories(TestFilter.Empty)).OrderBy(r => r).ToList(); + var totalTestCount = await runner.GetTestCount(filter); + var treeNode = ToTree(tests.Assembly, tests, filter, map); + if (treeNode?.Text != null) + treeNode = new UnitTestItem { Children = { treeNode } }; + Application.Instance.AsyncInvoke(() => + { + AvailableCategories = categories; + _testCountLabel.Text = $"{totalTestCount} Tests"; + _testMap = map; + _tree.DataStore = treeNode; + }); + }).ContinueWith( t => + { + var errorText = $"Exception: {t.Exception}"; + Console.WriteLine(errorText); + Application.Instance.Invoke(() => _log?.Append(errorText, true)); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + + class UnitTestItem : TreeGridItem + { + public string Text { get; set; } + public ITest Test { get; set; } + public Image Image { get; set; } + public ITestFilter Filter { get; set; } + } + + UnitTestItem ToTree(Assembly assembly, ITest test, ITestFilter filter, IDictionary map) + { + // add a test + var name = test.Name; + if (test.IsAssembly) + { + var an = new AssemblyName(Path.GetFileNameWithoutExtension(test.Name)); + name = an.Name; + } + + if (!filter.Pass(test)) + return null; + + _lastResultMap.TryGetValue(test, out var result); + var worstChildResult = test.GetChildren() + .Select(t => _lastResultMap.TryGetValue(t, out var r) ? r : null) + .Where(r => r != null) + .OrderByDescending(r => r.Status) + .FirstOrDefault(); + if (worstChildResult?.Status > result?.Status) + result = worstChildResult; + + var item = new UnitTestItem + { + Text = name, + Test = test, + Image = GetStateImage(result), + Filter = new SingleTestFilter { Test = test, Assembly = assembly } + }; + map[test] = item; + if (test.HasChildren) + { + item.Expanded = !test.IsParameterized; + foreach (var child in test.Tests) + { + var treeItem = ToTree(assembly, child, filter, map); + if (treeItem != null) + item.Children.Add(treeItem); + } + if (MergeSingleNodes && item.Text != null) + { + while (item.Children.Count == 1) + { + // collapse test nodes + var child = item.Children[0] as UnitTestItem; + if (child.Children.Count == 0) + break; + if (!child.Text.StartsWith(item.Text, StringComparison.Ordinal)) + { + var separator = test.IsAssembly ? ":" : "."; + + child.Text = $"{item.Text}{separator}{child.Text}"; + } + child.Expanded |= test.IsAssembly; + item = child; + } + } + if (item.Children.Count == 0) + return null; + } + return item; + } + } +} diff --git a/src/Eto.UnitTest/UI/UnitTestProgressBar.cs b/src/Eto.UnitTest/UI/UnitTestProgressBar.cs new file mode 100644 index 0000000..47fbbd2 --- /dev/null +++ b/src/Eto.UnitTest/UI/UnitTestProgressBar.cs @@ -0,0 +1,48 @@ +using Eto.Forms; +using Eto.Drawing; + +namespace Eto.UnitTest.UI +{ + class UnitTestProgressBar : Drawable + { + float progress; + Color color = Colors.Green; + public float Progress + { + get => progress; + set + { + if (progress != value) + { + progress = value; + Invalidate(); + } + } + } + + public Color Color + { + get => color; + set + { + if (color != value) + { + color = value; + Invalidate(); + } + } + } + + public UnitTestProgressBar() + { + Size = new Size(200, 5); + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var size = new SizeF(Width * progress, Height); + e.Graphics.FillRectangle(Color, 0, 0, size.Width, size.Height); + } + } +} diff --git a/src/Eto.UnitTest/UnitTestLogEventArgs.cs b/src/Eto.UnitTest/UnitTestLogEventArgs.cs new file mode 100644 index 0000000..82fbc5a --- /dev/null +++ b/src/Eto.UnitTest/UnitTestLogEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Eto.UnitTest +{ + public class UnitTestLogEventArgs : EventArgs + { + public string Message { get; } + + public UnitTestLogEventArgs(string message) + { + Message = message; + } + } +} diff --git a/src/Eto.UnitTest/UnitTestProgressEventArgs.cs b/src/Eto.UnitTest/UnitTestProgressEventArgs.cs new file mode 100644 index 0000000..92c0f30 --- /dev/null +++ b/src/Eto.UnitTest/UnitTestProgressEventArgs.cs @@ -0,0 +1,39 @@ +using System; + +namespace Eto.UnitTest +{ + public class UnitTestProgressEventArgs : EventArgs + { + public int TestCaseCount { get; private set; } + public int CompletedCount { get; private set; } + public int AssertCount { get; private set; } + public int FailCount { get; private set; } + public int PassCount { get; private set; } + public int WarningCount { get; private set; } + public int InconclusiveCount { get; private set; } + public int SkipCount { get; private set; } + public ITestResult CurrentResult { get; private set; } + + public UnitTestProgressEventArgs(int testCaseCount) + { + TestCaseCount = testCaseCount; + } + + internal void SetCount(int count) + { + CompletedCount = count; + } + + public void AddResult(ITestResult result) + { + CurrentResult = result; + CompletedCount++; + AssertCount += result.AssertCount; + FailCount += result.FailCount; + WarningCount += result.WarningCount; + PassCount += result.PassCount; + InconclusiveCount += result.InconclusiveCount; + SkipCount += result.SkipCount; + } + } +} diff --git a/src/Eto.UnitTest/UnitTestResultEventArgs.cs b/src/Eto.UnitTest/UnitTestResultEventArgs.cs new file mode 100644 index 0000000..509d90d --- /dev/null +++ b/src/Eto.UnitTest/UnitTestResultEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Eto.UnitTest +{ + public sealed class UnitTestResultEventArgs : EventArgs + { + public ITestResult Result { get; private set; } + + public UnitTestResultEventArgs(ITestResult result) + { + Result = result; + } + } +} diff --git a/src/Eto.UnitTest/UnitTestTestEventArgs.cs b/src/Eto.UnitTest/UnitTestTestEventArgs.cs new file mode 100644 index 0000000..3000583 --- /dev/null +++ b/src/Eto.UnitTest/UnitTestTestEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Eto.UnitTest +{ + public sealed class UnitTestTestEventArgs : EventArgs + { + public ITest Test { get; private set; } + + public UnitTestTestEventArgs(ITest test) + { + Test = test; + } + } +}