using GalaSoft.MvvmLight; using System.Windows.Input; using System; using System.Windows; using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Win32; using Xunit; using Xunit.Abstractions; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Collections.Specialized; using System.Threading; using System.Threading.Tasks; namespace xunit.runner.wpf.ViewModel { public class MainViewModel : ViewModelBase { private readonly ObservableCollection allTestCases = new ObservableCollection(); private CancellationTokenSource filterCancellationTokenSource = new CancellationTokenSource(); private bool isBusy; private bool isCancelRequested; public MainViewModel() { if (IsInDesignMode) { this.Assemblies.Add(new TestAssemblyViewModel(@"C:\Code\TestAssembly.dll")); } CommandBindings = CreateCommandBindings(); this.MethodsCaption = "Methods (0)"; TestCases = new FilteredCollectionView>( allTestCases, TestCaseMatches, Tuple.Create(string.Empty, TestState.All), TestComparer.Instance); this.TestCases.CollectionChanged += TestCases_CollectionChanged; this.RunCommand = new RelayCommand(OnExecuteRun, CanExecuteRun); this.CancelCommand = new RelayCommand(OnExecuteCancel, CanExecuteCancel); } private static bool TestCaseMatches(TestCaseViewModel testCase, Tuple filterTextAndTestState) => testCase.DisplayName.Contains(filterTextAndTestState.Item1) && (testCase.State & filterTextAndTestState.Item2) == filterTextAndTestState.Item2; private void TestCases_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { MethodsCaption = $"Methods ({TestCases.Count})"; } public ICommand ExitCommand { get; } = new RelayCommand(OnExecuteExit); public ICommand RunCommand { get; } public ICommand CancelCommand { get; } public CommandBindingCollection CommandBindings { get; } private string methodsCaption; public string MethodsCaption { get { return methodsCaption; } private set { Set(ref methodsCaption, value); } } private string searchQuery = string.Empty; public string SearchQuery { get { return searchQuery; } set { if (Set(ref searchQuery, value)) { FilterAfterDelay(); } } } private TestState resultFilter = TestState.All; public TestState ResultFilter { get { return resultFilter; } set { if (Set(ref resultFilter, value)) { this.FilterAfterDelay(); } } } private void FilterAfterDelay() { filterCancellationTokenSource.Cancel(); filterCancellationTokenSource = new CancellationTokenSource(); var token = filterCancellationTokenSource.Token; Task .Delay(TimeSpan.FromMilliseconds(500), token) .ContinueWith( x => { TestCases.FilterArgument = Tuple.Create(SearchQuery, ResultFilter); }, token, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); } private CommandBindingCollection CreateCommandBindings() { var openBinding = new CommandBinding(ApplicationCommands.Open, OnExecuteOpen); CommandManager.RegisterClassCommandBinding(typeof(MainViewModel), openBinding); return new CommandBindingCollection { openBinding, }; } public ObservableCollection Assemblies { get; } = new ObservableCollection(); public FilteredCollectionView> TestCases { get; } private void OnExecuteOpen(object sender, ExecutedRoutedEventArgs e) { var fileDialog = new OpenFileDialog { Filter = "Unit Test Assemblies|*.dll", }; if (fileDialog.ShowDialog(Application.Current.MainWindow) != true) { return; } var fileName = fileDialog.FileName; try { var xunit = new XunitFrontController( useAppDomain: true, assemblyFileName: fileName, shadowCopy: false); var testDiscoveryVisitor = new TestDiscoveryVisitor(); xunit.Find(includeSourceInformation: false, messageSink: testDiscoveryVisitor, discoveryOptions: TestFrameworkOptions.ForDiscovery()); testDiscoveryVisitor.Finished.WaitOne(); allTestCases.AddRange(testDiscoveryVisitor.TestCases); Assemblies.Add(new TestAssemblyViewModel(fileName)); } catch (Exception ex) { MessageBox.Show(Application.Current.MainWindow, ex.ToString()); } } private class TestDiscoveryVisitor : TestMessageVisitor { public List TestCases { get; } = new List(); public IDictionary> Traits { get; } = new Dictionary>(); protected override bool Visit(ITestCaseDiscoveryMessage testCaseDiscovered) { var testCase = testCaseDiscovered.TestCase; TestCases.Add(new TestCaseViewModel(testCase, testCaseDiscovered.TestAssembly.Assembly.AssemblyPath)); foreach (var k in testCase.Traits.Keys) { IList value; if (!Traits.TryGetValue(k, out value)) { value = new List(); Traits[k] = value; } value.AddRange(testCase.Traits[k]); } return true; } } private class TestRunVisitor : TestMessageVisitor { private readonly Func isCancelRequested; private readonly IEnumerable testCases; public TestRunVisitor(IEnumerable testCases, Func isCancelRequested) { this.testCases = testCases; this.isCancelRequested = isCancelRequested; } protected override bool Visit(ITestFailed testFailed) { var testCase = testCases.Single(tc => tc.DisplayName == testFailed.TestCase.DisplayName); testCase.State = TestState.Failed; return !isCancelRequested(); } protected override bool Visit(ITestPassed testPassed) { var testCase = testCases.Single(tc => tc.DisplayName == testPassed.TestCase.DisplayName); testCase.State = TestState.Passed; return !isCancelRequested(); } protected override bool Visit(ITestSkipped testSkipped) { var testCase = testCases.Single(tc => tc.DisplayName == testSkipped.TestCase.DisplayName); testCase.State = TestState.Skipped; return !isCancelRequested(); } } private static void OnExecuteExit() { Application.Current.Shutdown(); } private bool CanExecuteRun() => !isBusy && TestCases.Any(); private void OnExecuteRun() { try { var selectedAssemblies = TestCases.ToLookup(tc => tc.AssemblyFileName); foreach (var assembly in selectedAssemblies) { var xunit = new XunitFrontController( assemblyFileName: assembly.Key, useAppDomain: true, shadowCopy: false); var testRunVisitor = new TestRunVisitor(allTestCases, () => isCancelRequested); xunit.RunTests(assembly.Select(tcvm => tcvm.TestCase).ToArray(), testRunVisitor, TestFrameworkOptions.ForExecution()); testRunVisitor.Finished.WaitOne(); } } finally { isBusy = false; isCancelRequested = false; } } private bool CanExecuteCancel() => isBusy; private void OnExecuteCancel() { this.isCancelRequested = true; } } public enum TestState { All = 0, Passed, Failed, Skipped, NotRun } public class TestComparer : IComparer { public static TestComparer Instance { get; } = new TestComparer(); public int Compare(TestCaseViewModel x, TestCaseViewModel y) => StringComparer.Ordinal.Compare(x.DisplayName, y.DisplayName); private TestComparer() { } } }