From 6bef01ca40a09a57e82fb1aeb723ed49a79fa2fc Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Sat, 15 May 2021 18:48:40 +0100 Subject: [PATCH 1/3] output directory from AllProjectsCoverageOutputFolder tests to come... --- .../Core/Cobertura/CoberturaUtil.cs | 9 +- .../Core/Cobertura/ICoberturaUtil.cs | 2 +- .../Core/CoverageToolOutputManager.cs | 80 ++++++++++++++ FineCodeCoverage/Core/FCCEngine.cs | 54 ++++----- .../Core/ICoverageToolOutputManager.cs | 15 +++ .../Core/Model/CoverageProject.cs | 26 ++--- .../Core/Model/ICoverageProject.cs | 4 +- .../ReportGenerator/ReportGeneratorUtil.cs | 60 +++++----- FineCodeCoverage/Core/Utilities/FileUtil.cs | 46 +++++++- FineCodeCoverage/Core/Utilities/IFileUtil.cs | 5 + FineCodeCoverage/FineCodeCoverage.csproj | 2 + .../CoverageToolOutputManager_Tests.cs | 35 ++++++ FineCodeCoverageTests/FCCEngine_Tests.cs | 103 ++++++++++-------- .../FineCodeCoverageTests.csproj | 1 + 14 files changed, 311 insertions(+), 131 deletions(-) create mode 100644 FineCodeCoverage/Core/CoverageToolOutputManager.cs create mode 100644 FineCodeCoverage/Core/ICoverageToolOutputManager.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs diff --git a/FineCodeCoverage/Core/Cobertura/CoberturaUtil.cs b/FineCodeCoverage/Core/Cobertura/CoberturaUtil.cs index 95b0c6d0..e6cbe363 100644 --- a/FineCodeCoverage/Core/Cobertura/CoberturaUtil.cs +++ b/FineCodeCoverage/Core/Cobertura/CoberturaUtil.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using FineCodeCoverage.Engine.Model; using System.ComponentModel.Composition; +using System.IO; namespace FineCodeCoverage.Engine.Cobertura { @@ -16,9 +17,9 @@ internal class CoberturaUtil:ICoberturaUtil private CoverageReport coverageReport; public List CoverageLines { get; private set; } - private CoverageReport LoadReportFile(string inputFilePath) + private CoverageReport LoadReport(string xml) { - using (var reader = XmlReader.Create(inputFilePath, READER_SETTINGS)) + using (var reader = XmlReader.Create(new StringReader(xml), READER_SETTINGS)) { var report = (CoverageReport)SERIALIZER.Deserialize(reader); return report; @@ -67,11 +68,11 @@ private CoverageReport LoadReportFile(string inputFilePath) // return jsonText; //} - public void ProcessCoberturaXmlFile(string xmlFilePath) + public void ProcessCoberturaXml(string xml) { CoverageLines = new List(); - coverageReport = LoadReportFile(xmlFilePath); + coverageReport = LoadReport(xml); foreach (var package in coverageReport.Packages.Package) { diff --git a/FineCodeCoverage/Core/Cobertura/ICoberturaUtil.cs b/FineCodeCoverage/Core/Cobertura/ICoberturaUtil.cs index 68655d4a..670fb2d2 100644 --- a/FineCodeCoverage/Core/Cobertura/ICoberturaUtil.cs +++ b/FineCodeCoverage/Core/Cobertura/ICoberturaUtil.cs @@ -7,7 +7,7 @@ interface ICoberturaUtil { List CoverageLines { get; } - void ProcessCoberturaXmlFile(string xmlFilePath); + void ProcessCoberturaXml(string xml); string[] GetSourceFiles(string assemblyName, string qualifiedClassName, int file); } } \ No newline at end of file diff --git a/FineCodeCoverage/Core/CoverageToolOutputManager.cs b/FineCodeCoverage/Core/CoverageToolOutputManager.cs new file mode 100644 index 00000000..e590e51c --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutputManager.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Engine +{ + [Export(typeof(ICoverageToolOutputManager))] + internal class CoverageToolOutputManager : ICoverageToolOutputManager + { + private readonly ILogger logger; + private readonly IFileUtil fileUtil; + private const string unifiedHtmlFileName = "index.html"; + private const string unifiedXmlFileName = "Cobertura.xml"; + private const string processedHtmlFileName = "index-processed.html"; + private const string projectCoverageToolOutputFolderName = "coverage-tool-output"; + private string outputFolderForAllProjects; + private List coverageProjects; + + [ImportingConstructor] + public CoverageToolOutputManager(IFileUtil fileUtil, ILogger logger) + { + this.logger = logger; + this.fileUtil = fileUtil; + } + + public void SetProjectCoverageOutputFolder(List coverageProjects) + { + this.coverageProjects = coverageProjects; + DetermineOutputFolderForAllProjects(); + if(outputFolderForAllProjects == null) + { + foreach(var coverageProject in coverageProjects) + { + coverageProject.CoverageOutputFolder = Path.Combine(coverageProject.FCCOutputFolder, projectCoverageToolOutputFolderName); + } + } + else + { + fileUtil.TryEmptyDirectory(outputFolderForAllProjects); + foreach (var coverageProject in coverageProjects) + { + coverageProject.CoverageOutputFolder = Path.Combine(outputFolderForAllProjects, coverageProject.ProjectName); + } + } + } + + public void SetReportOutput(string unifiedHtml, string processedReport, string unifiedXml) + { + var outputFolder = outputFolderForAllProjects ?? coverageProjects[0].CoverageOutputFolder; + + fileUtil.WriteAllText(Path.Combine(outputFolder, unifiedHtmlFileName), unifiedHtml); + fileUtil.WriteAllText(Path.Combine(outputFolder, processedHtmlFileName), processedReport); + fileUtil.WriteAllText(Path.Combine(outputFolder, unifiedXmlFileName), unifiedXml); + } + + private void DetermineOutputFolderForAllProjects() + { + outputFolderForAllProjects = null; + var coverageProjectWithAllProjectsCoverageOutputFolder = coverageProjects.FirstOrDefault(cp => cp.AllProjectsCoverageOutputFolder != null); + if(coverageProjectWithAllProjectsCoverageOutputFolder != null) + { + var allProjectsCoverageOutputFolder = fileUtil.EnsureAbsolute( + coverageProjectWithAllProjectsCoverageOutputFolder.AllProjectsCoverageOutputFolder, + fileUtil.ParentDirectoryPath(coverageProjectWithAllProjectsCoverageOutputFolder.ProjectFile) + ); + + outputFolderForAllProjects = allProjectsCoverageOutputFolder; + logger.Log($"Outputting coverage files to - {outputFolderForAllProjects}"); + return; + } + + + logger.Log($"Outputting coverage files in project output folder"); + + } + } +} diff --git a/FineCodeCoverage/Core/FCCEngine.cs b/FineCodeCoverage/Core/FCCEngine.cs index 7286db85..0d17e232 100644 --- a/FineCodeCoverage/Core/FCCEngine.cs +++ b/FineCodeCoverage/Core/FCCEngine.cs @@ -22,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."; - internal const string errorReadingReportGeneratorOutputMessage = "error reading report generator output"; private readonly object colorThemeService; private string CurrentTheme => $"{((dynamic)colorThemeService)?.CurrentTheme?.Name}".Trim(); @@ -44,6 +43,7 @@ internal class FCCEngine : IFCCEngine private readonly IAppDataFolder appDataFolder; private readonly IServiceProvider serviceProvider; private IInitializeStatusProvider initializeStatusProvider; + private readonly ICoverageToolOutputManager coverageOutputManager; internal System.Threading.Tasks.Task reloadCoverageTask; [ImportingConstructor] @@ -56,10 +56,12 @@ public FCCEngine( IAppOptionsProvider appOptionsProvider, ILogger logger, IAppDataFolder appDataFolder, + ICoverageToolOutputManager coverageOutputManager, [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider ) { + this.coverageOutputManager = coverageOutputManager; this.coverageUtilManager = coverageUtilManager; this.coberturaUtil = coberturaUtil; this.msTestPlatformUtil = msTestPlatformUtil; @@ -142,32 +144,16 @@ private async System.Threading.Tasks.Task RunCoverageAsync(List coverageLines, string reportFilePath) + private void UpdateUI(List coverageLines, string reportHtml) { CoverageLines = coverageLines; UpdateMarginTags?.Invoke(new UpdateMarginTagsEventArgs()); - RaiseUpdateOutputWindow(reportFilePath); + RaiseUpdateOutputWindow(reportHtml); } private async System.Threading.Tasks.Task<(List coverageLines,string reportFilePath)> RunAndProcessReportAsync(string[] coverOutputFiles,CancellationToken cancellationToken) @@ -175,7 +161,7 @@ private void UpdateUI(List coverageLines, string reportFilePath) cancellationToken.ThrowIfCancellationRequested(); List coverageLines = null; - string reportFilePath = null; + string processedReport = null; var darkMode = CurrentTheme.Equals("Dark", StringComparison.OrdinalIgnoreCase); @@ -183,13 +169,13 @@ private void UpdateUI(List coverageLines, string reportFilePath) if (result.Success) { - coberturaUtil.ProcessCoberturaXmlFile(result.UnifiedXmlFile); + coberturaUtil.ProcessCoberturaXml(result.UnifiedXml); coverageLines = coberturaUtil.CoverageLines; - reportGeneratorUtil.ProcessUnifiedHtmlFile(result.UnifiedHtmlFile, darkMode, out var htmlFilePath); - reportFilePath = htmlFilePath; + processedReport = reportGeneratorUtil.ProcessUnifiedHtml(result.UnifiedHtml, darkMode); + coverageOutputManager.SetReportOutput(result.UnifiedHtml, processedReport, result.UnifiedXml); } - return (coverageLines, reportFilePath); + return (coverageLines, processedReport); } private async System.Threading.Tasks.Task PrepareCoverageProjectsAsync(List coverageProjects, CancellationToken cancellationToken) @@ -213,7 +199,7 @@ private async System.Threading.Tasks.Task PrepareCoverageProjectsAsync(List coverageLines, string reportFilePath)> t) + private void ReloadCoverageTaskContinuation(System.Threading.Tasks.Task<(List coverageLines, string reportHtml)> t) { switch (t.Status) { @@ -228,7 +214,7 @@ private void ReloadCoverageTaskContinuation(System.Threading.Tasks.Task<(List { List coverageLines = null; - string reportFilePath = null; + string reportHtml = null; await PollInitializedStatusAsync(cancellationToken); @@ -271,16 +257,18 @@ public void ReloadCoverage(Func diff --git a/FineCodeCoverage/Core/ICoverageToolOutputManager.cs b/FineCodeCoverage/Core/ICoverageToolOutputManager.cs new file mode 100644 index 00000000..dc5e59f4 --- /dev/null +++ b/FineCodeCoverage/Core/ICoverageToolOutputManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Engine +{ + internal interface ICoverageToolOutputManager + { + void SetProjectCoverageOutputFolder(List coverageProjects); + void SetReportOutput(string unifiedHtml, string processedReport, string unifiedXml); + } +} diff --git a/FineCodeCoverage/Core/Model/CoverageProject.cs b/FineCodeCoverage/Core/Model/CoverageProject.cs index f5a8c1a6..3a79575c 100644 --- a/FineCodeCoverage/Core/Model/CoverageProject.cs +++ b/FineCodeCoverage/Core/Model/CoverageProject.cs @@ -24,10 +24,9 @@ internal class CoverageProject : ICoverageProject private readonly bool canUseMsBuildWorkspace; private XElement projectFileXElement; private IAppOptions settings; - private string fccPath; private readonly string fccFolderName = "fine-code-coverage"; private readonly string buildOutputFolderName = "build-output"; - private string buildOutputPath; + private string BuildOutputPath => Path.Combine(FCCOutputFolder, buildOutputFolderName); private readonly string coverageToolOutputFolderName = "coverage-tool-output"; public CoverageProject(IAppOptionsProvider appOptionsProvider, IFileSynchronizationUtil fileSynchronizationUtil, ILogger logger, DTE dte, bool canUseMsBuildWorkspace) @@ -39,6 +38,9 @@ public CoverageProject(IAppOptionsProvider appOptionsProvider, IFileSynchronizat this.canUseMsBuildWorkspace = canUseMsBuildWorkspace; } + public string AllProjectsCoverageOutputFolder => ProjectFileXElement.XPathSelectElement($"/PropertyGroup/AllProjectsCoverageOutputFolder")?.Value; + + public string FCCOutputFolder => Path.Combine(ProjectOutputFolder, fccFolderName); public bool IsDotNetSdkStyle() { return ProjectFileXElement @@ -126,7 +128,7 @@ public bool IsDotNetSdkStyle() public bool HasFailed => !string.IsNullOrWhiteSpace(FailureStage) || !string.IsNullOrWhiteSpace(FailureDescription); public string ProjectFile { get; set; } public string ProjectName { get; set; } - public string CoverageOutputFile { get; set; } + public string CoverageOutputFile => Path.Combine(CoverageOutputFolder, $"{ProjectName}.coverage.xml"); private bool TypeMatch(Type type, params Type[] otherTypes) { @@ -356,7 +358,6 @@ public async System.Threading.Tasks.Task StepAsync(string stepName, Func> GetReferencedProjectsFromProjectFile return referencedProjectFiles.Select(referencedProjectProjectFile => new ReferencedProject(referencedProjectProjectFile)).ToList(); } - private void SetPaths() - { - fccPath = Path.Combine(ProjectOutputFolder, fccFolderName); - buildOutputPath = Path.Combine(fccPath, buildOutputFolderName); - CoverageOutputFolder = Path.Combine(fccPath, coverageToolOutputFolderName); - CoverageOutputFile = Path.Combine(CoverageOutputFolder, $"{ProjectName}.coverage.xml"); - } private void EnsureDirectories() { EnsureFccDirectory(); @@ -508,11 +502,11 @@ private void EnsureDirectories() } private void EnsureFccDirectory() { - CreateIfDoesNotExist(fccPath); + CreateIfDoesNotExist(FCCOutputFolder); } private void EnsureBuildOutputDirectory() { - CreateIfDoesNotExist(buildOutputPath); + CreateIfDoesNotExist(BuildOutputPath); } private void CreateIfDoesNotExist(string path) { @@ -546,7 +540,7 @@ private void EnsureEmptyOutputFolder() private void CleanDirectory() { var exclusions = new List { buildOutputFolderName, coverageToolOutputFolderName }; - var fccDirectory = new DirectoryInfo(fccPath); + var fccDirectory = new DirectoryInfo(FCCOutputFolder); fccDirectory.EnumerateFileSystemInfos().AsParallel().ForAll(fileOrDirectory => { @@ -570,8 +564,8 @@ private void CleanDirectory() } private void SynchronizeBuildOutput() { - fileSynchronizationUtil.Synchronize(ProjectOutputFolder, buildOutputPath, fccFolderName); - TestDllFile = Path.Combine(buildOutputPath, Path.GetFileName(TestDllFile)); + fileSynchronizationUtil.Synchronize(ProjectOutputFolder, BuildOutputPath, fccFolderName); + TestDllFile = Path.Combine(BuildOutputPath, Path.GetFileName(TestDllFile)); } } diff --git a/FineCodeCoverage/Core/Model/ICoverageProject.cs b/FineCodeCoverage/Core/Model/ICoverageProject.cs index 11a6fa5a..5c438c02 100644 --- a/FineCodeCoverage/Core/Model/ICoverageProject.cs +++ b/FineCodeCoverage/Core/Model/ICoverageProject.cs @@ -8,7 +8,9 @@ namespace FineCodeCoverage.Engine.Model { internal interface ICoverageProject { - string CoverageOutputFile { get; set; } + string AllProjectsCoverageOutputFolder { get; } + string FCCOutputFolder { get; } + string CoverageOutputFile { get; } string CoverageOutputFolder { get; set; } List ExcludedReferencedProjects { get; } string FailureDescription { get; set; } diff --git a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs index 7be19979..91294c07 100644 --- a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -17,15 +17,15 @@ namespace FineCodeCoverage.Engine.ReportGenerator interface IReportGeneratorUtil { void Initialize(string appDataFolder); - void ProcessUnifiedHtmlFile(string htmlFile, bool darkMode, out string coverageHtml); + string ProcessUnifiedHtml(string htmlForProcessing, bool darkMode); Task RunReportGeneratorAsync(IEnumerable coverOutputFiles, bool darkMode, bool throwError = false); } internal class ReportGeneratorResult { - public string UnifiedHtmlFile { get; set; } - public string UnifiedXmlFile { get; set; } + public string UnifiedHtml { get; set; } + public string UnifiedXml { get; set; } public bool Success { get; set; } } @@ -37,6 +37,7 @@ internal partial class ReportGeneratorUtil: IReportGeneratorUtil private readonly ILogger logger; private readonly IToolFolder toolFolder; private readonly IToolZipProvider toolZipProvider; + private readonly IFileUtil fileUtil; private const string zipPrefix = "reportGenerator"; private const string zipDirectoryName = "reportGenerator"; @@ -48,9 +49,11 @@ public ReportGeneratorUtil( IProcessUtil processUtil, ILogger logger, IToolFolder toolFolder, - IToolZipProvider toolZipProvider + IToolZipProvider toolZipProvider, + IFileUtil fileUtil ) { + this.fileUtil = fileUtil; this.assemblyUtil = assemblyUtil; this.processUtil = processUtil; this.logger = logger; @@ -68,16 +71,14 @@ public void Initialize(string appDataFolder) public async Task RunReportGeneratorAsync(IEnumerable coverOutputFiles, bool darkMode, bool throwError = false) { var title = "ReportGenerator Run"; - var outputFolder = Path.GetDirectoryName(coverOutputFiles.OrderBy(x => x).First()); // use location of first file to output reports + var tempDirectory = fileUtil.CreateTempDirectory(); - Directory.GetFiles(outputFolder, "*.htm*").ToList().ForEach(File.Delete); // delete html files if they exist - - var unifiedHtmlFile = Path.Combine(outputFolder, "index.html"); - var unifiedXmlFile = Path.Combine(outputFolder, "Cobertura.xml"); + var unifiedHtmlFile = Path.Combine(tempDirectory, "index.html"); + var unifiedXmlFile = Path.Combine(tempDirectory, "Cobertura.xml"); var reportGeneratorSettings = new List(); - reportGeneratorSettings.Add($@"""-targetdir:{outputFolder}"""); + reportGeneratorSettings.Add($@"""-targetdir:{tempDirectory}"""); async Task run(string outputReportType, string inputReports) { @@ -106,7 +107,7 @@ async Task run(string outputReportType, string inputReports) { FilePath = ReportGeneratorExePath, Arguments = string.Join(" ", reportTypeSettings), - WorkingDirectory = outputFolder + WorkingDirectory = tempDirectory }); @@ -129,37 +130,37 @@ async Task run(string outputReportType, string inputReports) return false; } - var reportGeneratorResult = new ReportGeneratorResult { Success = false, UnifiedHtmlFile = unifiedHtmlFile, UnifiedXmlFile = unifiedXmlFile }; + + var reportGeneratorResult = new ReportGeneratorResult { Success = false, UnifiedHtml = null, UnifiedXml = null }; + var coberturaResult = await run("Cobertura", string.Join(";", coverOutputFiles)); - if (!coberturaResult) + if (coberturaResult) { - return reportGeneratorResult; + var htmlResult = await run("HtmlInline_AzurePipelines", unifiedXmlFile); + if (htmlResult) + { + reportGeneratorResult.UnifiedXml = fileUtil.ReadAllText(unifiedXmlFile); + reportGeneratorResult.UnifiedHtml = fileUtil.ReadAllText(unifiedHtmlFile); + reportGeneratorResult.Success = true; + } + } - reportGeneratorResult.Success = await run("HtmlInline_AzurePipelines", unifiedXmlFile); return reportGeneratorResult; } - public void ProcessUnifiedHtmlFile(string htmlFile, bool darkMode, out string coverageHtml) + public string ProcessUnifiedHtml(string htmlForProcessing, bool darkMode) { - coverageHtml = assemblyUtil.RunInAssemblyResolvingContext(() => + return assemblyUtil.RunInAssemblyResolvingContext(() => { - // read [htmlFile] into memory - - var htmlFileContent = File.ReadAllText(htmlFile); - - var folder = Path.GetDirectoryName(htmlFile); - - // create and save doc util - var doc = new HtmlDocument(); doc.OptionFixNestedTags = true; doc.OptionAutoCloseOnEnd = true; - doc.LoadHtml(htmlFileContent); + doc.LoadHtml(htmlForProcessing); 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")); @@ -497,7 +498,7 @@ Risk Hotspots htmlSb.Replace("branchCoverageAvailable = true", "branchCoverageAvailable = false"); - var html = string.Join( + return string.Join( Environment.NewLine, htmlSb.ToString().Split('\r', '\n') .Select(line => @@ -563,11 +564,6 @@ Risk Hotspots return line; })); - // save - - var resultHtmlFile = Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(htmlFile)}-processed{Path.GetExtension(htmlFile)}"); - File.WriteAllText(resultHtmlFile, html); - return resultHtmlFile; }); } } diff --git a/FineCodeCoverage/Core/Utilities/FileUtil.cs b/FineCodeCoverage/Core/Utilities/FileUtil.cs index a31e29ad..3651fc20 100644 --- a/FineCodeCoverage/Core/Utilities/FileUtil.cs +++ b/FineCodeCoverage/Core/Utilities/FileUtil.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.Composition; +using System; +using System.ComponentModel.Composition; using System.IO; namespace FineCodeCoverage.Core.Utilities @@ -6,14 +7,57 @@ namespace FineCodeCoverage.Core.Utilities [Export(typeof(IFileUtil))] internal class FileUtil : IFileUtil { + public string CreateTempDirectory() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + return tempDirectory; + } + + public bool DirectoryExists(string directory) + { + return Directory.Exists(directory); + } + + public string EnsureAbsolute(string directory, string possiblyRelativeTo) + { + if (!Path.IsPathRooted(directory)) + { + directory = Path.GetFullPath(Path.Combine(possiblyRelativeTo, directory)); + } + return directory; + } + + public string ParentDirectoryPath(string filePath) + { + return new FileInfo(filePath).Directory.FullName; + } + public string ReadAllText(string path) { return File.ReadAllText(path); } + public void TryEmptyDirectory(string directory) + { + DirectoryInfo directoryInfo = new DirectoryInfo(directory); + if (directoryInfo.Exists) + { + foreach (FileInfo file in directoryInfo.GetFiles()) + { + file.TryDelete(); + } + foreach (DirectoryInfo subDir in directoryInfo.GetDirectories()) + { + subDir.TryDelete(true); + } + } + } + public void WriteAllText(string path, string contents) { File.WriteAllText(path, contents); } + } } diff --git a/FineCodeCoverage/Core/Utilities/IFileUtil.cs b/FineCodeCoverage/Core/Utilities/IFileUtil.cs index 04bfc10c..efdde014 100644 --- a/FineCodeCoverage/Core/Utilities/IFileUtil.cs +++ b/FineCodeCoverage/Core/Utilities/IFileUtil.cs @@ -4,5 +4,10 @@ internal interface IFileUtil { string ReadAllText(string path); void WriteAllText(string path, string contents); + string CreateTempDirectory(); + bool DirectoryExists(string directory); + void TryEmptyDirectory(string directory); + string EnsureAbsolute(string directory, string possiblyRelativeTo); + string ParentDirectoryPath(string filePath); } } diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index 04b8611f..683dd33e 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -73,6 +73,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs b/FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs new file mode 100644 index 00000000..032dbeb4 --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMoq; +using FineCodeCoverage.Engine; +using NUnit.Framework; + +namespace FineCodeCoverageTests +{ + class CoverageToolOutputManager_Tests + { + private AutoMoqer mocker; + private CoverageToolOutputManager coverageToolOutputManager; + [SetUp] + public void SetUp() + { + mocker = new AutoMoqer(); + coverageToolOutputManager = mocker.Create(); + } + + [Test] + public void Should_Set_CoverageOutputFolder_To_Sub_Folder_Of_CoverageProject_FCCOutputFolder_For_All_When_Do_Not_Specify() + { + + } + + [Test] + public void Should_Set_CoverageOutputFolder_To_ProjectName_Sub_Folder_Of_First_Providing_AllProjectsCoverageOutputFolder() + { + + } + } +} diff --git a/FineCodeCoverageTests/FCCEngine_Tests.cs b/FineCodeCoverageTests/FCCEngine_Tests.cs index e34eb833..b33ea637 100644 --- a/FineCodeCoverageTests/FCCEngine_Tests.cs +++ b/FineCodeCoverageTests/FCCEngine_Tests.cs @@ -102,7 +102,6 @@ public class FCCEngine_ReloadCoverage_Tests { private AutoMoqer mocker; private FCCEngine fccEngine; - private string tempReportGeneratedHtml; private List updateMarginTagsEvents; private List> updateMarginTagsCoverageLines; private List updateOutputWindowEvents; @@ -116,7 +115,6 @@ public void SetUp() updateMarginTagsEvents = new List(); updateMarginTagsCoverageLines = new List>(); updateOutputWindowEvents = new List(); - tempReportGeneratedHtml = null; fccEngine.UpdateMarginTags += (UpdateMarginTagsEventArgs e) => { @@ -130,15 +128,6 @@ public void SetUp() }; } - [TearDown] - public void Delete_ReportGeneratedHtml() - { - if (tempReportGeneratedHtml != null) - { - File.Delete(tempReportGeneratedHtml); - } - } - [Test] public async Task Should_Log_Starting_When_Initialized() { @@ -279,8 +268,6 @@ public async Task Should_Process_ReportGenerator_Output_If_Success() { var passedProject = CreateSuitableProject(); - var unifiedXmlFile = "unified.xml"; - var unifiedHtmlFile = "unified.html"; var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => rg.RunReportGeneratorAsync( @@ -291,19 +278,44 @@ public async Task Should_Process_ReportGenerator_Output_If_Success() new ReportGeneratorResult { Success = true, - UnifiedXmlFile = unifiedXmlFile, - UnifiedHtmlFile = unifiedHtmlFile + UnifiedXml = "Unified xml", + UnifiedHtml = "Unified html" } ); - var _ = ""; - mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtmlFile(unifiedHtmlFile, It.IsAny(), out _)); + mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtml("Unified html", It.IsAny())); await ReloadInitializedCoverage(passedProject.Object); - mocker.Verify(coberturaUtil => coberturaUtil.ProcessCoberturaXmlFile(unifiedXmlFile)); + mocker.Verify(coberturaUtil => coberturaUtil.ProcessCoberturaXml("Unified xml")); mockReportGenerator.VerifyAll(); } + [Test] + public async Task Should_Set_Report_Output_If_Success() + { + var passedProject = CreateSuitableProject(); + + var mockReportGenerator = mocker.GetMock(); + mockReportGenerator.Setup(rg => + rg.RunReportGeneratorAsync( + It.IsAny>(), + It.IsAny(), + true).Result) + .Returns( + new ReportGeneratorResult + { + Success = true, + UnifiedXml = "Unified xml", + UnifiedHtml = "Unified html" + } + ); + + mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtml("Unified html", It.IsAny())).Returns("Processed html"); + + await ReloadInitializedCoverage(passedProject.Object); + mocker.Verify(coverageToolOutputManager => coverageToolOutputManager.SetReportOutput("Unified html","Processed html","Unified xml")); + } + [Test] public async Task Should_Not_Process_ReportGenerator_Output_If_Failure() { @@ -323,10 +335,32 @@ public async Task Should_Not_Process_ReportGenerator_Output_If_Failure() ); await ReloadInitializedCoverage(passedProject.Object); - mocker.Verify(coberturaUtil => coberturaUtil.ProcessCoberturaXmlFile(It.IsAny()), Times.Never()); + mocker.Verify(coberturaUtil => coberturaUtil.ProcessCoberturaXml(It.IsAny()), Times.Never()); } - + + [Test] + public async Task Should_Not_Set_Report_Output_If_Failure() + { + var passedProject = CreateSuitableProject(); + + var mockReportGenerator = mocker.GetMock(); + mockReportGenerator.Setup(rg => + rg.RunReportGeneratorAsync( + It.IsAny>(), + It.IsAny(), + true).Result) + .Returns( + new ReportGeneratorResult + { + Success = false, + } + ); + + await ReloadInitializedCoverage(passedProject.Object); + mocker.Verify(coverageToolOutputManager => coverageToolOutputManager.SetReportOutput(It.IsAny(), It.IsAny(), It.IsAny()),Times.Never()); + } + [Test] public async Task Should_Clear_UI_Then_Update_UI_When_ReloadCoverage_Completes_Fully() { @@ -355,21 +389,6 @@ public async Task Should_Clear_UI_When_ReloadCoverage_And_No_CoverageProjects() Assert.Null(updateOutputWindowEvents[1].HtmlContent); } - [Test] // CoverageLines will be present and tags added hence done - unlikely for this branch to occur - public async Task Should_Log_Done_When_Exception_Reading_Report_Html() - { - await ThrowReadingReportHtml(); - VerifyLogsReloadCoverageStatus(ReloadCoverageStatus.Done); - mocker.Verify(l => l.Log(FCCEngine.errorReadingReportGeneratorOutputMessage)); - } - - [Test] - public async Task Should_Log_Message_When_Exception_Reading_Report_Html() - { - await ThrowReadingReportHtml(); - mocker.Verify(l => l.Log(FCCEngine.errorReadingReportGeneratorOutputMessage)); - } - [Test] public async Task Should_Update_OutputWindow_With_Null_HtmlContent_When_Reading_Report_Html_Throws() { @@ -471,25 +490,23 @@ private void VerifyClearUIEvents(int eventNumber) private async Task<(string reportGeneratedHtmlContent, List updatedCoverageLines)> RunToCompletion(bool noCoverageProjects) { - var passedProject = CreateSuitableProject(); - + var coverageProject = CreateSuitableProject().Object; var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => rg.RunReportGeneratorAsync( - It.IsAny>(), + It.Is>(coverOutputFiles => coverOutputFiles.Count() == 1 && coverOutputFiles.First() == coverageProject.CoverageOutputFile), It.IsAny(), true).Result) .Returns( new ReportGeneratorResult { Success = true, + UnifiedHtml = "Unified" } ); var reportGeneratedHtmlContent = ""; - tempReportGeneratedHtml = Path.GetTempFileName(); - File.WriteAllText(tempReportGeneratedHtml, reportGeneratedHtmlContent); - mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtmlFile(It.IsAny(), It.IsAny(), out tempReportGeneratedHtml)); + mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtml("Unified", It.IsAny())).Returns(reportGeneratedHtmlContent); List coverageLines = new List() { new CoverageLine() }; mocker.GetMock().Setup(coberturaUtil => coberturaUtil.CoverageLines).Returns(coverageLines); @@ -499,7 +516,7 @@ private void VerifyClearUIEvents(int eventNumber) } else { - await ReloadInitializedCoverage(passedProject.Object); + await ReloadInitializedCoverage(coverageProject); } return (reportGeneratedHtmlContent, coverageLines); @@ -524,7 +541,7 @@ private async Task ThrowReadingReportHtml() ); var badPath = "^&$!"; - mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtmlFile(It.IsAny(), It.IsAny(), out badPath)); + //mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtml(It.IsAny(), It.IsAny(), out badPath)); List coverageLines = new List() { new CoverageLine() }; mocker.GetMock().Setup(coberturaUtil => coberturaUtil.CoverageLines).Returns(coverageLines); diff --git a/FineCodeCoverageTests/FineCodeCoverageTests.csproj b/FineCodeCoverageTests/FineCodeCoverageTests.csproj index 2f472ded..7697050b 100644 --- a/FineCodeCoverageTests/FineCodeCoverageTests.csproj +++ b/FineCodeCoverageTests/FineCodeCoverageTests.csproj @@ -86,6 +86,7 @@ + From c71f6dee1acf21f668f55c6f9d9d29d7e8a49958 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Mon, 17 May 2021 14:13:17 +0100 Subject: [PATCH 2/3] solution based --- ...overageToolOutputFolderSolutionProvider.cs | 33 ++++ ...ageToolOutputFolderFromSolutionProvider.cs | 39 +++++ .../CoverageToolOutputManager.cs | 33 ++-- ...overageToolOutputFolderSolutionProvider.cs | 34 ++++ .../ICoverageToolOutputFolderProvider.cs | 10 ++ ...overageToolOutputFolderSolutionProvider.cs | 9 ++ .../ICoverageToolOutputManager.cs | 8 +- .../ISolutionFolderProvider.cs | 7 + .../SolutionFolderProvider.cs | 27 ++++ FineCodeCoverage/Core/FCCEngine.cs | 3 +- .../Core/Model/CoverageProject.cs | 2 - .../Core/Model/ICoverageProject.cs | 1 - .../ReportGenerator/ReportGeneratorUtil.cs | 2 + FineCodeCoverage/Core/Utilities/FileUtil.cs | 5 + FineCodeCoverage/Core/Utilities/IFileUtil.cs | 1 + .../Core/Utilities/LinqExtensions.cs | 25 +++ FineCodeCoverage/Core/Utilities/MEF.cs | 36 +++++ .../Core/Utilities/ReflectionExtensions.cs | 18 +++ FineCodeCoverage/FineCodeCoverage.csproj | 14 +- FineCodeCoverage/Options/AppOptions.cs | 5 + FineCodeCoverage/Options/IAppOptions.cs | 1 + .../CoverageToolOutputManager_Tests.cs | 35 ---- ...eToolOutputFolderSolutionProvider_Tests.cs | 75 +++++++++ ...lOutputFolderFromSolutionProvider_Tests.cs | 111 +++++++++++++ .../CoverageToolOutputManager_Tests.cs | 153 ++++++++++++++++++ .../CoverageToolOutput_Exports_Tests.cs | 21 +++ ...eToolOutputFolderSolutionProvider_Tests.cs | 53 ++++++ .../SolutionFolderProvider_Tests.cs | 63 ++++++++ FineCodeCoverageTests/FCCEngine_Tests.cs | 35 +++- .../FineCodeCoverageTests.csproj | 9 +- .../Test helpers/MefOrderAssertions.cs | 49 ++++++ README.md | 12 ++ 32 files changed, 856 insertions(+), 73 deletions(-) create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/AppOptionsCoverageToolOutputFolderSolutionProvider.cs create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputFolderFromSolutionProvider.cs rename FineCodeCoverage/Core/{ => CoverageToolOutput}/CoverageToolOutputManager.cs (65%) create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/FccOutputExistenceCoverageToolOutputFolderSolutionProvider.cs create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderProvider.cs create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderSolutionProvider.cs rename FineCodeCoverage/Core/{ => CoverageToolOutput}/ICoverageToolOutputManager.cs (51%) create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/ISolutionFolderProvider.cs create mode 100644 FineCodeCoverage/Core/CoverageToolOutput/SolutionFolderProvider.cs create mode 100644 FineCodeCoverage/Core/Utilities/LinqExtensions.cs create mode 100644 FineCodeCoverage/Core/Utilities/MEF.cs create mode 100644 FineCodeCoverage/Core/Utilities/ReflectionExtensions.cs delete mode 100644 FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputManager_Tests.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs create mode 100644 FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs create mode 100644 FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs diff --git a/FineCodeCoverage/Core/CoverageToolOutput/AppOptionsCoverageToolOutputFolderSolutionProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/AppOptionsCoverageToolOutputFolderSolutionProvider.cs new file mode 100644 index 00000000..f641bed6 --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/AppOptionsCoverageToolOutputFolderSolutionProvider.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel.Composition; +using System.IO; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Options; + +namespace FineCodeCoverage.Engine +{ + [Order(1, typeof(ICoverageToolOutputFolderSolutionProvider))] + class AppOptionsCoverageToolOutputFolderSolutionProvider : ICoverageToolOutputFolderSolutionProvider + { + private readonly IAppOptionsProvider appOptionsProvider; + + [ImportingConstructor] + public AppOptionsCoverageToolOutputFolderSolutionProvider(IAppOptionsProvider appOptionsProvider) + { + this.appOptionsProvider = appOptionsProvider; + } + public string Provide(Func solutionFolderProvider) + { + var appOptions = appOptionsProvider.Get(); + if (!String.IsNullOrEmpty(appOptions.FCCSolutionOutputDirectoryName)) + { + var solutionFolder = solutionFolderProvider(); + if(solutionFolder != null) + { + return Path.Combine(solutionFolder, appOptions.FCCSolutionOutputDirectoryName); + } + } + return null; + } + } +} diff --git a/FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputFolderFromSolutionProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputFolderFromSolutionProvider.cs new file mode 100644 index 00000000..d031f328 --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputFolderFromSolutionProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Engine +{ + [Order(1, typeof(ICoverageToolOutputFolderProvider))] + class CoverageToolOutputFolderFromSolutionProvider : ICoverageToolOutputFolderProvider + { + private readonly ISolutionFolderProvider solutionFolderProvider; + private readonly IOrderedEnumerable> solutionFolderProviders; + + [ImportingConstructor] + public CoverageToolOutputFolderFromSolutionProvider(ISolutionFolderProvider solutionFolderProvider, [ImportMany] IEnumerable> solutionFolderProviders) + { + this.solutionFolderProvider = solutionFolderProvider; + this.solutionFolderProviders = solutionFolderProviders.OrderBy(p => p.Metadata.Order); + } + + public string Provide(List coverageProjects) + { + var provided = false; + string providedDirectory = null; + return solutionFolderProviders.SelectFirstNonNull(p => p.Value.Provide(() => + { + if(!provided) + { + providedDirectory = solutionFolderProvider.Provide(coverageProjects[0].ProjectFile); + provided = true; + } + return providedDirectory; + })); + + } + } +} diff --git a/FineCodeCoverage/Core/CoverageToolOutputManager.cs b/FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputManager.cs similarity index 65% rename from FineCodeCoverage/Core/CoverageToolOutputManager.cs rename to FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputManager.cs index e590e51c..603b0b42 100644 --- a/FineCodeCoverage/Core/CoverageToolOutputManager.cs +++ b/FineCodeCoverage/Core/CoverageToolOutput/CoverageToolOutputManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; @@ -18,19 +19,21 @@ internal class CoverageToolOutputManager : ICoverageToolOutputManager private const string projectCoverageToolOutputFolderName = "coverage-tool-output"; private string outputFolderForAllProjects; private List coverageProjects; + private readonly IOrderedEnumerable> outputFolderProviders; [ImportingConstructor] - public CoverageToolOutputManager(IFileUtil fileUtil, ILogger logger) + public CoverageToolOutputManager(IFileUtil fileUtil, ILogger logger,[ImportMany] IEnumerable> outputFolderProviders) { this.logger = logger; this.fileUtil = fileUtil; + this.outputFolderProviders = outputFolderProviders.OrderBy(p => p.Metadata.Order); } public void SetProjectCoverageOutputFolder(List coverageProjects) { this.coverageProjects = coverageProjects; - DetermineOutputFolderForAllProjects(); - if(outputFolderForAllProjects == null) + DetermineOutputFolder(); + if (outputFolderForAllProjects == null) { foreach(var coverageProject in coverageProjects) { @@ -47,7 +50,7 @@ public void SetProjectCoverageOutputFolder(List coverageProjec } } - public void SetReportOutput(string unifiedHtml, string processedReport, string unifiedXml) + public void OutputReports(string unifiedHtml, string processedReport, string unifiedXml) { var outputFolder = outputFolderForAllProjects ?? coverageProjects[0].CoverageOutputFolder; @@ -56,25 +59,13 @@ public void SetReportOutput(string unifiedHtml, string processedReport, string u fileUtil.WriteAllText(Path.Combine(outputFolder, unifiedXmlFileName), unifiedXml); } - private void DetermineOutputFolderForAllProjects() + private void DetermineOutputFolder() { - outputFolderForAllProjects = null; - var coverageProjectWithAllProjectsCoverageOutputFolder = coverageProjects.FirstOrDefault(cp => cp.AllProjectsCoverageOutputFolder != null); - if(coverageProjectWithAllProjectsCoverageOutputFolder != null) + outputFolderForAllProjects = outputFolderProviders.SelectFirstNonNull(p => p.Value.Provide(coverageProjects)); + if(outputFolderForAllProjects != null) { - var allProjectsCoverageOutputFolder = fileUtil.EnsureAbsolute( - coverageProjectWithAllProjectsCoverageOutputFolder.AllProjectsCoverageOutputFolder, - fileUtil.ParentDirectoryPath(coverageProjectWithAllProjectsCoverageOutputFolder.ProjectFile) - ); - - outputFolderForAllProjects = allProjectsCoverageOutputFolder; - logger.Log($"Outputting coverage files to - {outputFolderForAllProjects}"); - return; + logger.Log($"FCC output in {outputFolderForAllProjects}"); } - - - logger.Log($"Outputting coverage files in project output folder"); - } } } diff --git a/FineCodeCoverage/Core/CoverageToolOutput/FccOutputExistenceCoverageToolOutputFolderSolutionProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/FccOutputExistenceCoverageToolOutputFolderSolutionProvider.cs new file mode 100644 index 00000000..07a56727 --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/FccOutputExistenceCoverageToolOutputFolderSolutionProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel.Composition; +using System.IO; +using FineCodeCoverage.Core.Utilities; + +namespace FineCodeCoverage.Engine +{ + [Order(2,typeof(ICoverageToolOutputFolderSolutionProvider))] + class FccOutputExistenceCoverageToolOutputFolderSolutionProvider : ICoverageToolOutputFolderSolutionProvider + { + private const string fccOutputFolderName = "fcc-output"; + private readonly IFileUtil fileUtil; + + [ImportingConstructor] + public FccOutputExistenceCoverageToolOutputFolderSolutionProvider(IFileUtil fileUtil) + { + this.fileUtil = fileUtil; + } + + public string Provide(Func solutionFolderProvider) + { + var solutionFolder = solutionFolderProvider(); + if (solutionFolder != null) + { + var provided = Path.Combine(solutionFolder, fccOutputFolderName); + if (fileUtil.DirectoryExists(provided)) + { + return provided; + } + } + return null; + } + } +} diff --git a/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderProvider.cs new file mode 100644 index 00000000..4a8c52bc --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using FineCodeCoverage.Engine.Model; + +namespace FineCodeCoverage.Engine +{ + internal interface ICoverageToolOutputFolderProvider + { + string Provide(List coverageProjects); + } +} diff --git a/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderSolutionProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderSolutionProvider.cs new file mode 100644 index 00000000..345427ab --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputFolderSolutionProvider.cs @@ -0,0 +1,9 @@ +using System; + +namespace FineCodeCoverage.Engine +{ + interface ICoverageToolOutputFolderSolutionProvider + { + string Provide(Func solutionFolderProvider); + } +} diff --git a/FineCodeCoverage/Core/ICoverageToolOutputManager.cs b/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputManager.cs similarity index 51% rename from FineCodeCoverage/Core/ICoverageToolOutputManager.cs rename to FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputManager.cs index dc5e59f4..63393c3a 100644 --- a/FineCodeCoverage/Core/ICoverageToolOutputManager.cs +++ b/FineCodeCoverage/Core/CoverageToolOutput/ICoverageToolOutputManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using FineCodeCoverage.Engine.Model; namespace FineCodeCoverage.Engine @@ -10,6 +6,6 @@ namespace FineCodeCoverage.Engine internal interface ICoverageToolOutputManager { void SetProjectCoverageOutputFolder(List coverageProjects); - void SetReportOutput(string unifiedHtml, string processedReport, string unifiedXml); + void OutputReports(string unifiedHtml, string processedReport, string unifiedXml); } } diff --git a/FineCodeCoverage/Core/CoverageToolOutput/ISolutionFolderProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/ISolutionFolderProvider.cs new file mode 100644 index 00000000..89d8a2b0 --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/ISolutionFolderProvider.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Engine +{ + interface ISolutionFolderProvider + { + string Provide(string projectFile); + } +} diff --git a/FineCodeCoverage/Core/CoverageToolOutput/SolutionFolderProvider.cs b/FineCodeCoverage/Core/CoverageToolOutput/SolutionFolderProvider.cs new file mode 100644 index 00000000..d6fe68ac --- /dev/null +++ b/FineCodeCoverage/Core/CoverageToolOutput/SolutionFolderProvider.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; + +namespace FineCodeCoverage.Engine +{ + [Export(typeof(ISolutionFolderProvider))] + class SolutionFolderProvider : ISolutionFolderProvider + { + public string Provide(string projectFile) + { + string provided = null; + var directory = new FileInfo(projectFile).Directory; + while(directory != null) + { + var isSolutionDirectory = directory.EnumerateFiles().Any(f => f.Name.EndsWith(".sln")); + if (isSolutionDirectory) + { + provided = directory.FullName; + break; + } + directory = directory.Parent; + } + return provided; + } + } +} diff --git a/FineCodeCoverage/Core/FCCEngine.cs b/FineCodeCoverage/Core/FCCEngine.cs index 0d17e232..5b72d0a1 100644 --- a/FineCodeCoverage/Core/FCCEngine.cs +++ b/FineCodeCoverage/Core/FCCEngine.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using System.IO; using System.Linq; using System.Threading; using FineCodeCoverage.Core.Utilities; @@ -173,7 +172,7 @@ private void UpdateUI(List coverageLines, string reportHtml) coverageLines = coberturaUtil.CoverageLines; processedReport = reportGeneratorUtil.ProcessUnifiedHtml(result.UnifiedHtml, darkMode); - coverageOutputManager.SetReportOutput(result.UnifiedHtml, processedReport, result.UnifiedXml); + coverageOutputManager.OutputReports(result.UnifiedHtml, processedReport, result.UnifiedXml); } return (coverageLines, processedReport); } diff --git a/FineCodeCoverage/Core/Model/CoverageProject.cs b/FineCodeCoverage/Core/Model/CoverageProject.cs index 3a79575c..ad3deecb 100644 --- a/FineCodeCoverage/Core/Model/CoverageProject.cs +++ b/FineCodeCoverage/Core/Model/CoverageProject.cs @@ -38,8 +38,6 @@ public CoverageProject(IAppOptionsProvider appOptionsProvider, IFileSynchronizat this.canUseMsBuildWorkspace = canUseMsBuildWorkspace; } - public string AllProjectsCoverageOutputFolder => ProjectFileXElement.XPathSelectElement($"/PropertyGroup/AllProjectsCoverageOutputFolder")?.Value; - public string FCCOutputFolder => Path.Combine(ProjectOutputFolder, fccFolderName); public bool IsDotNetSdkStyle() { diff --git a/FineCodeCoverage/Core/Model/ICoverageProject.cs b/FineCodeCoverage/Core/Model/ICoverageProject.cs index 5c438c02..cfec5c08 100644 --- a/FineCodeCoverage/Core/Model/ICoverageProject.cs +++ b/FineCodeCoverage/Core/Model/ICoverageProject.cs @@ -8,7 +8,6 @@ namespace FineCodeCoverage.Engine.Model { internal interface ICoverageProject { - string AllProjectsCoverageOutputFolder { get; } string FCCOutputFolder { get; } string CoverageOutputFile { get; } string CoverageOutputFolder { get; set; } diff --git a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs index 91294c07..1ed8a972 100644 --- a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -147,6 +147,8 @@ async Task run(string outputReportType, string inputReports) } + fileUtil.TryDeleteDirectory(tempDirectory); + return reportGeneratorResult; } diff --git a/FineCodeCoverage/Core/Utilities/FileUtil.cs b/FineCodeCoverage/Core/Utilities/FileUtil.cs index 3651fc20..bfaef6f1 100644 --- a/FineCodeCoverage/Core/Utilities/FileUtil.cs +++ b/FineCodeCoverage/Core/Utilities/FileUtil.cs @@ -14,6 +14,11 @@ public string CreateTempDirectory() return tempDirectory; } + public void TryDeleteDirectory(string directory) + { + new DirectoryInfo(directory).TryDelete(); + } + public bool DirectoryExists(string directory) { return Directory.Exists(directory); diff --git a/FineCodeCoverage/Core/Utilities/IFileUtil.cs b/FineCodeCoverage/Core/Utilities/IFileUtil.cs index efdde014..3e9376a8 100644 --- a/FineCodeCoverage/Core/Utilities/IFileUtil.cs +++ b/FineCodeCoverage/Core/Utilities/IFileUtil.cs @@ -9,5 +9,6 @@ internal interface IFileUtil void TryEmptyDirectory(string directory); string EnsureAbsolute(string directory, string possiblyRelativeTo); string ParentDirectoryPath(string filePath); + void TryDeleteDirectory(string directory); } } diff --git a/FineCodeCoverage/Core/Utilities/LinqExtensions.cs b/FineCodeCoverage/Core/Utilities/LinqExtensions.cs new file mode 100644 index 00000000..76fd7508 --- /dev/null +++ b/FineCodeCoverage/Core/Utilities/LinqExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.Utilities +{ + internal static class LinqExtensions + { + public static TTransformed SelectFirstNonNull(this IEnumerable source, Func select) where TTransformed : class + { + foreach (var element in source) + { + var selected = select(element); + if (selected != null) + { + return selected; + } + + } + return null; + } + } +} diff --git a/FineCodeCoverage/Core/Utilities/MEF.cs b/FineCodeCoverage/Core/Utilities/MEF.cs new file mode 100644 index 00000000..9f0daa92 --- /dev/null +++ b/FineCodeCoverage/Core/Utilities/MEF.cs @@ -0,0 +1,36 @@ + +using System; +using System.ComponentModel.Composition; + +namespace FineCodeCoverage.Core.Utilities +{ + internal interface IOrderMetadata + { + int Order { get; } + } + + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class OrderAttribute : ExportAttribute, IOrderMetadata + { + public OrderAttribute(int order, Type contractType) + : base(contractType) + { + Order = order; + } + + public OrderAttribute(int order, string contractName) + : base(contractName) + { + Order = order; + } + + public OrderAttribute(int order, string contractName, Type contractType) + : base(contractName, contractType) + { + Order = order; + } + + public int Order { get;private set;} + } +} diff --git a/FineCodeCoverage/Core/Utilities/ReflectionExtensions.cs b/FineCodeCoverage/Core/Utilities/ReflectionExtensions.cs new file mode 100644 index 00000000..a1f68a13 --- /dev/null +++ b/FineCodeCoverage/Core/Utilities/ReflectionExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace FineCodeCoverage.Core.Utilities +{ + public static class ReflectionExtensions + { + public static TCustomAttribute[] GetTypedCustomAttributes(this ICustomAttributeProvider customAttributeProvider, bool inherit) where TCustomAttribute:Attribute + { + var attributes = customAttributeProvider.GetCustomAttributes(typeof(TCustomAttribute), inherit); + return attributes as TCustomAttribute[]; + } + } +} diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index 683dd33e..1f7ef590 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -68,14 +68,24 @@ + + + + + + + - + + + + @@ -140,7 +150,7 @@ - + diff --git a/FineCodeCoverage/Options/AppOptions.cs b/FineCodeCoverage/Options/AppOptions.cs index 26cad83b..cb617714 100644 --- a/FineCodeCoverage/Options/AppOptions.cs +++ b/FineCodeCoverage/Options/AppOptions.cs @@ -12,6 +12,7 @@ internal class AppOptions : DialogPage, IAppOptions private const string excludeIncludeCategory = "Exclude / Include"; private const string coverletCategory = "Coverlet"; private const string openCoverCategory = "OpenCover"; + private const string outputCategory = "Output"; public AppOptions():this(false) { @@ -132,6 +133,10 @@ You can also ignore additional attributes by adding to this list (short name or [Category(openCoverCategory)] public string OpenCoverCustomPath { get; set; } + [Description("To have fcc output visible in a sub folder of your solution provide this name")] + [Category(outputCategory)] + public string FCCSolutionOutputDirectoryName { get; set; } + [SuppressMessage("Usage", "VSTHRD010:Invoke single-threaded types on Main thread")] public override void SaveSettingsToStorage() { diff --git a/FineCodeCoverage/Options/IAppOptions.cs b/FineCodeCoverage/Options/IAppOptions.cs index 2681c702..a754e329 100644 --- a/FineCodeCoverage/Options/IAppOptions.cs +++ b/FineCodeCoverage/Options/IAppOptions.cs @@ -17,5 +17,6 @@ public interface IAppOptions bool CoverletConsoleLocal { get; } string CoverletCollectorDirectoryPath { get; } string OpenCoverCustomPath { get; } + string FCCSolutionOutputDirectoryName { get; } } } \ No newline at end of file diff --git a/FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs b/FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs deleted file mode 100644 index 032dbeb4..00000000 --- a/FineCodeCoverageTests/CoverageToolOutputManager_Tests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AutoMoq; -using FineCodeCoverage.Engine; -using NUnit.Framework; - -namespace FineCodeCoverageTests -{ - class CoverageToolOutputManager_Tests - { - private AutoMoqer mocker; - private CoverageToolOutputManager coverageToolOutputManager; - [SetUp] - public void SetUp() - { - mocker = new AutoMoqer(); - coverageToolOutputManager = mocker.Create(); - } - - [Test] - public void Should_Set_CoverageOutputFolder_To_Sub_Folder_Of_CoverageProject_FCCOutputFolder_For_All_When_Do_Not_Specify() - { - - } - - [Test] - public void Should_Set_CoverageOutputFolder_To_ProjectName_Sub_Folder_Of_First_Providing_AllProjectsCoverageOutputFolder() - { - - } - } -} diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs new file mode 100644 index 00000000..ead21d0e --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/AppOptionsCoverageToolOutputFolderSolutionProvider_Tests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMoq; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Options; +using FineCodeCoverageTests.Test_helpers; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.CoverageToolOutput_Tests +{ + class AppOptionsCoverageToolOutputFolderSolutionProvider_Tests + { + private AutoMoqer mocker; + + [SetUp] + public void SetUp() + { + mocker = new AutoMoqer(); + + } + + [TestCase(null)] + [TestCase("")] + public void Should_Return_Null_Without_Getting_Solution_Folder_When_AppOption_FCCSolutionOutputDirectoryName_NotSet(string optionValue) + { + var mockAppOptionsProvider = mocker.GetMock(); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(options => options.FCCSolutionOutputDirectoryName).Returns(optionValue); + mockAppOptionsProvider.Setup(aop => aop.Get()).Returns(mockAppOptions.Object); + + var provider = mocker.Create(); + var providedSolutionFolder = false; + Assert.Null(provider.Provide(() => + { + providedSolutionFolder = true; + return null; + })); + + Assert.False(providedSolutionFolder); + } + + [Test] + public void Should_Return_Null_If_No_Solution_Folder_Provided_To_It() + { + var mockAppOptionsProvider = mocker.GetMock(); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(options => options.FCCSolutionOutputDirectoryName).Returns("Value"); + mockAppOptionsProvider.Setup(aop => aop.Get()).Returns(mockAppOptions.Object); + var provider = mocker.Create(); + Assert.Null(provider.Provide(() => null)); + } + + [Test] + public void Should_Combine_The_Solution_Folder_With_FCCSolutionOutputDirectoryName() + { + var mockAppOptionsProvider = mocker.GetMock(); + var mockAppOptions = new Mock(); + mockAppOptions.SetupGet(options => options.FCCSolutionOutputDirectoryName).Returns("FCCOutput"); + mockAppOptionsProvider.Setup(aop => aop.Get()).Returns(mockAppOptions.Object); + var provider = mocker.Create(); + Assert.AreEqual(provider.Provide(() => "SolutionFolder"),Path.Combine("SolutionFolder","FCCOutput")); + } + + [Test] + public void Should_Have_First_Order() + { + MefOrderAssertions.TypeHasExpectedOrder(typeof(AppOptionsCoverageToolOutputFolderSolutionProvider), 1); + } + } +} diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs new file mode 100644 index 00000000..1918f5f3 --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputFolderFromSolutionProvider_Tests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Engine.Model; +using FineCodeCoverageTests.Test_helpers; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests.CoverageToolOutput_Tests +{ + class CoverageToolOutputFolderFromSolutionProvider_Tests + { + private List callOrder; + private AutoMoqer mocker; + + [SetUp] + public void SetUp() + { + mocker = new AutoMoqer(); + + } + + private void SetUpProviders(bool provider1First, string provider1Provides, string provider2Provides) + { + callOrder = new List(); + var mockOrderMetadata1 = new Mock(); + mockOrderMetadata1.Setup(o => o.Order).Returns(provider1First ? 1 : 2); + var mockOrderMetadata2 = new Mock(); + mockOrderMetadata2.Setup(o => o.Order).Returns(provider1First ? 2 : 1); + + var mockCoverageToolOutputFolderProvider1 = new Mock(); + mockCoverageToolOutputFolderProvider1.Setup(p => p.Provide(It.IsAny>())).Returns(provider1Provides).Callback(() => callOrder.Add(1)); + var mockCoverageToolOutputFolderProvider2 = new Mock(); + mockCoverageToolOutputFolderProvider2.Setup(p => p.Provide(It.IsAny>())).Returns(provider2Provides).Callback(() => callOrder.Add(2)); + List> lazyOrderedProviders = new List> + { + new Lazy(()=>mockCoverageToolOutputFolderProvider1.Object,mockOrderMetadata1.Object), + new Lazy(()=>mockCoverageToolOutputFolderProvider2.Object,mockOrderMetadata2.Object) + }; + mocker.SetInstance>>(lazyOrderedProviders); + } + + public void Should_Have_First_Order() + { + MefOrderAssertions.TypeHasExpectedOrder(typeof(CoverageToolOutputFolderFromSolutionProvider), 1); + } + + [TestCase(true, 1, 2)] + [TestCase(false, 2, 1)] + public void Should_Use_Providers_In_Order(bool provider1First, int expectedFirst, int expectedSecond) + { + SetUpProviders(provider1First, null, null); + var coverageToolOutputFolderFromSolutionProvider = mocker.Create(); + coverageToolOutputFolderFromSolutionProvider.Provide(null); + Assert.AreEqual(callOrder, new List { expectedFirst, expectedSecond }); + } + //need to check if there is a coverage project ? + [Test] + public void Should_Stop_Asking_Providers_When_One_Returns_The_Folder() + { + SetUpProviders(true, "Folder", "_"); + var coverageToolOutputFolderFromSolutionProvider = mocker.Create(); + Assert.AreEqual(coverageToolOutputFolderFromSolutionProvider.Provide(null),"Folder"); + Assert.AreEqual(callOrder, new List { 1 }); + } + + [Test] + public void Should_Provide_The_Solution_Folder_Once_From_The_Solution_Folder_Provider_Wth_ProjectFile_Of_First_CoverageProject() + { + var mockProject1 = new Mock(); + mockProject1.Setup(p => p.ProjectFile).Returns("project.csproj"); + var mockProject2 = new Mock(); + mockProject2.Setup(p => p.ProjectFile).Returns("project2.csproj"); + var coverageProjects = new List{ mockProject1.Object, mockProject2.Object}; + + var mockOrderMetadata1 = new Mock(); + mockOrderMetadata1.Setup(o => o.Order).Returns(1); + + var mockCoverageToolOutputFolderProvider1 = new Mock(); + Func solutionFolderProviderFunc = null; + mockCoverageToolOutputFolderProvider1.Setup(p => p.Provide(It.IsAny>())) + .Callback>(solnFolderProvider => + { + solutionFolderProviderFunc = solnFolderProvider; + }); + List> lazyOrderedProviders = new List> + { + new Lazy(()=>mockCoverageToolOutputFolderProvider1.Object,mockOrderMetadata1.Object), + }; + mocker.SetInstance>>(lazyOrderedProviders); + + var mockSolutionFolderProvider = mocker.GetMock(); + mockSolutionFolderProvider.Setup(sfp => sfp.Provide("project.csproj")).Returns("SolutionPath"); + var coverageToolOutputFolderFromSolutionProvider = mocker.Create(); + + coverageToolOutputFolderFromSolutionProvider.Provide(coverageProjects); + + var solutionFolder = solutionFolderProviderFunc(); + var solutionFolder2 = solutionFolderProviderFunc(); + Assert.AreEqual(solutionFolder, "SolutionPath"); + Assert.AreEqual(solutionFolder2, "SolutionPath"); + mockSolutionFolderProvider.Verify(sfp => sfp.Provide("project.csproj"), Times.Once()); + + } + } +} diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputManager_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputManager_Tests.cs new file mode 100644 index 00000000..692b0cda --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutputManager_Tests.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using FineCodeCoverage.Engine.Model; +using Moq; +using NUnit.Framework; + +namespace FineCodeCoverageTests +{ + class CoverageToolOutputManager_Tests + { + private AutoMoqer mocker; + private Mock mockProject1; + private Mock mockProject2; + private List coverageProjects; + private List callOrder; + + [SetUp] + public void SetUp() + { + mocker = new AutoMoqer(); + mockProject1 = new Mock(); + mockProject1.Setup(p => p.FCCOutputFolder).Returns("p1output"); + mockProject1.Setup(p => p.ProjectName).Returns("project1"); + mockProject1.SetupProperty(p => p.CoverageOutputFolder); + mockProject2 = new Mock(); + mockProject2.Setup(p => p.FCCOutputFolder).Returns("p2output"); + mockProject2.Setup(p => p.ProjectName).Returns("project2"); + coverageProjects = new List { mockProject1.Object, mockProject2.Object }; + } + + private void SetUpProviders(bool provider1First,string provider1Provides, string provider2Provides) + { + callOrder = new List(); + var mockOrderMetadata1 = new Mock(); + mockOrderMetadata1.Setup(o => o.Order).Returns(provider1First? 1 : 2); + var mockOrderMetadata2 = new Mock(); + mockOrderMetadata2.Setup(o => o.Order).Returns(provider1First ? 2 : 1); + + var mockCoverageToolOutputFolderProvider1 = new Mock(); + mockCoverageToolOutputFolderProvider1.Setup(p => p.Provide(coverageProjects)).Returns(provider1Provides).Callback(() => callOrder.Add(1)); + var mockCoverageToolOutputFolderProvider2 = new Mock(); + mockCoverageToolOutputFolderProvider2.Setup(p => p.Provide(coverageProjects)).Returns(provider2Provides).Callback(() => callOrder.Add(2)); + List> lazyOrderedProviders = new List> + { + new Lazy(()=>mockCoverageToolOutputFolderProvider1.Object,mockOrderMetadata1.Object), + new Lazy(()=>mockCoverageToolOutputFolderProvider2.Object,mockOrderMetadata2.Object) + }; + mocker.SetInstance>>(lazyOrderedProviders); + } + + [TestCase(true,1, 2)] + [TestCase(false, 2, 1)] + public void Should_Use_Providers_In_Order_When_Determining_CoverageProject_Output_Folder(bool provider1First, int expectedFirst, int expectedSecond) + { + SetUpProviders(provider1First, null, null); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + Assert.AreEqual(callOrder, new List { expectedFirst, expectedSecond }); + } + + [Test] + public void Should_Stop_Asking_Providers_When_One_Provides_Value() + { + SetUpProviders(true, "_", "_"); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + Assert.AreEqual(callOrder, new List { 1 }); + } + + [Test] + public void Should_Try_Empty_Provided_Output_Folder() + { + SetUpProviders(true, "Provided", "_"); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + mocker.Verify(f => f.TryEmptyDirectory("Provided")); + } + + + [Test] + public void Should_Log_When_Provided() + { + SetUpProviders(true, "Provided", "_"); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + mocker.Verify(l => l.Log("FCC output in Provided")); + } + + [Test] + public void Should_Set_CoverageOutputFolder_To_ProjectName_Sub_Folder_Of_Provided() + { + SetUpProviders(true, "Provided", "_"); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + + var expectedProject1OutputFolder = Path.Combine("Provided", mockProject1.Object.ProjectName); + var expectedProject2OutputFolder = Path.Combine("Provided", mockProject2.Object.ProjectName); + mockProject1.VerifySet(p => p.CoverageOutputFolder = expectedProject1OutputFolder); + mockProject2.VerifySet(p => p.CoverageOutputFolder = expectedProject2OutputFolder); + + } + + [Test] + public void Should_Set_CoverageOutputFolder_To_Sub_Folder_Of_CoverageProject_FCCOutputFolder_For_All_When_Not_Provided() + { + SetUpProviders(true, null, null); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + + var expectedProject1OutputFolder = Path.Combine(mockProject1.Object.FCCOutputFolder, "coverage-tool-output"); + var expectedProject2OutputFolder = Path.Combine(mockProject2.Object.FCCOutputFolder, "coverage-tool-output"); + mockProject1.VerifySet(p => p.CoverageOutputFolder = expectedProject1OutputFolder); + mockProject2.VerifySet(p => p.CoverageOutputFolder = expectedProject2OutputFolder); + } + + [Test] + public void Should_Output_Reports_To_First_Project_CoverageOutputFolder_When_Not_Provided() + { + SetUpProviders(true, null, null); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + + coverageToolOutputManager.OutputReports("unified html", "processed report", "unified xml"); + + var firstProjectOutputFolder = mockProject1.Object.CoverageOutputFolder; + + mocker.Verify(f => f.WriteAllText(Path.Combine(firstProjectOutputFolder, "index.html"), "unified html")); + mocker.Verify(f => f.WriteAllText(Path.Combine(firstProjectOutputFolder, "index-processed.html"), "processed report")); + mocker.Verify(f => f.WriteAllText(Path.Combine(firstProjectOutputFolder, "Cobertura.xml"), "unified xml")); + + } + + [Test] + public void Should_Output_Reports_To_Provided_When_Not_Provided() + { + SetUpProviders(true, "Provided", null); + var coverageToolOutputManager = mocker.Create(); + coverageToolOutputManager.SetProjectCoverageOutputFolder(coverageProjects); + + coverageToolOutputManager.OutputReports("unified html", "processed report", "unified xml"); + + mocker.Verify(f => f.WriteAllText(Path.Combine("Provided", "index.html"), "unified html")); + mocker.Verify(f => f.WriteAllText(Path.Combine("Provided", "index-processed.html"), "processed report")); + mocker.Verify(f => f.WriteAllText(Path.Combine("Provided", "Cobertura.xml"), "unified xml")); + + } + + } +} diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs new file mode 100644 index 00000000..7e81b05e --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/CoverageToolOutput_Exports_Tests.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FineCodeCoverage.Engine; +using FineCodeCoverageTests.Test_helpers; +using NUnit.Framework; + +namespace FineCodeCoverageTests +{ + class CoverageToolOutput_Exports_Tests + { + [Test] + [Ignore("FileLoadException Microsoft.VisualStudio.Threading")] + public void ICoverageToolOutputFolderProvider_Should_Have_Consistent_Ordered_Exports() + { + MefOrderAssertions.InterfaceExportsHaveConsistentOrder(typeof(ICoverageToolOutputFolderProvider)); + } + } +} diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs new file mode 100644 index 00000000..e2009e3e --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMoq; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using FineCodeCoverageTests.Test_helpers; +using NUnit.Framework; + +namespace FineCodeCoverageTests.CoverageToolOutput_Tests +{ + class FccOutputExistenceCoverageToolOutputFolderSolutionProvider_Tests + { + private AutoMoqer mocker; + + [SetUp] + public void SetUp() + { + mocker = new AutoMoqer(); + + } + + [Test] + public void Should_Return_Null_If_No_Solution_Folder_Provided_To_It() + { + var provider = mocker.Create(); + Assert.Null(provider.Provide(() => null)); + } + + [TestCase(true)] + [TestCase(false)] + public void Should_Return_Path_To_FCC_Output_Folder_In_Solution_Folder_If_Exists(bool exists) + { + var solutionFolder = "SolutionFolder"; + var expected = Path.Combine("SolutionFolder", "fcc-output"); + + var mockFileUtil = mocker.GetMock(); + mockFileUtil.Setup(fu => fu.DirectoryExists(expected)).Returns(exists); + + var provider = mocker.Create(); + Assert.AreEqual(provider.Provide(() => solutionFolder),exists? expected : null); + } + + [Test] + public void Should_Have_Second_Order() + { + MefOrderAssertions.TypeHasExpectedOrder(typeof(FccOutputExistenceCoverageToolOutputFolderSolutionProvider), 2); + } + } +} diff --git a/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs b/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs new file mode 100644 index 00000000..0a1cd1d6 --- /dev/null +++ b/FineCodeCoverageTests/CoverageToolOutput_Tests/SolutionFolderProvider_Tests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Engine; +using NUnit.Framework; + +namespace FineCodeCoverageTests.CoverageToolOutput_Tests +{ + class SolutionFolderProvider_Tests + { + private string tempDirectory; + private FileUtil fileUtil = new FileUtil(); + + [SetUp] + public void Create_Temp_Directory() + { + tempDirectory = fileUtil.CreateTempDirectory(); + File.WriteAllText(Path.Combine(tempDirectory, "my.sln"), ""); + } + + [TearDown] + public void Delete_Temp_Directories() + { + fileUtil.TryDeleteDirectory(tempDirectory); + } + + [Test] + public void Should_Work_When_Solution_And_Test_Project_Are_In_Same_Folder() + { + var solutionFolderProvider = new SolutionFolderProvider(); + var provided = solutionFolderProvider.Provide(Path.Combine(tempDirectory, "my.proj")); + + Assert.AreEqual(tempDirectory, provided); + } + + [Test] + public void Should_Look_up_Directory_Tree() + { + var projectDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory, "Project")); + + var solutionFolderProvider = new SolutionFolderProvider(); + var provided = solutionFolderProvider.Provide(Path.Combine(projectDirectory.FullName, "my.proj")); + + Assert.AreEqual(tempDirectory, provided); + + } + + [Test] + public void Should_Return_Null_When_No_Solution_Directory_Ascendant() + { + tempDirectory = fileUtil.CreateTempDirectory(); + + var solutionFolderProvider = new SolutionFolderProvider(); + var provided = solutionFolderProvider.Provide(Path.Combine(tempDirectory, "my.proj")); + + Assert.Null(provided); + } + } +} diff --git a/FineCodeCoverageTests/FCCEngine_Tests.cs b/FineCodeCoverageTests/FCCEngine_Tests.cs index b33ea637..83f392de 100644 --- a/FineCodeCoverageTests/FCCEngine_Tests.cs +++ b/FineCodeCoverageTests/FCCEngine_Tests.cs @@ -232,7 +232,36 @@ await ReloadSuitableCoverageProject(mockCoverageProject => { mocker.Verify(coverageUtilManager => coverageUtilManager.RunCoverageAsync(coverageProject, true)); } - + + [Test] + public async Task Should_Allow_The_CoverageOutputManager_To_SetProjectCoverageOutputFolder() + { + var mockCoverageToolOutputManager = mocker.GetMock(); + mockCoverageToolOutputManager.Setup(om => om.SetProjectCoverageOutputFolder(It.IsAny>())). + Callback>(coverageProjects => + { + coverageProjects[0].CoverageOutputFolder = "Set by ICoverageToolOutputManager"; + }); + + ICoverageProject coverageProjectAfterCoverageOutputManager = null; + var coverageUtilManager = mocker.GetMock(); + coverageUtilManager.Setup(mgr => mgr.RunCoverageAsync(It.IsAny(), It.IsAny())) + .Callback((cp, _) => + { + coverageProjectAfterCoverageOutputManager = cp; + }); + + await ReloadSuitableCoverageProject(mockCoverageProject => { + mockCoverageProject.SetupProperty(cp => cp.CoverageOutputFolder); + mockCoverageProject.Setup(p => p.StepAsync("Run Coverage Tool", It.IsAny>())).Callback>((_, runCoverTool) => + { + runCoverTool(mockCoverageProject.Object); + }); + }); + + Assert.AreEqual(coverageProjectAfterCoverageOutputManager.CoverageOutputFolder, "Set by ICoverageToolOutputManager"); + } + [Test] // Not testing dark mode as ui will change public async Task Should_Run_Report_Generator_With_Output_Files_From_Coverage_For_Coverage_Projects_That_Have_Not_Failed() { @@ -313,7 +342,7 @@ public async Task Should_Set_Report_Output_If_Success() mockReportGenerator.Setup(rg => rg.ProcessUnifiedHtml("Unified html", It.IsAny())).Returns("Processed html"); await ReloadInitializedCoverage(passedProject.Object); - mocker.Verify(coverageToolOutputManager => coverageToolOutputManager.SetReportOutput("Unified html","Processed html","Unified xml")); + mocker.Verify(coverageToolOutputManager => coverageToolOutputManager.OutputReports("Unified html","Processed html","Unified xml")); } [Test] @@ -358,7 +387,7 @@ public async Task Should_Not_Set_Report_Output_If_Failure() ); await ReloadInitializedCoverage(passedProject.Object); - mocker.Verify(coverageToolOutputManager => coverageToolOutputManager.SetReportOutput(It.IsAny(), It.IsAny(), It.IsAny()),Times.Never()); + mocker.Verify(coverageToolOutputManager => coverageToolOutputManager.OutputReports(It.IsAny(), It.IsAny(), It.IsAny()),Times.Never()); } [Test] diff --git a/FineCodeCoverageTests/FineCodeCoverageTests.csproj b/FineCodeCoverageTests/FineCodeCoverageTests.csproj index 7697050b..f2a22e28 100644 --- a/FineCodeCoverageTests/FineCodeCoverageTests.csproj +++ b/FineCodeCoverageTests/FineCodeCoverageTests.csproj @@ -67,6 +67,7 @@ ..\packages\NUnit.3.13.1\lib\net45\nunit.framework.dll + @@ -86,8 +87,14 @@ - + + + + + + + diff --git a/FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs b/FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs new file mode 100644 index 00000000..b2154e39 --- /dev/null +++ b/FineCodeCoverageTests/Test helpers/MefOrderAssertions.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FineCodeCoverage.Core.Utilities; +using NUnit.Framework; + +namespace FineCodeCoverageTests.Test_helpers +{ + public static class MefOrderAssertions + { + private static FineCodeCoverage.Core.Utilities.OrderAttribute GetOrderAtrribute(Type classType) + { + return classType.GetTypedCustomAttributes( + false)[0]; + } + public static void TypeHasExpectedOrder(Type classType,int expectedOrder) + { + Assert.AreEqual(GetOrderAtrribute(classType).Order, expectedOrder); + } + + public static void InterfaceExportsHaveConsistentOrder(Type interfaceType) + { + var types = interfaceType.Assembly.GetTypes(); + var derivations = types.Where(t => t != interfaceType && interfaceType.IsAssignableFrom(t)); + var orders = derivations.Select(d => + { + var orderAttribute = GetOrderAtrribute(d); + if (orderAttribute == null) + { + throw new Exception("Missing mef attribute"); + } + if (orderAttribute.ContractType != interfaceType) + { + throw new Exception("Incorrect contract type"); + } + return orderAttribute.Order; + }).OrderBy(i => i).ToList(); + Assert.Greater(orders.Count, 0); + var count = 1; + foreach(var order in orders) + { + Assert.AreEqual(order, count); + count++; + } + } + } +} diff --git a/README.md b/README.md index a548d60b..8dbdce63 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ CoverletConsoleLocal Specify true to use your own dotnet tools local install o CoverletConsoleCustomPath Specify path to coverlet console exe if you need functionality that the FCC version does not provide. CoverletConsoleGlobal Specify true to use your own dotnet tools global install of coverlet console. +FCCSolutionOutputDirectoryName To have fcc output visible in a sub folder of your solution provide this name + The "CoverletConsole" settings have precedence Local / CustomPath / Global. Both 'Exclude' and 'Include' options can be used together but 'Exclude' takes precedence. @@ -151,6 +153,16 @@ Examples Both 'Exclude' and 'Include' options can be used together but 'Exclude' takes precedence. ``` + +## FCC Output +FCC outputs, by default, inside each test project's Debug folder. +If you prefer you can specify a folder to contain the files output internally and used by FCC. +Both of the methods below look for a directory containing a .sln file in an ascendant directory of the directory containing the +first test project file. If such a solution directory is found then the logic applies. + +If the solution directory has a sub directory fcc-output then it will automatically be used. + +Alternatively, if you supply FCCSolutionOutputDirectoryName in options the directory will be created if necessary and used. ## Contribute Check out the [contribution guidelines](CONTRIBUTING.md) From b89f358c40f6644c61b27c09bcdabd113ee3ae24 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Mon, 17 May 2021 14:14:32 +0100 Subject: [PATCH 3/3] rename IReportGeneratorUtil method name --- FineCodeCoverage/Core/FCCEngine.cs | 2 +- .../ReportGenerator/ReportGeneratorUtil.cs | 4 ++-- FineCodeCoverageTests/FCCEngine_Tests.cs | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/FineCodeCoverage/Core/FCCEngine.cs b/FineCodeCoverage/Core/FCCEngine.cs index 5b72d0a1..b400ff1c 100644 --- a/FineCodeCoverage/Core/FCCEngine.cs +++ b/FineCodeCoverage/Core/FCCEngine.cs @@ -164,7 +164,7 @@ private void UpdateUI(List coverageLines, string reportHtml) var darkMode = CurrentTheme.Equals("Dark", StringComparison.OrdinalIgnoreCase); - var result = await reportGeneratorUtil.RunReportGeneratorAsync(coverOutputFiles, darkMode, true); + var result = await reportGeneratorUtil.GenerateAsync(coverOutputFiles, darkMode, true); if (result.Success) { diff --git a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs index 1ed8a972..8a8badba 100644 --- a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -18,7 +18,7 @@ interface IReportGeneratorUtil { void Initialize(string appDataFolder); string ProcessUnifiedHtml(string htmlForProcessing, bool darkMode); - Task RunReportGeneratorAsync(IEnumerable coverOutputFiles, bool darkMode, bool throwError = false); + Task GenerateAsync(IEnumerable coverOutputFiles, bool darkMode, bool throwError = false); } @@ -68,7 +68,7 @@ public void Initialize(string appDataFolder) ?? Directory.GetFiles(zipDestination, "*reportGenerator*.exe", SearchOption.AllDirectories).FirstOrDefault(); } - public async Task RunReportGeneratorAsync(IEnumerable coverOutputFiles, bool darkMode, bool throwError = false) + public async Task GenerateAsync(IEnumerable coverOutputFiles, bool darkMode, bool throwError = false) { var title = "ReportGenerator Run"; var tempDirectory = fileUtil.CreateTempDirectory(); diff --git a/FineCodeCoverageTests/FCCEngine_Tests.cs b/FineCodeCoverageTests/FCCEngine_Tests.cs index 83f392de..eb54b6f5 100644 --- a/FineCodeCoverageTests/FCCEngine_Tests.cs +++ b/FineCodeCoverageTests/FCCEngine_Tests.cs @@ -273,7 +273,7 @@ public async Task Should_Run_Report_Generator_With_Output_Files_From_Coverage_Fo passedProject.Setup(p => p.CoverageOutputFile).Returns(passedProjectCoverageOutputFile); mocker.GetMock().Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.Is( coverOutputFiles => coverOutputFiles.Count() == 1 && coverOutputFiles.First() == passedProjectCoverageOutputFile), It.IsAny(), @@ -289,7 +289,7 @@ public async Task Should_Run_Report_Generator_With_Output_Files_From_Coverage_Fo public async Task Should_Not_Run_ReportGenerator_If_No_Successful_Projects() { await ReloadInitializedCoverage(); - mocker.Verify(rg => rg.RunReportGeneratorAsync(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); + mocker.Verify(rg => rg.GenerateAsync(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] @@ -299,7 +299,7 @@ public async Task Should_Process_ReportGenerator_Output_If_Success() var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.IsAny>(), It.IsAny(), true).Result) @@ -326,7 +326,7 @@ public async Task Should_Set_Report_Output_If_Success() var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.IsAny>(), It.IsAny(), true).Result) @@ -352,7 +352,7 @@ public async Task Should_Not_Process_ReportGenerator_Output_If_Failure() var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.IsAny>(), It.IsAny(), true).Result) @@ -375,7 +375,7 @@ public async Task Should_Not_Set_Report_Output_If_Failure() var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.IsAny>(), It.IsAny(), true).Result) @@ -522,7 +522,7 @@ private void VerifyClearUIEvents(int eventNumber) var coverageProject = CreateSuitableProject().Object; var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.Is>(coverOutputFiles => coverOutputFiles.Count() == 1 && coverOutputFiles.First() == coverageProject.CoverageOutputFile), It.IsAny(), true).Result) @@ -558,7 +558,7 @@ private async Task ThrowReadingReportHtml() var mockReportGenerator = mocker.GetMock(); mockReportGenerator.Setup(rg => - rg.RunReportGeneratorAsync( + rg.GenerateAsync( It.IsAny>(), It.IsAny(), true).Result) @@ -612,7 +612,7 @@ private async Task StopCoverage() private void SetUpSuccessfulRunReportGenerator() { mocker.GetMock() - .Setup(rg => rg.RunReportGeneratorAsync( + .Setup(rg => rg.GenerateAsync( It.IsAny>(), It.IsAny(), It.IsAny()