// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) // This code is distributed under the GNU LGPL (for details please see \doc\license.txt) using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using ICSharpCode.Core; using ICSharpCode.NRefactory.Utils; using ICSharpCode.SharpDevelop; using ICSharpCode.SharpDevelop.Gui; using ICSharpCode.SharpDevelop.Project; using ICSharpCode.SharpDevelop.Workbench; namespace ICSharpCode.UnitTesting.Frameworks { /// /// Manages the execution of tests across multiple projects. /// Takes care of building the projects (if necessary) and showing progress in the UI. /// public class TestExecutionManager { readonly IBuildService buildService; readonly IUnitTestTaskService taskService; readonly IUnitTestSaveAllFilesCommand saveAllFilesCommand; readonly ITestService testService; readonly IWorkbench workbench; readonly IMessageLoop mainThread; readonly IStatusBarService statusBarService; readonly IBuildOptions buildOptions; public TestExecutionManager() { this.buildService = SD.BuildService; this.taskService = new UnitTestTaskService(); this.saveAllFilesCommand = new UnitTestSaveAllFilesCommand(); this.testService = SD.GetRequiredService(); this.workbench = SD.Workbench; this.statusBarService = SD.StatusBar; this.mainThread = SD.MainThread; this.buildOptions = new UnitTestBuildOptions(); } readonly MultiDictionary testsByProject = new MultiDictionary(); CancellationToken cancellationToken; ITestProject currentProjectBeingTested; IProgressMonitor testProgressMonitor; public async Task RunTestsAsync(IEnumerable selectedTests, TestExecutionOptions options, CancellationToken cancellationToken) { this.cancellationToken = cancellationToken; GroupTestsByProject(selectedTests); ClearTasks(); ShowUnitTestsPad(); ShowOutputPad(); ResetTestResults(); saveAllFilesCommand.SaveAllFiles(); // Run the build, if necessary: var projectsToBuild = testsByProject.Keys.Where(p => p.IsBuildNeededBeforeTestRun).Select(p => p.Project).ToList(); if (projectsToBuild.Count > 0) { using (cancellationToken.Register(buildService.CancelBuild)) { var buildOptions = new BuildOptions(BuildTarget.Build); buildOptions.BuildDetection = BuildOptions.BuildOnExecute; var buildResults = await buildService.BuildAsync(projectsToBuild, buildOptions); if (buildResults.Result != BuildResultCode.Success) return; } } cancellationToken.ThrowIfCancellationRequested(); using (IProgressMonitor progressMonitor = statusBarService.CreateProgressMonitor(cancellationToken)) { int projectsLeftToRun = testsByProject.Count; foreach (IGrouping g in testsByProject.OrderBy(g => g.Key.DisplayName)) { currentProjectBeingTested = g.Key; progressMonitor.TaskName = GetProgressMonitorLabel(currentProjectBeingTested); progressMonitor.Progress = GetProgress(projectsLeftToRun); using (testProgressMonitor = progressMonitor.CreateSubTask(1.0 / testsByProject.Count)) { using (ITestRunner testRunner = currentProjectBeingTested.CreateTestRunner(options)) { testRunner.TestFinished += testRunner_TestFinished; var writer = new MessageViewCategoryTextWriter(testService.UnitTestMessageView); await testRunner.RunAsync(g, testProgressMonitor, writer, testProgressMonitor.CancellationToken); } } projectsLeftToRun--; progressMonitor.CancellationToken.ThrowIfCancellationRequested(); } } ShowErrorList(); } void GroupTestsByProject(IEnumerable selectedTests) { foreach (ITest test in selectedTests) { if (test == null) continue; if (test.ParentProject == null) { // When a solution is selected, select all its projects individually foreach (ITest project in test.NestedTests) { Debug.Assert(project == project.ParentProject); testsByProject.Add(project.ParentProject, project); } } else { testsByProject.Add(test.ParentProject, test); } cancellationToken.ThrowIfCancellationRequested(); } } void ClearTasks() { taskService.BuildMessageViewCategory.Clear(); taskService.ClearExceptCommentTasks(); testService.UnitTestMessageView.Clear(); } void ShowUnitTestsPad() { var descriptor = workbench.GetPad(typeof(UnitTestsPad)); descriptor.BringPadToFront(); var pad = descriptor.PadContent as UnitTestsPad; if (pad != null) { pad.TreeView.SelectedTests = testsByProject.Values; } } void ShowOutputPad() { testService.UnitTestMessageView.Activate(true); } void ResetTestResults() { cancellationToken.ThrowIfCancellationRequested(); foreach (ITest test in testsByProject.Values) { test.ResetTestResults(); } cancellationToken.ThrowIfCancellationRequested(); } string GetProgressMonitorLabel(ITestProject project) { StringTagPair tagPair = new StringTagPair("Name", project.DisplayName); return StringParser.Parse("${res:ICSharpCode.UnitTesting.StatusBarProgressLabel}", tagPair); } double GetProgress(int projectsLeftToRunCount) { int totalProjectCount = testsByProject.Count; return (double)(totalProjectCount - projectsLeftToRunCount) / totalProjectCount; } void testRunner_TestFinished(object sender, TestFinishedEventArgs e) { mainThread.InvokeAsyncAndForget(delegate { ShowResult(e.Result); }); } protected void ShowResult(TestResult result) { if (IsTestResultFailureOrIsIgnored(result)) { AddTaskForTestResult(result); UpdateProgressMonitorStatus(result); } UpdateTestResult(result); } bool IsTestResultFailureOrIsIgnored(TestResult result) { return result.IsFailure || result.IsIgnored; } void AddTaskForTestResult(TestResult testResult) { SDTask task = TestResultTask.Create(testResult, currentProjectBeingTested); taskService.Add(task); } void UpdateProgressMonitorStatus(TestResult result) { if (testProgressMonitor != null) { if (result.IsFailure) { testProgressMonitor.Status = OperationStatus.Error; } else if (result.IsIgnored && testProgressMonitor.Status == OperationStatus.Normal) { testProgressMonitor.Status = OperationStatus.Warning; } } } void UpdateTestResult(TestResult result) { if (currentProjectBeingTested != null) { currentProjectBeingTested.UpdateTestResult(result); } } void ShowErrorList() { if (taskService.SomethingWentWrong && buildOptions.ShowErrorListAfterBuild) { workbench.GetPad(typeof(ErrorListPad)).BringPadToFront(); } } } }