diff --git a/README.md b/README.md index 1434068..203a718 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ Liquid Test Reports are logger extensions for the [Visual Studio Test Platform]( ### Latest: +##### 1.3.2 beta + +- [Cli] + - Add filename pattern matching support for CLI + ##### 1.2.1 beta - [Cli] @@ -31,13 +36,6 @@ Liquid Test Reports are logger extensions for the [Visual Studio Test Platform]( - Add TRX mapping - Move template error logging from report generator into base test logger -#### 1.0.9 - -**Changed** - -- [All] Revert target framework from .NET standard 2.1 back to .NET standard 2.0 for broader compatibility - - For use with .NET Core 2.x, see [Compatibility](#Compatibility) - [Previous changes](./docs/Changelog.md) @@ -61,7 +59,8 @@ liquid [options] **Options:** **--inputs [inputs]** Array of formatted configuration strings for test report inputs, with configurations separated by a semicolon - - **File=file-name;** The path of the input file. + - **File=file-name;** The path or glob pattern for input file/s + - **Folder=folder-name;** - Base directory for finding test files - **Format=report-format;** Optional input report format, case insensitive, supported values are `Trx` of `JUnit`. Defaults to `Trx`. - **GroupTitle=group-title;** Optional title to group reports under, test runs with the same group title will be merged. - **TestPrefix=test-prefix;** Optional test suffix, if provided test origination for the provided report will have the suffix appended to its name. @@ -92,6 +91,16 @@ liquid --inputs "File=xUnit-net461-sample.trx;Format=Trx" --output-file SingleIn #### More Examples +**File glob pattern relative to current directory** + +```bash +liquid --inputs "File=**/*sample.trx" --output-file report.md +``` +**File glob pattern using specific directory** +```bash +liquid --inputs "File=**/*sample.trx;Folder=C:\MyTestFolder" --output-file report.md +``` + **Report from single input, with a custom title** - [Sample Output](docs/samples/cli/CustomTitle.md) ``` bash diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ba688e..69a7230 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,7 +7,7 @@ pool: variables: buildConfiguration: 'Release' majorVersion: 1 - minorVersion: 2 + minorVersion: 3 suffix: 'beta' stages: @@ -52,7 +52,6 @@ stages: - task: DotNetCoreCLI@2 displayName: 'dotnet pack' - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) inputs: command: custom projects: | @@ -66,7 +65,6 @@ stages: - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact' - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) inputs: PathtoPublish: $(Build.ArtifactStagingDirectory) ArtifactName: 'drop' diff --git a/docs/Changelog.md b/docs/Changelog.md index 0830b34..f02c329 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,5 +1,12 @@ ## Changelog +#### 1.0.9 + +**Changed** + +- [All] Revert target framework from .NET standard 2.1 back to .NET standard 2.0 for broader compatibility + - For use with .NET Core 2.x, see [Compatibility](#Compatibility) + #### 1.0.1 **Changed** diff --git a/src/LiquidTestReports.Cli/ConsoleRunner.cs b/src/LiquidTestReports.Cli/ConsoleRunner.cs index 8c5e95c..7cef741 100644 --- a/src/LiquidTestReports.Cli/ConsoleRunner.cs +++ b/src/LiquidTestReports.Cli/ConsoleRunner.cs @@ -1,5 +1,6 @@ using DotLiquid; using DotLiquid.Exceptions; +using LiquidTestReports.Cli.Models; using LiquidTestReports.Cli.Resources; using LiquidTestReports.Cli.Services; using LiquidTestReports.Core; @@ -24,8 +25,8 @@ internal class ConsoleRunner internal ConsoleRunner(ReportInput[] inputs, FileInfo outputFile) { - _errorConsole = AnsiConsole.Create(new AnsiConsoleSettings { Out = Console.Error }); - _standardConsole = AnsiConsole.Create(new AnsiConsoleSettings { Out = Console.Out }); + _errorConsole = AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(Console.Error) }); + _standardConsole = AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(Console.Out) }); _inputs = inputs; _outputFile = outputFile; } @@ -131,27 +132,30 @@ private void WriteInputTable() foreach (var input in _inputs) { - table.AddRow(input.File.Name, - string.IsNullOrEmpty(input.GroupTitle) ? "default" : input.GroupTitle, - string.IsNullOrEmpty(input.TestSuffix) ? "n/a" : input.TestSuffix, - input.File.Exists.ToString(), - input.Format.ToString(), - (!input.File.Exists || input.Format == InputFormatType.Unknown).ToString()); + foreach (var file in input.Files) + { + table.AddRow(file.Name, + string.IsNullOrEmpty(input.GroupTitle) ? "default" : input.GroupTitle, + string.IsNullOrEmpty(input.TestSuffix) ? "n/a" : input.TestSuffix, + file.Exists.ToString(), + input.Format.ToString(), + (!file.Exists || input.Format == InputFormatType.Unknown).ToString()); + } } _standardConsole.WriteLine(); - _standardConsole.Render(table); + _standardConsole.Write(table); } private void WriteHeader(string title) { _standardConsole.WriteLine(); - _standardConsole.Render(new FigletText("Liquid Test Reports").Centered().Color(Color.Blue)); + _standardConsole.Write(new FigletText("Liquid Test Reports").Centered().Color(Color.Blue)); _standardConsole.WriteLine(); - _standardConsole.Render(new FigletText("Cli Tool").Centered().Color(Color.White)); + _standardConsole.Write(new FigletText("Cli Tool").Centered().Color(Color.White)); _standardConsole.WriteLine(); _standardConsole.WriteLine(); - _standardConsole.Render(new Rule(title).RuleStyle("grey").LeftAligned()); + _standardConsole.Write(new Rule(title).RuleStyle("grey").LeftAligned()); _standardConsole.WriteLine(); } diff --git a/src/LiquidTestReports.Cli/LiquidTestReports.Cli.csproj b/src/LiquidTestReports.Cli/LiquidTestReports.Cli.csproj index caa448d..2973bce 100644 --- a/src/LiquidTestReports.Cli/LiquidTestReports.Cli.csproj +++ b/src/LiquidTestReports.Cli/LiquidTestReports.Cli.csproj @@ -12,8 +12,9 @@ nuget_cli.png - - + + + @@ -47,6 +48,6 @@ LiquidTestReports.Cli LiquidTestReports.Cli 2021 - .NET tool to combine and convert TRX tests reports into Markdown with Liquid template support + .NET tool to combine and convert TRX and JUnit tests reports into Markdown with Liquid template support diff --git a/src/LiquidTestReports.Cli/Models/ReportInput.cs b/src/LiquidTestReports.Cli/Models/ReportInput.cs new file mode 100644 index 0000000..7b1e5bf --- /dev/null +++ b/src/LiquidTestReports.Cli/Models/ReportInput.cs @@ -0,0 +1,129 @@ +using DotLiquid; +using LiquidTestReports.Core.Models; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace LiquidTestReports.Cli.Models +{ + /// + /// Per Test File Configuration + /// + public class ReportInput : IReportInput + { + /// + /// Configuration for test report input + /// + /// + /// + /// Formatted configuration string for test report input - example: "File=TestRun1.trx;GroupTitle=.NETCORE 3.1 Tests;TestSuffix=Windows 10" + /// (Required) File=file-name - The path or glob pattern for input files + /// (Optional) Folder=folder-name - Base directory for finding test files + /// (Optional) Format=report-format - Optional input report format, case insensitive, supported values are `Trx` of `JUnit` + /// (Optional) GroupTitle=group-title - Optional title to group reports under, test runs with the same group title will be merged + /// (Optional) TestPrefix=optional:test-prefix - Optional test suffix, if provided test origination for the provided report will have the suffix appended to its name + /// + public ReportInput(string inputString) + { + if(string.IsNullOrWhiteSpace(inputString)) + throw new ArgumentNullException(nameof(inputString)); + + var splitInputs = inputString.Split(';'); + var parameters = new Dictionary(Template.NamingConvention.StringComparer); + foreach (var input in splitInputs) + { + var parameter = input.Split('='); + if (parameter.Length == 2 && !string.IsNullOrEmpty(parameter[0]) && !string.IsNullOrEmpty(parameter[1])) + { + parameters.Add(parameter[0], parameter[1]); + } + else + { + throw new ArgumentException($"Incorrect number of arguments provided, Confirm parameter '{inputString}' uses the convention of 'key=value;'"); + } + } + + if (parameters.TryGetValue(nameof(Folder), out var folder)) + { + Folder = new DirectoryInfo(folder); + } + + if (parameters.TryGetValue(nameof(File), out var file)) + { + FileInfo fileInfo = null; + + if (Folder is null) + { + fileInfo = new FileInfo(file); + } + + if (fileInfo?.Exists is true) + { + Files = new[] { fileInfo }; + } + else // treat as glob pattern + { + var workingFolder = Folder ?? new DirectoryInfo(Directory.GetCurrentDirectory()); + var results = new Matcher() + .AddInclude(file) + .Execute(new DirectoryInfoWrapper(workingFolder)); + + Files = results.HasMatches ? + results.Files.Select(match => new FileInfo(match.Path)) : + throw new ArgumentException("File did not match any files"); + } + } + else + { + throw new ArgumentNullException("No parameter file name has been provided"); + } + + if (parameters.TryGetValue(nameof(GroupTitle), out var title)) + { + GroupTitle = title; + } + + if (parameters.TryGetValue(nameof(TestSuffix), out var testPrefix)) + { + TestSuffix = testPrefix; + } + + if (parameters.TryGetValue(nameof(Format), out var format)) + { + Format = Enum.TryParse(format, true, out var formatType) + ? formatType + : InputFormatType.Unknown; + } + } + + /// + /// Base directory for finding test files + /// + public DirectoryInfo Folder { get; } + + /// + /// File containing test content + /// + public IEnumerable Files { get; } + + /// + /// Test file format, if not provided, defaults to Trx + /// + public InputFormatType Format { get; } = InputFormatType.Trx; + + /// + /// Test group title, title used for all tests from this input, + /// Where group titles are the same, results will be merged into the same group + /// + public string GroupTitle { get; } + + /// + /// Test suffix, this prefix will be appended to individual test titles + /// + public string TestSuffix { get; } + + } +} diff --git a/src/LiquidTestReports.Cli/Program.cs b/src/LiquidTestReports.Cli/Program.cs index fd747be..0929313 100644 --- a/src/LiquidTestReports.Cli/Program.cs +++ b/src/LiquidTestReports.Cli/Program.cs @@ -1,4 +1,5 @@ -using LiquidTestReports.Core.Models; +using LiquidTestReports.Cli.Models; +using LiquidTestReports.Core.Models; using System; using System.IO; using System.Threading.Tasks; diff --git a/src/LiquidTestReports.Cli/Services/InputProcessingService.cs b/src/LiquidTestReports.Cli/Services/InputProcessingService.cs index 6722480..84e78b5 100644 --- a/src/LiquidTestReports.Cli/Services/InputProcessingService.cs +++ b/src/LiquidTestReports.Cli/Services/InputProcessingService.cs @@ -1,5 +1,6 @@ using LiquidTestReports.Cli.adapters; using LiquidTestReports.Cli.Loaders; +using LiquidTestReports.Cli.Models; using LiquidTestReports.Core.Drops; using LiquidTestReports.Core.Mappers; using LiquidTestReports.Core.Models; @@ -30,22 +31,25 @@ internal TestRunDrop Process() foreach (var input in _inputs) { - switch (input.Format) + foreach (var file in input.Files) { - case InputFormatType.Trx: - { - var results = TrxLoader.FromFile(input.File.FullName); - TrxMapper.Map(results, testRunDrop, input); - break; - } - case InputFormatType.JUnit: - { - var results = JUnitLoader.FromFile(input.File.FullName); - JUnitMapper.Map(results, testRunDrop, input); - break; - } - default: - throw new NotImplementedException(); + switch (input.Format) + { + case InputFormatType.Trx: + { + var results = TrxLoader.FromFile(file.FullName); + TrxMapper.Map(results, testRunDrop, input); + break; + } + case InputFormatType.JUnit: + { + var results = JUnitLoader.FromFile(file.FullName); + JUnitMapper.Map(results, testRunDrop, input); + break; + } + default: + throw new NotImplementedException(); + } } } diff --git a/src/LiquidTestReports.Core/Abstractions/IReportInput.cs b/src/LiquidTestReports.Core/Abstractions/IReportInput.cs new file mode 100644 index 0000000..1445833 --- /dev/null +++ b/src/LiquidTestReports.Core/Abstractions/IReportInput.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.IO; + +namespace LiquidTestReports.Core.Models +{ + public interface IReportInput + { + IEnumerable Files { get; } + InputFormatType Format { get; } + string GroupTitle { get; } + string TestSuffix { get; } + } +} \ No newline at end of file diff --git a/src/LiquidTestReports.Core/Mappers/JUnitMapper.cs b/src/LiquidTestReports.Core/Mappers/JUnitMapper.cs index 73b3644..a5b8b2e 100644 --- a/src/LiquidTestReports.Core/Mappers/JUnitMapper.cs +++ b/src/LiquidTestReports.Core/Mappers/JUnitMapper.cs @@ -18,7 +18,7 @@ public static class JUnitMapper /// Instance of test results from deserialised JUnit input /// Instance to map and merge results into /// User configured input for current source - public static void Map(Testsuites source, TestRunDrop destination, ReportInput inputConfiguration = null) + public static void Map(Testsuites source, TestRunDrop destination, IReportInput inputConfiguration = null) { foreach (var testsuite in source.Testsuite) { diff --git a/src/LiquidTestReports.Core/Mappers/TrxMapper.cs b/src/LiquidTestReports.Core/Mappers/TrxMapper.cs index 8cf968d..4b9498d 100644 --- a/src/LiquidTestReports.Core/Mappers/TrxMapper.cs +++ b/src/LiquidTestReports.Core/Mappers/TrxMapper.cs @@ -22,7 +22,7 @@ public class TrxMapper /// Instance of test results from deserialised TRX input /// Instance to map and merge results into /// User configured input for current source - public static void Map(TestRunType source, TestRunDrop destination, ReportInput inputConfiguration = null) + public static void Map(TestRunType source, TestRunDrop destination, IReportInput inputConfiguration = null) { var times = source.Times.FirstOrDefault(); var started = DateTimeOffset.Parse(times.Start); diff --git a/src/LiquidTestReports.Core/Models/ReportInput.cs b/src/LiquidTestReports.Core/Models/ReportInput.cs deleted file mode 100644 index 2e90849..0000000 --- a/src/LiquidTestReports.Core/Models/ReportInput.cs +++ /dev/null @@ -1,85 +0,0 @@ -using DotLiquid; -using System; -using System.Collections.Generic; -using System.IO; - -namespace LiquidTestReports.Core.Models -{ - /// - /// Per Test File Configuration - /// - public class ReportInput - { - /// - /// Configuration for test report input - /// - /// - /// - /// Formatted configuration string for test report input - example: "File=TestRun1.trx;GroupTitle=.NETCORE 3.1 Tests;TestSuffix=Windows 10" - /// (Required) File= - The path of the input file - /// (Optional) GroupTitle= - Optional title to group reports under, test runs with the same group title will be merged - /// (Optional) TestPrefix= - Optional test suffix, if provided test origination for the provided report will have the suffix appended to its name - /// - public ReportInput(string inputString) - { - var splitInputs = inputString.Split(';'); - var parameters = new Dictionary(Template.NamingConvention.StringComparer); - foreach (var input in splitInputs) - { - var parameter = input.Split('='); - if (parameter.Length == 2) - { - parameters.Add(parameter[0], parameter[1]); - } - else - { - throw new ArgumentException($"Incorrect number of arguments provided, Confirm parameter '{parameter}' uses the convention of 'key=value;'"); - } - } - - File = parameters.TryGetValue(nameof(File), out var file) - ? new FileInfo(file) - : throw new ArgumentNullException("No parameter file name has been provided"); - - - if (parameters.TryGetValue(nameof(GroupTitle), out var title)) - { - GroupTitle = title; - } - - if (parameters.TryGetValue(nameof(TestSuffix), out var testPrefix)) - { - TestSuffix = testPrefix; - } - - if (parameters.TryGetValue(nameof(Format), out var format)) - { - Format = Enum.TryParse(format, true, out var formatType) - ? formatType - : InputFormatType.Unknown; - } - } - - /// - /// File containing test content - /// - public FileInfo File { get; } - - /// - /// Test file format, if not provided, defaults to Trx - /// - public InputFormatType Format { get; } = InputFormatType.Trx; - - /// - /// Test group title, title used for all tests from this input, - /// Where group titles are the same, results will be merged into the same group - /// - public string GroupTitle { get; } - - /// - /// Test suffix, this prefix will be appended to individual test titles - /// - public string TestSuffix { get; } - - } -} diff --git a/test/LiquidTestReports.Cli.Tests/Models/ReportInputTests.cs b/test/LiquidTestReports.Cli.Tests/Models/ReportInputTests.cs new file mode 100644 index 0000000..366b578 --- /dev/null +++ b/test/LiquidTestReports.Cli.Tests/Models/ReportInputTests.cs @@ -0,0 +1,60 @@ +using LiquidTestReports.Cli.Models; +using System; +using System.IO; +using Xunit; + +namespace LiquidTestReports.Cli.Tests.Models +{ + public class ReportInputTests + { + [Theory] + [InlineData("", typeof(ArgumentNullException), "Value cannot be null.")] + [InlineData(null, typeof(ArgumentNullException), "Value cannot be null.")] + [InlineData("File=", typeof(ArgumentException), "Incorrect number of arguments provided, Confirm parameter 'File=' uses the convention of 'key=value;'")] + public void ConstructFromString_WithInvalidInput_ThrowsException(string input, Type exeptionType, string errorMessage) + { + // Arrange + var inputString = input; + var expectedExceptionMessage = errorMessage; + + // Act + void reportInput() => new ReportInput(input); + + // Assert + var exception = Assert.Throws(exeptionType, reportInput); + Assert.Contains(expectedExceptionMessage, exception.Message); + } + + [Theory] + [InlineData("File=*.pattern-not-a-file")] + [InlineData(@"File=C:\null\absolute-not-a-file.trx")] + public void ConstructFromString_WithNoFilesMatched_ThrowsArguementException(string input) + { + // Arrange + var inputString = input; + var expectedExceptionMessage = "File did not match any files"; + + // Act + void reportInput() => new ReportInput(input); + + // Assert + var exception = Assert.Throws(reportInput); + Assert.Contains(expectedExceptionMessage, exception.Message); + } + + [Theory] + [InlineData("File=**/*junit-sample.xml;Folder={0};Format=JUnit;GroupTitle=JUnit Tests")] + [InlineData("File=**/*sample.trx;Folder={0};Format=Trx;GroupTitle=Trx Tests")] + public void ConstructFromString_WithFilesMatched_ContainsExpectedFiles(string input) + { + // Arrange + var inputString = string.Format(input, Environment.CurrentDirectory); + + // Act + var reportInput = new ReportInput(inputString); + + // Assert + Assert.NotEmpty(reportInput.Files); + } + } +} diff --git a/test/LiquidTestReports.Cli.Tests/ProgramTests.cs b/test/LiquidTestReports.Cli.Tests/ProgramTests.cs index b200fb6..582b09e 100644 --- a/test/LiquidTestReports.Cli.Tests/ProgramTests.cs +++ b/test/LiquidTestReports.Cli.Tests/ProgramTests.cs @@ -1,4 +1,5 @@ -using LiquidTestReports.Core.Models; +using LiquidTestReports.Cli.Models; +using LiquidTestReports.Core.Models; using System; using System.Collections.Generic; using System.IO; @@ -115,5 +116,45 @@ public void Main_JUnitWithTitle_GeneratesReport() // Assert Assert.True(destinationReport.Exists); } + + [Fact] + public void Main_JUnitAndTrxGlob_GeneratesReport() + { + //Arrange + var title = "My Full Stack Test Report (JUnit + TRX)"; + var titleTest = "junitTest.md"; + var destinationReport = new FileInfo(Path.Combine(_outputFolder, titleTest)); + var files = new List + { + new ReportInput($"File=**/*junit-sample.xml;Folder={Environment.CurrentDirectory};Format=JUnit;GroupTitle=JUnit Tests"), + new ReportInput($"File=**/*sample.trx;Folder={Environment.CurrentDirectory};Format=Trx;GroupTitle=Trx Tests") + }; + + // Act + Program.Main(files.ToArray(), destinationReport, title); + + // Assert + Assert.True(destinationReport.Exists); + } + + [Fact] + public void Main_JUnitAndTrxGlosb_GeneratesReport() + { + //Arrange + var title = "My Full Stack Test Report (JUnit + TRX)"; + var titleTest = "junitTest.md"; + var destinationReport = new FileInfo(Path.Combine(_outputFolder, titleTest)); + var files = new List + { + new ReportInput($"File=*/*xunit*.trx;Format=Trx;GroupTitle=Trx Tests") + }; + + // Act + Program.Main(files.ToArray(), destinationReport, title); + + // Assert + Assert.True(destinationReport.Exists); + } + } }