From 9967ac4de756958a7889328183b95a6e3f6d3917 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Sun, 13 Feb 2022 16:33:44 +0000 Subject: [PATCH] add logging to the Fine Code Coverage tool window (#227) * add logging to the Fine Code Coverage tool window * Fine Code Coverage link to FCC Output Window Pane * Ensure output window is open before activating FCC pane. * refactor responsibilities * Use font and colors / environment font both for font-family and font-size in combination with dpi. * delete test that does not apply anymore --- FineCodeCoverage/FineCodeCoverage.csproj | 4 + .../FineCodeCoverage2022.csproj | 4 + FineCodeCoverageTests/FCCEngine_Tests.cs | 61 +- .../TestContainerDiscovery_Tests.cs | 11 - README.md | 13 +- .../Resources/dummyReportToProcess.html | 687 ++++++++++++++++++ Shared Files/Resources/reportparts.xml | 18 + SharedProject/Core/CoverageUtilManager.cs | 16 +- SharedProject/Core/FCCEngine.cs | 131 +++- SharedProject/Core/ICoverageUtilManager.cs | 1 + SharedProject/Core/IFCCEngine.cs | 8 + SharedProject/Core/Model/CoverageProject.cs | 16 +- ...verageProjectFileSynchronizationDetails.cs | 12 + SharedProject/Core/Model/ICoverageProject.cs | 2 +- .../Core/ReportGenerator/IReportColours.cs | 16 + .../Core/ReportGenerator/JsThemeStyling.cs | 16 + .../Core/ReportGenerator/ReportColours.cs | 30 + .../ReportGenerator/ReportColoursProvider.cs | 38 +- .../ReportGenerator/ReportGeneratorUtil.cs | 240 +++++- .../Core/Utilities/IResourceProvider.cs | 7 + .../Core/Utilities/ISolutionEvents.cs | 9 + .../Core/Utilities/ResourceProvider.cs | 22 + .../Core/Utilities/SolutionEvents.cs | 30 + SharedProject/Impl/Logger.cs | 32 +- .../TestContainerDiscoverer.cs | 52 +- .../Output/OutputToolWindowControl.xaml | 2 +- .../Output/OutputToolWindowControl.xaml.cs | 136 +++- SharedProject/Output/ScriptManager.cs | 21 +- SharedProject/SharedProject.projitems | 5 + 29 files changed, 1434 insertions(+), 206 deletions(-) create mode 100644 Shared Files/Resources/dummyReportToProcess.html create mode 100644 SharedProject/Core/Model/CoverageProjectFileSynchronizationDetails.cs create mode 100644 SharedProject/Core/Utilities/IResourceProvider.cs create mode 100644 SharedProject/Core/Utilities/ISolutionEvents.cs create mode 100644 SharedProject/Core/Utilities/ResourceProvider.cs create mode 100644 SharedProject/Core/Utilities/SolutionEvents.cs diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index d5b18a41..9f53747d 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -73,6 +73,10 @@ Resources\LICENSE true + + Resources\dummyReportToProcess.html + true + Resources\reportparts.xml true diff --git a/FineCodeCoverage2022/FineCodeCoverage2022.csproj b/FineCodeCoverage2022/FineCodeCoverage2022.csproj index 7fd9529c..f9034c69 100644 --- a/FineCodeCoverage2022/FineCodeCoverage2022.csproj +++ b/FineCodeCoverage2022/FineCodeCoverage2022.csproj @@ -65,6 +65,10 @@ OutputToolWindowPackage.vsct Menus.ctmenu + + Resources\dummyReportToProcess.html + true + Resources\reportparts.xml true diff --git a/FineCodeCoverageTests/FCCEngine_Tests.cs b/FineCodeCoverageTests/FCCEngine_Tests.cs index 2f2934b7..283b6c77 100644 --- a/FineCodeCoverageTests/FCCEngine_Tests.cs +++ b/FineCodeCoverageTests/FCCEngine_Tests.cs @@ -342,44 +342,6 @@ public async Task Should_Not_Process_ReportGenerator_Output_If_Failure() } - [Test] - public async Task Should_Clear_UI_Then_Update_UI_When_ReloadCoverage_Completes_Fully() - { - fccEngine.CoverageLines = new List(); - var (reportGeneratedHtmlContent, updatedCoverageLines) = await RunToCompletion(false); - - VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Done); - - VerifyClearUIEvents(0); - - Assert.AreSame(updatedCoverageLines, updateMarginTagsCoverageLines[1]); - Assert.AreEqual(reportGeneratedHtmlContent, updateOutputWindowEvents[1].HtmlContent); - - } - - [Test] - public async Task Should_Clear_UI_When_ReloadCoverage_And_No_CoverageProjects() - { - fccEngine.CoverageLines = new List(); - - await RunToCompletion(true); - - VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Done); - - Assert.Null(updateMarginTagsCoverageLines[1]); - Assert.Null(updateOutputWindowEvents[1].HtmlContent); - } - - [Test] - public async Task Should_Update_OutputWindow_With_Null_HtmlContent_When_Reading_Report_Html_Throws() - { - await ThrowReadingReportHtml(); - - Assert.AreEqual(updateMarginTagsCoverageLines[1].Count, 1); - Assert.Null(updateOutputWindowEvents[1].HtmlContent); - - } - [Test] public async Task Should_Log_Single_Exception_From_Aggregate_Exception() { @@ -389,14 +351,6 @@ public async Task Should_Log_Single_Exception_From_Aggregate_Exception() mocker.Verify(l => l.Log(exception)); } - [Test] - public async Task Should_Clear_UI_When_There_Is_An_Exception() - { - fccEngine.CoverageLines = new List(); - await ThrowException(); - VerifyClearUIEvents(1); - } - [Test] public async Task Should_Cancel_Running_Coverage_Logging_Cancelled_When_StopCoverage() { @@ -404,15 +358,6 @@ public async Task Should_Cancel_Running_Coverage_Logging_Cancelled_When_StopCove VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Cancelled); } - [Test] - public async Task Should_Not_Update_UI_When_ReloadCoverage_Is_Cancelled() - { - await StopCoverage(); - Assert.AreEqual(1, updateMarginTagsEvents.Count); - Assert.AreEqual(1, updateOutputWindowEvents.Count); - - } - [Test] public async Task Should_Cancel_ProcessUtil_Tasks_When_StopCoverage() { @@ -450,7 +395,7 @@ public async Task Should_Cancel_Existing_ReloadCoverage_When_ReloadCoverage() Thread.Sleep(1000); t.Start(); - }).Returns(Task.CompletedTask); + }).Returns(Task.FromResult(new CoverageProjectFileSynchronizationDetails())); await ReloadInitializedCoverage(mockSuitableCoverageProject.Object); VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Cancelled); @@ -554,7 +499,7 @@ private async Task StopCoverage() { fccEngine.StopCoverage(); - }).Returns(Task.CompletedTask); + }).Returns(Task.FromResult(new CoverageProjectFileSynchronizationDetails())); await ReloadInitializedCoverage(mockSuitableCoverageProject.Object); } @@ -585,7 +530,7 @@ private Mock CreateSuitableProject() var mockSuitableCoverageProject = new Mock(); mockSuitableCoverageProject.Setup(p => p.ProjectFile).Returns("Defined.csproj"); mockSuitableCoverageProject.Setup(p => p.Settings.Enabled).Returns(true); - mockSuitableCoverageProject.Setup(p => p.PrepareForCoverageAsync()).Returns(Task.CompletedTask); + mockSuitableCoverageProject.Setup(p => p.PrepareForCoverageAsync()).Returns(Task.FromResult(new CoverageProjectFileSynchronizationDetails())); mockSuitableCoverageProject.Setup(p => p.StepAsync("Run Coverage Tool", It.IsAny>())).Returns(Task.CompletedTask); return mockSuitableCoverageProject; } diff --git a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs index 296cf5dd..fd2bb838 100644 --- a/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs +++ b/FineCodeCoverageTests/TestContainerDiscovery_Tests.cs @@ -227,16 +227,5 @@ public void Should_Handle_Any_Exception_In_OperationState_Changed_Handler_Loggin RaiseTestExecutionCancelling(); mocker.Verify(logger => logger.Log("Error processing unit test events", exception)); } - - [TestCase(true)] - [TestCase(false)] - public void Should_Clear_UI_When_Enabled_Setting_Is_Set_To_False(bool newEnabled) - { - var mockAppOptions = new Mock(); - mockAppOptions.Setup(o => o.Enabled).Returns(newEnabled); - mocker.GetMock().Raise(optionsProvider => optionsProvider.OptionsChanged += null, mockAppOptions.Object); - mocker.Verify(engine => engine.ClearUI(), newEnabled ? Times.Never() : Times.Once()); - - } } } \ No newline at end of file diff --git a/README.md b/README.md index a41faf71..c88b379f 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,26 @@ [![Build status](https://ci.appveyor.com/api/projects/status/yq8s0ridnphpx4ig?svg=true)](https://ci.appveyor.com/project/FortuneN/finecodecoverage) -Download this extension from the [Visual Studio Market Place](https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage) +Download this extension from the [Visual Studio Market Place ( vs 2019 )](https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage), [Visual Studio Market Place ( vs 2022 )](https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage2022) or download from [releases](https://github.com/FortuneN/FineCodeCoverage/releases). Older versions can be obtained from [here](https://ci.appveyor.com/project/FortuneN/finecodecoverage/history). ---------------------------------------- +--- Prerequisites Only that the test adapters are nuget packages. For instance, the NUnit Test Adapter extension is not sufficient. FCC will copy your test dll and dependencies to a sub folder this may affect your tests. The alternative is to set the option AdjacentBuildOutput to true. ---------------------------------------- + +--- Introduction Fine Code Coverage works by reacting to the visual studio test explorer, providing coverage from each test project containing tests that you have selected -to run. This coverage is presented as a single unified report as well as coloured margins alongside your code. +to run. This coverage is presented as a single unified report in the Fine Code Coverage Tool Window as well as coloured margins alongside your code. This coverage is not dynamic and represents the coverage obtained from the last time you executed tests. When the coverage becomes outdated, you can click the 'FCC Clear UI' button in Tools or run coverage again. +Details of how FCC is progressing with code coverage can be found in the Coverage Log tab in the Fine Code Coverage Tool Window with more detailed logs in the FCC Output Window Pane. If you experience issues then providing the logs from the output window will help to understand the nature of the problem. + The coverage is provided by either [OpenCover](https://github.com/OpenCover/opencover) for old style projects and [Coverlet](https://github.com/coverlet-coverage/coverlet) for new style sdk projects. FCC provides an abstraction over both so that it is possible to ignore the differences between the two but there are circumstances where it is important to be aware of cover tool that will be run. This is most apparent when Coverlet is used, please read on for the specifics. @@ -28,7 +31,7 @@ but there may be a preview version that you want to use. This can be configured Configuration is available with Visual Studio settings and project msbuild properties. All visual studio settings can be overridden from test project settings and some settings can only be set in project files. ---------------------------------------- +--- ### Watch Introduction Video diff --git a/Shared Files/Resources/dummyReportToProcess.html b/Shared Files/Resources/dummyReportToProcess.html new file mode 100644 index 00000000..51ca1742 --- /dev/null +++ b/Shared Files/Resources/dummyReportToProcess.html @@ -0,0 +1,687 @@ + + + + + + +Summary - Coverage Report + +
+

SummaryStarSponsor

+ ++++ + + + + + + + + + + + + + + +
Generated on:09/02/2022 - 12:58:25
Parser:CoberturaParser
Assemblies:0
Classes:0
Files:0
Covered lines:0
Uncovered lines:0
Coverable lines:0
Total lines:0
Line coverage:
Covered branches:0
Total branches:0
+ + +

Coverage

+ + +

No assemblies have been covered.

+
+ + \ No newline at end of file diff --git a/Shared Files/Resources/reportparts.xml b/Shared Files/Resources/reportparts.xml index 3354387a..a6b44552 100644 --- a/Shared Files/Resources/reportparts.xml +++ b/Shared Files/Resources/reportparts.xml @@ -26,4 +26,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SharedProject/Core/CoverageUtilManager.cs b/SharedProject/Core/CoverageUtilManager.cs index 11303c61..11496574 100644 --- a/SharedProject/Core/CoverageUtilManager.cs +++ b/SharedProject/Core/CoverageUtilManager.cs @@ -5,6 +5,7 @@ using FineCodeCoverage.Engine.Coverlet; using FineCodeCoverage.Engine.Model; using FineCodeCoverage.Engine.OpenCover; +using FineCodeCoverage.Engine.ReportGenerator; namespace FineCodeCoverage.Engine { @@ -20,23 +21,30 @@ public CoverageUtilManager(IOpenCoverUtil openCoverUtil, ICoverletUtil coverletU this.openCoverUtil = openCoverUtil; this.coverletUtil = coverletUtil; } - + + public string CoverageToolName(ICoverageProject project) + { + return project.IsDotNetSdkStyle() ? "Coverlet" : "OpenCover"; + } + public void Initialize(string appDataFolder) { openCoverUtil.Initialize(appDataFolder); coverletUtil.Initialize(appDataFolder); } - public Task RunCoverageAsync(ICoverageProject project, bool throwError = false) + public async Task RunCoverageAsync(ICoverageProject project, bool throwError = false) { + bool result; if (project.IsDotNetSdkStyle()) { - return coverletUtil.RunCoverletAsync(project, throwError); + result =await coverletUtil.RunCoverletAsync(project, throwError); } else { - return openCoverUtil.RunOpenCoverAsync(project, throwError); + result = await openCoverUtil.RunOpenCoverAsync(project, throwError); } + return result; } } } diff --git a/SharedProject/Core/FCCEngine.cs b/SharedProject/Core/FCCEngine.cs index 26596994..36dbc3c1 100644 --- a/SharedProject/Core/FCCEngine.cs +++ b/SharedProject/Core/FCCEngine.cs @@ -3,6 +3,7 @@ using System.ComponentModel.Composition; using System.Linq; using System.Threading; +using System.Windows; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Cobertura; using FineCodeCoverage.Engine.Model; @@ -10,7 +11,7 @@ using FineCodeCoverage.Engine.ReportGenerator; using FineCodeCoverage.Impl; using FineCodeCoverage.Options; -using Microsoft.VisualStudio.Shell; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine { @@ -21,7 +22,6 @@ internal class FCCEngine : IFCCEngine { internal int InitializeWait { get; set; } = 5000; internal const string initializationFailedMessagePrefix = "Initialization failed. Please check the following error which may be resolved by reopening visual studio which will start the initialization process again."; - private readonly object colorThemeService; private CancellationTokenSource cancellationTokenSource; public event UpdateMarginTagsDelegate UpdateMarginTags; @@ -30,19 +30,41 @@ internal class FCCEngine : IFCCEngine public string AppDataFolderPath { get; private set; } public List CoverageLines { get; internal set; } + private DpiScale dpiScale; + public DpiScale Dpi + { + get => dpiScale; + set + { + reportGeneratorUtil.DpiScale = value; + dpiScale = value; + UpdateReportWithDpiFontChanges(); + + } + } + private FontDetails environmentFontDetails; + public FontDetails EnvironmentFontDetails { + get => environmentFontDetails; + set { + environmentFontDetails = value; + reportGeneratorUtil.EnvironmentFontDetails = value; + UpdateReportWithDpiFontChanges(); + } + } + private readonly ICoverageUtilManager coverageUtilManager; private readonly ICoberturaUtil coberturaUtil; private readonly IMsTestPlatformUtil msTestPlatformUtil; private readonly IReportGeneratorUtil reportGeneratorUtil; private readonly IProcessUtil processUtil; - private readonly IAppOptionsProvider appOptionsProvider; private readonly ILogger logger; private readonly IAppDataFolder appDataFolder; - private readonly IServiceProvider serviceProvider; private IInitializeStatusProvider initializeStatusProvider; private readonly ICoverageToolOutputManager coverageOutputManager; internal System.Threading.Tasks.Task reloadCoverageTask; + private ISolutionEvents solutionEvents; // keep alive + private bool hasGeneratedReport; [ImportingConstructor] public FCCEngine( @@ -51,25 +73,30 @@ public FCCEngine( IMsTestPlatformUtil msTestPlatformUtil, IReportGeneratorUtil reportGeneratorUtil, IProcessUtil processUtil, - IAppOptionsProvider appOptionsProvider, ILogger logger, IAppDataFolder appDataFolder, ICoverageToolOutputManager coverageOutputManager, - [Import(typeof(SVsServiceProvider))] - IServiceProvider serviceProvider + ISolutionEvents solutionEvents, + IAppOptionsProvider appOptionsProvider ) { + this.solutionEvents = solutionEvents; + solutionEvents.AfterClosing += (s,args) => ClearOutputWindow(false); + appOptionsProvider.OptionsChanged += (appOptions) => + { + if (!appOptions.Enabled) + { + ClearUI(); + } + }; this.coverageOutputManager = coverageOutputManager; this.coverageUtilManager = coverageUtilManager; this.coberturaUtil = coberturaUtil; this.msTestPlatformUtil = msTestPlatformUtil; this.reportGeneratorUtil = reportGeneratorUtil; this.processUtil = processUtil; - this.appOptionsProvider = appOptionsProvider; this.logger = logger; this.appDataFolder = appDataFolder; - this.serviceProvider = serviceProvider; - colorThemeService = serviceProvider.GetService(typeof(SVsColorThemeService)); } internal string GetLogReloadCoverageStatusMessage(ReloadCoverageStatus reloadCoverageStatus) @@ -97,8 +124,20 @@ public void ClearUI() { CoverageLines = null; UpdateMarginTags?.Invoke(new UpdateMarginTagsEventArgs()); + ClearOutputWindow(true); + } + + private void ClearOutputWindow(bool withHistory) + { + RaiseUpdateOutputWindow(reportGeneratorUtil.BlankReport(withHistory)); + } - UpdateOutputWindow?.Invoke(new UpdateOutputWindowEventArgs { }); + private void UpdateReportWithDpiFontChanges() + { + if (hasGeneratedReport) + { + reportGeneratorUtil.UpdateReportWithDpiFontChanges(); + } } public void StopCoverage() @@ -111,7 +150,9 @@ public void StopCoverage() private CancellationToken Reset() { - ClearUI(); + CoverageLines = null; + UpdateMarginTags?.Invoke(new UpdateMarginTagsEventArgs()); + StopCoverage(); cancellationTokenSource = new CancellationTokenSource(); @@ -130,7 +171,32 @@ private async System.Threading.Tasks.Task RunCoverageAsync(List coverageUtilManager.RunCoverageAsync(project, true)); + var coverageProjectHasFailed = coverageProject.HasFailed; + if (coverageProjectHasFailed) // todo remove step + { + await reportGeneratorUtil.LogCoverageProcessAsync($"{coverageProject.ProjectName} failed : {coverageProject.FailureDescription}"); + } + await coverageProject.StepAsync("Run Coverage Tool", async (project) => + { + var start = DateTime.Now; + try + { + var coverageTool = coverageUtilManager.CoverageToolName(project); + await reportGeneratorUtil.LogCoverageProcessAsync($"Starting {coverageTool} coverage for {project.ProjectName}"); + await coverageUtilManager.RunCoverageAsync(project, true); + }catch(Exception exc) + { + await reportGeneratorUtil.LogCoverageProcessAsync($"{coverageProject.ProjectName} failed : {exc}"); + throw exc; + } + if (!cancellationToken.IsCancellationRequested) + { + var duration = DateTime.Now - start; + var durationMessage = $"Completed coverage for {coverageProject.ProjectName} : {duration}"; + logger.Log(durationMessage); + await reportGeneratorUtil.LogCoverageProcessAsync(durationMessage); + } + }); } var passedProjects = coverageProjects.Where(p => !p.HasFailed); @@ -145,11 +211,17 @@ private void RaiseUpdateOutputWindow(string reportHtml) { UpdateOutputWindowEventArgs updateOutputWindowEventArgs = new UpdateOutputWindowEventArgs { HtmlContent = reportHtml}; UpdateOutputWindow?.Invoke(updateOutputWindowEventArgs); + hasGeneratedReport = true; } + private void UpdateUI(List coverageLines, string reportHtml) { CoverageLines = coverageLines; UpdateMarginTags?.Invoke(new UpdateMarginTagsEventArgs()); + if (reportHtml == null) + { + reportHtml = reportGeneratorUtil.BlankReport(true); + } RaiseUpdateOutputWindow(reportHtml); } @@ -159,7 +231,7 @@ private void UpdateUI(List coverageLines, string reportHtml) List coverageLines = null; string processedReport = null; - + var result = await reportGeneratorUtil.GenerateAsync(coverOutputFiles,reportOutputFolder, true); if (result.Success) @@ -191,29 +263,42 @@ private async System.Threading.Tasks.Task PrepareCoverageProjectsAsync(List coverageLines, string reportHtml)> t) + private async System.Threading.Tasks.Task ReloadCoverageTaskContinuationAsync(System.Threading.Tasks.Task<(List coverageLines, string reportHtml)> t) { switch (t.Status) { case System.Threading.Tasks.TaskStatus.Canceled: LogReloadCoverageStatus(ReloadCoverageStatus.Cancelled); + await reportGeneratorUtil.LogCoverageProcessAsync("Coverage cancelled"); break; case System.Threading.Tasks.TaskStatus.Faulted: LogReloadCoverageStatus(ReloadCoverageStatus.Error); logger.Log(t.Exception.InnerExceptions[0]); - ClearUI(); + await reportGeneratorUtil.LogCoverageProcessAsync(t.Exception.ToString()); break; case System.Threading.Tasks.TaskStatus.RanToCompletion: LogReloadCoverageStatus(ReloadCoverageStatus.Done); -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits +#pragma warning disable VSTHRD103 // Call async methods when in an async method UpdateUI(t.Result.coverageLines, t.Result.reportHtml); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits +#pragma warning restore VSTHRD103 // Call async methods when in an async method break; } + await reportGeneratorUtil.EndOfCoverageRunAsync(); } @@ -229,6 +314,7 @@ private async System.Threading.Tasks.Task PollInitializedStatusAsync(Cancellatio return; case InitializeStatus.Initializing: + await reportGeneratorUtil.LogCoverageProcessAsync("Initializing"); LogReloadCoverageStatus(ReloadCoverageStatus.Initializing); await System.Threading.Tasks.Task.Delay(InitializeWait); break; @@ -249,6 +335,7 @@ public void ReloadCoverage(Func { - ReloadCoverageTaskContinuation(t); + _ = ReloadCoverageTaskContinuationAsync(t); }, System.Threading.Tasks.TaskScheduler.Default); } + public void ReadyForReport() + { + ClearOutputWindow(false); + } } } \ No newline at end of file diff --git a/SharedProject/Core/ICoverageUtilManager.cs b/SharedProject/Core/ICoverageUtilManager.cs index 95834ee2..0da7268b 100644 --- a/SharedProject/Core/ICoverageUtilManager.cs +++ b/SharedProject/Core/ICoverageUtilManager.cs @@ -7,5 +7,6 @@ internal interface ICoverageUtilManager { void Initialize(string appDataFolder); Task RunCoverageAsync(ICoverageProject project, bool throwError = false); + string CoverageToolName(ICoverageProject project); } } diff --git a/SharedProject/Core/IFCCEngine.cs b/SharedProject/Core/IFCCEngine.cs index b1d22a7d..37a72172 100644 --- a/SharedProject/Core/IFCCEngine.cs +++ b/SharedProject/Core/IFCCEngine.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Windows; using FineCodeCoverage.Engine.Model; using FineCodeCoverage.Impl; +using FineCodeCoverage.Output; namespace FineCodeCoverage.Engine { @@ -13,8 +15,14 @@ internal interface IFCCEngine void Initialize(IInitializeStatusProvider initializeStatusProvider); void StopCoverage(); void ReloadCoverage(Func>> coverageRequestCallback); + + DpiScale Dpi { get; set; } + void ClearUI(); List CoverageLines { get; } + FontDetails EnvironmentFontDetails { get; set; } + + void ReadyForReport(); } } \ No newline at end of file diff --git a/SharedProject/Core/Model/CoverageProject.cs b/SharedProject/Core/Model/CoverageProject.cs index a0d81fec..144286fa 100644 --- a/SharedProject/Core/Model/CoverageProject.cs +++ b/SharedProject/Core/Model/CoverageProject.cs @@ -380,12 +380,13 @@ public async System.Threading.Tasks.Task StepAsync(string stepName, Func PrepareForCoverageAsync() { EnsureDirectories(); CleanFCCDirectory(); - SynchronizeBuildOutput(); + var synchronizationDetails = SynchronizeBuildOutput(); await SetIncludedExcludedReferencedProjectsAsync(); + return synchronizationDetails; } private async System.Threading.Tasks.Task SetIncludedExcludedReferencedProjectsAsync() @@ -606,10 +607,17 @@ private void CleanFCCDirectory() }); } - private void SynchronizeBuildOutput() + private CoverageProjectFileSynchronizationDetails SynchronizeBuildOutput() { - fileSynchronizationUtil.Synchronize(ProjectOutputFolder, BuildOutputPath, fccFolderName); + var start = DateTime.Now; + var logs = fileSynchronizationUtil.Synchronize(ProjectOutputFolder, BuildOutputPath, fccFolderName); + var duration = DateTime.Now - start; TestDllFile = Path.Combine(BuildOutputPath, Path.GetFileName(TestDllFile)); + return new CoverageProjectFileSynchronizationDetails + { + Logs = logs, + Duration = duration + }; } } diff --git a/SharedProject/Core/Model/CoverageProjectFileSynchronizationDetails.cs b/SharedProject/Core/Model/CoverageProjectFileSynchronizationDetails.cs new file mode 100644 index 00000000..f65e7ca2 --- /dev/null +++ b/SharedProject/Core/Model/CoverageProjectFileSynchronizationDetails.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FineCodeCoverage.Engine.Model +{ + internal class CoverageProjectFileSynchronizationDetails + { + public List Logs { get; set; } = new List(); + public TimeSpan Duration { get; set; } + } +} diff --git a/SharedProject/Core/Model/ICoverageProject.cs b/SharedProject/Core/Model/ICoverageProject.cs index d8ba24fa..40e4649a 100644 --- a/SharedProject/Core/Model/ICoverageProject.cs +++ b/SharedProject/Core/Model/ICoverageProject.cs @@ -28,6 +28,6 @@ internal interface ICoverageProject bool IsDotNetSdkStyle(); Task StepAsync(string stepName, Func action); - Task PrepareForCoverageAsync(); + Task PrepareForCoverageAsync(); } } \ No newline at end of file diff --git a/SharedProject/Core/ReportGenerator/IReportColours.cs b/SharedProject/Core/ReportGenerator/IReportColours.cs index d18aa6f2..1798e0f7 100644 --- a/SharedProject/Core/ReportGenerator/IReportColours.cs +++ b/SharedProject/Core/ReportGenerator/IReportColours.cs @@ -58,6 +58,22 @@ internal interface IReportColours Color SliderRightColour { get; } Color SliderThumbColour { get; } + + Color ButtonBorderColour { get; } + Color ButtonBorderDisabledColour { get; } + Color ButtonBorderFocusedColour { get; } + Color ButtonBorderHoverColour { get; } + Color ButtonBorderPressedColour { get; } + Color ButtonColour { get; } + Color ButtonDisabledColour { get; } + Color ButtonFocusedColour { get; } + Color ButtonHoverColour { get; } + Color ButtonPressedColour { get; } + Color ButtonTextColour { get; } + Color ButtonDisabledTextColour { get; } + Color ButtonFocusedTextColour { get; } + Color ButtonHoverTextColour { get; } + Color ButtonPressedTextColour { get; } } } diff --git a/SharedProject/Core/ReportGenerator/JsThemeStyling.cs b/SharedProject/Core/ReportGenerator/JsThemeStyling.cs index 71ff2cee..e3117268 100644 --- a/SharedProject/Core/ReportGenerator/JsThemeStyling.cs +++ b/SharedProject/Core/ReportGenerator/JsThemeStyling.cs @@ -39,6 +39,22 @@ public class JsThemeStyling public string SliderLeftColour; public string SliderRightColour; public string SliderThumbColour; + + public string ButtonBorderColour; + public string ButtonBorderDisabledColour; + public string ButtonBorderFocusedColour; + public string ButtonBorderHoverColour; + public string ButtonBorderPressedColour; + public string ButtonColour; + public string ButtonDisabledColour; + public string ButtonFocusedColour; + public string ButtonHoverColour; + public string ButtonPressedColour; + public string ButtonTextColour; + public string ButtonDisabledTextColour; + public string ButtonFocusedTextColour; + public string ButtonHoverTextColour; + public string ButtonPressedTextColour; //#pragma warning restore SA1401 // Fields should be private //#pragma warning restore IDE0079 // Remove unnecessary suppression } diff --git a/SharedProject/Core/ReportGenerator/ReportColours.cs b/SharedProject/Core/ReportGenerator/ReportColours.cs index 6a7710ce..09c23940 100644 --- a/SharedProject/Core/ReportGenerator/ReportColours.cs +++ b/SharedProject/Core/ReportGenerator/ReportColours.cs @@ -58,6 +58,36 @@ internal class ReportColours : IReportColours public Color SliderRightColour { get; set; } public Color SliderThumbColour { get; set; } + + public Color ButtonBorderColour { get; set; } + + public Color ButtonBorderDisabledColour { get; set; } + + public Color ButtonBorderFocusedColour { get; set; } + + public Color ButtonBorderHoverColour { get; set; } + + public Color ButtonBorderPressedColour { get; set; } + + public Color ButtonColour { get; set; } + + public Color ButtonDisabledColour { get; set; } + + public Color ButtonFocusedColour { get; set; } + + public Color ButtonHoverColour { get; set; } + + public Color ButtonPressedColour { get; set; } + + public Color ButtonTextColour { get; set; } + + public Color ButtonDisabledTextColour { get; set; } + + public Color ButtonFocusedTextColour { get; set; } + + public Color ButtonHoverTextColour { get; set; } + + public Color ButtonPressedTextColour { get; set; } } } diff --git a/SharedProject/Core/ReportGenerator/ReportColoursProvider.cs b/SharedProject/Core/ReportGenerator/ReportColoursProvider.cs index 95dd7512..dbce3218 100644 --- a/SharedProject/Core/ReportGenerator/ReportColoursProvider.cs +++ b/SharedProject/Core/ReportGenerator/ReportColoursProvider.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.Shell; using System; using System.ComponentModel.Composition; +using System.Drawing; using System.Reflection; namespace FineCodeCoverage.Engine.ReportGenerator @@ -19,6 +20,8 @@ static ReportColoursProvider() propertyInfos = typeof(ReportColours).GetProperties(); } + private ReportColours lastReportColours; + [ImportingConstructor] public ReportColoursProvider( [Import(typeof(SVsServiceProvider))] @@ -26,21 +29,48 @@ public ReportColoursProvider( IThemeResourceKeyProvider themeResourceKeyProvider ) { + this.themeResourceKeyProvider = themeResourceKeyProvider; var colorThemeService = serviceProvider.GetService(typeof(SVsColorThemeService)); + lastReportColours = GetReportColours(); VSColorTheme.ThemeChanged += VSColorTheme_ThemeChanged; - this.themeResourceKeyProvider = themeResourceKeyProvider; } private void VSColorTheme_ThemeChanged(ThemeChangedEventArgs e) { - var newColours = GetColours(); - ColoursChanged?.Invoke(this, newColours); + var newColours = GetReportColours(); + if(lastReportColours == null || !SameColours(lastReportColours, newColours)) + { + lastReportColours = newColours; + ColoursChanged?.Invoke(this, newColours); + } + + } + + private static bool SameColours(ReportColours oldColours, ReportColours newColours) + { + var same = true; + foreach(var propertyInfo in propertyInfos) + { + var oldColor = (Color)propertyInfo.GetValue(oldColours); + var newColor = (Color)propertyInfo.GetValue(newColours); + same = oldColor.ToString() == newColor.ToString(); + if (!same) + { + break; + } + } + return same; } public IReportColours GetColours() + { + return GetReportColours(); + } + + private ReportColours GetReportColours() { var reportColours = new ReportColours(); - foreach(var propertyInfo in propertyInfos) + foreach (var propertyInfo in propertyInfos) { var color = VSColorTheme.GetThemedColor(themeResourceKeyProvider.Provide(propertyInfo.Name)); propertyInfo.SetValue(reportColours, color); diff --git a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs index ee7a870b..a8f5b07c 100644 --- a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -5,27 +5,37 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Windows; using ExCSS; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Options; using FineCodeCoverage.Output; using Fizzler.Systems.HtmlAgilityPack; using HtmlAgilityPack; +using Microsoft.VisualStudio.Shell; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using ReportGeneratorPlugins; namespace FineCodeCoverage.Engine.ReportGenerator { + interface IReportGeneratorUtil { - void Initialize(string appDataFolder); + DpiScale DpiScale { get; set; } + + void Initialize(string appDataFolder); string ProcessUnifiedHtml(string htmlForProcessing,string reportOutputFolder); Task GenerateAsync(IEnumerable coverOutputFiles,string reportOutputFolder, bool throwError = false); + string BlankReport(bool withHistory); + System.Threading.Tasks.Task LogCoverageProcessAsync(string message); + System.Threading.Tasks.Task EndOfCoverageRunAsync(); + void UpdateReportWithDpiFontChanges(); - } + FontDetails EnvironmentFontDetails { get; set; } + } - internal class ReportGeneratorResult + internal class ReportGeneratorResult { public string UnifiedHtml { get; set; } public string UnifiedXmlFile { get; set; } @@ -43,17 +53,24 @@ internal partial class ReportGeneratorUtil : IReportGeneratorUtil private readonly IReportColoursProvider reportColoursProvider; private readonly IFileUtil fileUtil; private readonly IAppOptionsProvider appOptionsProvider; - private const string zipPrefix = "reportGenerator"; + private readonly IResourceProvider resourceProvider; + private readonly IShowFCCOutputPane showFCCOutputPane; + private const string zipPrefix = "reportGenerator"; private const string zipDirectoryName = "reportGenerator"; private const string ThemeChangedJSFunctionName = "themeChanged"; + private const string CoverageLogJSFunctionName = "coverageLog"; + private const string CoverageLogTabName = "Coverage Log"; + private const string ShowFCCWorkingJSFunctionName = "showFCCWorking"; + private const string FontChangedJSFunctionName = "fontChanged"; private readonly Base64ReportImage plusBase64ReportImage = new Base64ReportImage(".icon-plus", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNjAwIDczNnYxOTJxMCA0MC0yOCA2OHQtNjggMjhoLTQxNnY0MTZxMCA0MC0yOCA2OHQtNjggMjhoLTE5MnEtNDAgMC02OC0yOHQtMjgtNjh2LTQxNmgtNDE2cS00MCAwLTY4LTI4dC0yOC02OHYtMTkycTAtNDAgMjgtNjh0NjgtMjhoNDE2di00MTZxMC00MCAyOC02OHQ2OC0yOGgxOTJxNDAgMCA2OCAyOHQyOCA2OHY0MTZoNDE2cTQwIDAgNjggMjh0MjggNjh6Ii8+PC9zdmc+"); private readonly Base64ReportImage minusBase64ReportImage = new Base64ReportImage(".icon-minus", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgd2lkdGg9IjE3OTIiIGhlaWdodD0iMTc5MiIgdmlld0JveD0iMCAwIDE3OTIgMTc5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTYwMCA3MzZ2MTkycTAgNDAtMjggNjh0LTY4IDI4aC0xMjE2cS00MCAwLTY4LTI4dC0yOC02OHYtMTkycTAtNDAgMjgtNjh0NjgtMjhoMTIxNnE0MCAwIDY4IDI4dDI4IDY4eiIvPjwvc3ZnPg=="); private readonly Base64ReportImage downActiveBase64ReportImage = new Base64ReportImage(".icon-down-dir_active", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgd2lkdGg9IjE3OTIiIGhlaWdodD0iMTc5MiIgdmlld0JveD0iMCAwIDE3OTIgMTc5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMDA3OEQ0IiBkPSJNMTQwOCA3MDRxMCAyNi0xOSA0NWwtNDQ4IDQ0OHEtMTkgMTktNDUgMTl0LTQ1LTE5bC00NDgtNDQ4cS0xOS0xOS0xOS00NXQxOS00NSA0NS0xOWg4OTZxMjYgMCA0NSAxOXQxOSA0NXoiLz48L3N2Zz4="); private readonly Base64ReportImage downInactiveBase64ReportImage = new Base64ReportImage(".icon-down-dir", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNDA4IDcwNHEwIDI2LTE5IDQ1bC00NDggNDQ4cS0xOSAxOS00NSAxOXQtNDUtMTlsLTQ0OC00NDhxLTE5LTE5LTE5LTQ1dDE5LTQ1IDQ1LTE5aDg5NnEyNiAwIDQ1IDE5dDE5IDQ1eiIvPjwvc3ZnPg=="); private readonly Base64ReportImage upActiveBase64ReportImage = new Base64ReportImage(".icon-up-dir_active", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgd2lkdGg9IjE3OTIiIGhlaWdodD0iMTc5MiIgdmlld0JveD0iMCAwIDE3OTIgMTc5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMDA3OEQ0IiBkPSJNMTQwOCAxMjE2cTAgMjYtMTkgNDV0LTQ1IDE5aC04OTZxLTI2IDAtNDUtMTl0LTE5LTQ1IDE5LTQ1bDQ0OC00NDhxMTktMTkgNDUtMTl0NDUgMTlsNDQ4IDQ0OHExOSAxOSAxOSA0NXoiLz48L3N2Zz4="); - private readonly IScriptInvoker scriptInvoker; - private IReportColours reportColours; + private readonly IScriptManager scriptManager; + + private IReportColours reportColours; private JsThemeStyling jsReportColours; private IReportColours ReportColours { @@ -65,15 +82,15 @@ private IReportColours ReportColours } } private readonly bool showBranchCoverage = true; + private List logs = new List(); public string ReportGeneratorExePath { get; private set; } + public DpiScale DpiScale { get; set; } + public FontDetails EnvironmentFontDetails { get; set; } + + private string FontSize => $"{EnvironmentFontDetails.Size * DpiScale.DpiScaleX}px"; + private string FontName => EnvironmentFontDetails.Family.Source; - - static ReportGeneratorUtil() - { - - - } [ImportingConstructor] public ReportGeneratorUtil( IAssemblyUtil assemblyUtil, @@ -84,7 +101,9 @@ public ReportGeneratorUtil( IFileUtil fileUtil, IAppOptionsProvider appOptionsProvider, IReportColoursProvider reportColoursProvider, - IScriptInvoker scriptInvoker + IScriptManager scriptManager, + IResourceProvider resourceProvider, + IShowFCCOutputPane showFCCOutputPane ) { this.fileUtil = fileUtil; @@ -96,8 +115,22 @@ IScriptInvoker scriptInvoker this.toolZipProvider = toolZipProvider; this.reportColoursProvider = reportColoursProvider; this.reportColoursProvider.ColoursChanged += ReportColoursProvider_ColoursChanged; - this.scriptInvoker = scriptInvoker; - } + this.scriptManager = scriptManager; + this.resourceProvider = resourceProvider; + this.showFCCOutputPane = showFCCOutputPane; + scriptManager.ClearFCCWindowLogsEvent += ScriptManager_ClearFCCWindowLogsEvent; + scriptManager.ShowFCCOutputPaneEvent += ScriptManager_ShowFCCOutputPaneEvent; + } + + private async void ScriptManager_ShowFCCOutputPaneEvent(object sender, EventArgs e) + { + await showFCCOutputPane.ShowAsync(); + } + + private void ScriptManager_ClearFCCWindowLogsEvent(object sender, EventArgs e) + { + logs.Clear(); + } public void Initialize(string appDataFolder) { @@ -145,7 +178,7 @@ async Task run(string outputReportType, string inputReports) } logger.Log($"{title} Arguments [reporttype:{outputReportType}] {Environment.NewLine}{string.Join($"{Environment.NewLine}", reportTypeSettings)}"); - + var result = await processUtil .ExecuteAsync(new ExecuteRequest { @@ -179,18 +212,29 @@ async Task run(string outputReportType, string inputReports) var reportGeneratorResult = new ReportGeneratorResult { Success = false, UnifiedHtml = null, UnifiedXmlFile = unifiedXmlFile }; + var startTime = DateTime.Now; + await LogCoverageProcessAsync("Generating cobertura report"); var coberturaResult = await run("Cobertura", string.Join(";", coverOutputFiles)); + var duration = DateTime.Now - startTime; if (coberturaResult) { + var coberturaDurationMesage = $"Cobertura report generation duration - {duration}"; + await LogCoverageProcessAsync(coberturaDurationMesage); // result output includes duration for normal log + + startTime = DateTime.Now; + await LogCoverageProcessAsync("Generating html report"); var htmlResult = await run("HtmlInline_AzurePipelines", unifiedXmlFile); + duration = DateTime.Now - startTime; if (htmlResult) { + var htmlReportDurationMessage = $"Html report generation duration - {duration}"; + await LogCoverageProcessAsync(htmlReportDurationMessage); // result output includes duration for normal log reportGeneratorResult.UnifiedHtml = fileUtil.ReadAllText(unifiedHtmlFile); reportGeneratorResult.Success = true; } - } + } return reportGeneratorResult; @@ -836,6 +880,7 @@ private string HackGroupingToAllowAll(int groupingLevel) public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFolder) { + var previousLogMessages = $"[{string.Join(",",logs.Select(l => $"'{l}'"))}]"; var appOptions = appOptionsProvider.Get(); var namespacedClasses = appOptions.NamespacedClasses; ReportColours = reportColoursProvider.GetColours(); @@ -858,7 +903,7 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo htmlForProcessing = null; doc.DocumentNode.QuerySelectorAll(".footer").ToList().ForEach(x => x.SetAttributeValue("style", "display:none")); - doc.DocumentNode.QuerySelectorAll(".container").ToList().ForEach(x => x.SetAttributeValue("style", "margin:0;padding:0;border:0")); + doc.DocumentNode.QuerySelectorAll(".container").ToList().ForEach(x => x.SetAttributeValue("style", "margin:0;padding:25px;border:0")); doc.DocumentNode.QuerySelectorAll(".containerleft").ToList().ForEach(x => x.SetAttributeValue("style", "margin:0;padding:0;border:0")); doc.DocumentNode.QuerySelectorAll(".containerleft > h1 , .containerleft > p").ToList().ForEach(x => x.SetAttributeValue("style", "display:none")); @@ -956,15 +1001,18 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo var sliderThumbColour = jsReportColours.SliderThumbColour; htmlSb.Replace("", $@"