diff --git a/readme.md b/readme.md index 8db1b11..30c2f32 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,20 @@ And view results in an automatic pull request comment like: > NOTE: this behavior is triggered by the presence of the `GITHUB_REF_NAME` and `CI` environment variables. +--only-latest - Only use the most recently modified TRX file: + +trx --only-latest + + +--only-files - Specify specific TRX files to include (reads all files until the next -- flag): + +trx --only-files test1.trx test2.trx + +--only-tests - Specify one or more tests (until next -- flag) that are the only tests included in the output report rather than all the tests: + +trx --only-tests Test1 Test2 +The --only-files option supports both absolute and relative paths. Relative paths are first tried against the trx directory, then fall back to the current working directory. + ```shell USAGE: diff --git a/src/dotnet-trx/ConsoleMode.cs b/src/dotnet-trx/ConsoleMode.cs new file mode 100644 index 0000000..681f87e --- /dev/null +++ b/src/dotnet-trx/ConsoleMode.cs @@ -0,0 +1,8 @@ +namespace Devlooped; + +static class ConsoleMode +{ + public static bool BatchMode { get; set; } + public static bool NoColor { get; set; } + public static bool NoUpdates { get; set; } +} diff --git a/src/dotnet-trx/Program.cs b/src/dotnet-trx/Program.cs index 5650a7e..863e11f 100644 --- a/src/dotnet-trx/Program.cs +++ b/src/dotnet-trx/Program.cs @@ -1,5 +1,6 @@ // See https://aka.ms/new-console-template for more information using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -11,11 +12,35 @@ using Spectre.Console; using Spectre.Console.Cli; +ConsoleMode.BatchMode = args.Contains("--batch"); +ConsoleMode.NoColor = Environment.GetEnvironmentVariables().Contains("NO_COLOR") || ConsoleMode.BatchMode; +ConsoleMode.NoUpdates = args.Contains("--no-updates") || args.Contains("-u") || ConsoleMode.BatchMode; + + +if (ConsoleMode.BatchMode) +{ + // Ensure Spectre does not emit any VT/ANSI/OSC sequences (colors, cursor movement, hyperlinks, etc.) + AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Interactive = InteractionSupport.No, + }); + AnsiConsole.Profile.Width = 999999; + +} + var app = new CommandApp(); // Alias -? to -h for help -if (args.Contains("-?")) - args = args.Select(x => x == "-?" ? "-h" : x).ToArray(); +Dictionary MigratedCLI = new() { {"-?","-h"}, {"--unattended","-u" } }; +foreach (var toMigrate in MigratedCLI) +{ + var pos = Array.IndexOf(args,toMigrate.Key); + if (pos == -1) + continue; + args[pos] = toMigrate.Value; +} if (args.Contains("--debug")) Debugger.Launch(); @@ -23,14 +48,18 @@ app.Configure(config => { config.SetApplicationName(ThisAssembly.Project.ToolCommandName); - if (Environment.GetEnvironmentVariables().Contains("NO_COLOR")) + if (ConsoleMode.NoColor) config.Settings.HelpProviderStyles = null; }); if (args.Contains("--version")) { AnsiConsole.MarkupLine($"{ThisAssembly.Project.ToolCommandName} version [lime]{ThisAssembly.Project.Version}[/] ({ThisAssembly.Project.BuildDate})"); - AnsiConsole.MarkupLine($"[link]{ThisAssembly.Git.Url}/releases/tag/{ThisAssembly.Project.BuildRef}[/]"); + + if (ConsoleMode.BatchMode) + AnsiConsole.WriteLine($"{ThisAssembly.Git.Url}/releases/tag/{ThisAssembly.Project.BuildRef}"); + else + AnsiConsole.MarkupLine($"[link]{ThisAssembly.Git.Url}/releases/tag/{ThisAssembly.Project.BuildRef}[/]"); foreach (var message in await CheckUpdates(args)) AnsiConsole.MarkupLine(message); @@ -51,7 +80,7 @@ static async Task CheckUpdates(string[] args) { - if (args.Contains("-u") || args.Contains("--unattended")) + if (ConsoleMode.NoUpdates) return []; var providers = Repository.Provider.GetCoreV3(); diff --git a/src/dotnet-trx/TrxCommand.cs b/src/dotnet-trx/TrxCommand.cs index 37c27a0..d0aacb1 100644 --- a/src/dotnet-trx/TrxCommand.cs +++ b/src/dotnet-trx/TrxCommand.cs @@ -42,6 +42,14 @@ public class TrxSettings : CommandSettings [CommandOption("--version")] public bool Version { get; init; } + [Description("Do not output VT/ANSI formatting codes or progress status messages (emojis are still shown). No update check.")] + [CommandOption("--batch")] + public bool BatchMode { get; set; } + + [Description("No update check")] + [CommandOption("-u|--no-updates")] + public bool NoUpdates { get; set; } + [Description("Optional base directory for *.trx files discovery. Defaults to current directory.")] [CommandOption("-p|--path")] public string? Path { get; set; } @@ -109,6 +117,18 @@ public bool Skipped [DefaultValue(true)] public bool GitHubSummary { get; set; } = true; + [Description("Only use the most recently modified TRX file")] + [CommandOption("--only-latest")] + public bool OnlyLatest { get; set; } + + [Description("Specify specific TRX files to include (reads all files until the next -- flag)")] + [CommandOption("--only-files")] + public string[]? OnlyFiles { get; set; } + + [Description("Specify one or more tests (until next -- flag) that are the only tests included in the output report rather than all the tests")] + [CommandOption("--only-tests")] + public string[]? OnlyTests { get; set; } + public override ValidationResult Validate() { // Validate, normalize and default path. @@ -124,13 +144,16 @@ public override ValidationResult Validate() public override int Execute(CommandContext context, TrxSettings settings) { + // these should have already been handled but just incase + ConsoleMode.BatchMode |= settings.BatchMode; + ConsoleMode.NoUpdates |= settings.NoUpdates; + if (Environment.GetEnvironmentVariable("RUNNER_DEBUG") == "1") WriteLine(JsonSerializer.Serialize(new { settings }, indentedJson)); // We get this validated by the settings, so it's always non-null. var path = settings.Path!; var search = settings.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - var testIds = new HashSet(); var passed = 0; var failed = 0; var skipped = 0; @@ -148,27 +171,17 @@ public override int Execute(CommandContext context, TrxSettings settings) var results = new List(); - Status().Start("Discovering test results...", ctx => + if (ConsoleMode.BatchMode) + { + results = DiscoverResults(settings, path, search); + } + else { - // Process from newest files to oldest so that newest result we find (by test id) is the one we keep - foreach (var trx in Directory.EnumerateFiles(path, "*.trx", search).OrderByDescending(File.GetLastWriteTime)) + Status().Start("Discovering test results...", ctx => { - ctx.Status($"Discovering test results in {Path.GetFileName(trx).EscapeMarkup()}..."); - using var file = File.OpenRead(trx); - // Clears namespaces - var doc = HtmlDocument.Load(file, new HtmlReaderSettings { CaseFolding = Sgml.CaseFolding.None }); - foreach (var result in doc.CssSelectElements("UnitTestResult")) - { - var id = result.Attribute("testId")!.Value; - // Process only once per test id, this avoids duplicates when multiple trx files are processed - if (testIds.Add(id)) - results.Add(result); - } - } - - ctx.Status("Sorting tests by name..."); - results.Sort(new Comparison((x, y) => x.Attribute("testName")!.Value.CompareTo(y.Attribute("testName")!.Value))); - }); + results = DiscoverResults(settings, path, search, s => ctx.Status = s); + }); + } foreach (var result in results) { @@ -488,7 +501,10 @@ void WriteError(string baseDir, List failures, XElement result, StringBu stackTrace.ReplaceLineEndings(), relative, int.Parse(pos)); - cli.AppendLine(line.Replace(file, $"[link={file}][steelblue1_1]{relative}[/][/]")); + if (ConsoleMode.BatchMode) + cli.AppendLine(line.Replace(file, relative.EscapeMarkup())); + else + cli.AppendLine(line.Replace(file, $"[link={file}][steelblue1_1]{relative.EscapeMarkup()}[/][/]")); // TODO: can we render a useful link in comment details? details.AppendLineIndented(line.Replace(file, relative), "> "); } @@ -546,4 +562,58 @@ record Summary(int Passed, int Failed, int Skipped, TimeSpan Duration) } record Failed(string Test, string Title, string Message, string File, int Line); + + static List DiscoverResults(TrxSettings settings, string path, SearchOption search, Action? status = null) + { + var testIds = new HashSet(); + var results = new List(); + + IEnumerable files; + + if (settings.OnlyFiles is { Length: > 0 } onlyFiles) + { + files = onlyFiles.Select(f => + { + var p1 = System.IO.Path.Combine(path, f); + if (File.Exists(p1)) return p1; + var p2 = System.IO.Path.Combine(Directory.GetCurrentDirectory(), f); + if (File.Exists(p2)) return p2; + return f; + }); + } + else + { + files = Directory.EnumerateFiles(path, "*.trx", search); + } + + files = files.OrderByDescending(File.GetLastWriteTime); + + if (settings.OnlyLatest) + files = files.Take(1); + + // Process from newest files to oldest so that newest result we find (by test id) is the one we keep + foreach (var trx in files) + { + status?.Invoke($"Discovering test results in {Path.GetFileName(trx).EscapeMarkup()}..."); + using var file = File.OpenRead(trx); + // Clears namespaces + var doc = HtmlDocument.Load(file, new HtmlReaderSettings { CaseFolding = Sgml.CaseFolding.None }); + foreach (var result in doc.CssSelectElements("UnitTestResult")) + { + if (settings.OnlyTests is { Length: > 0 } onlyTests && + result.Attribute("testName")?.Value is string name && + !onlyTests.Contains(name)) + continue; + + var id = result.Attribute("testId")!.Value; + // Process only once per test id, this avoids duplicates when multiple trx files are processed + if (testIds.Add(id)) + results.Add(result); + } + } + + status?.Invoke("Sorting tests by name..."); + results.Sort(new Comparison((x, y) => x.Attribute("testName")!.Value.CompareTo(y.Attribute("testName")!.Value))); + return results; + } } diff --git a/src/dotnet-trx/dotnet-trx.csproj b/src/dotnet-trx/dotnet-trx.csproj index 5504ace..c2da5ce 100644 --- a/src/dotnet-trx/dotnet-trx.csproj +++ b/src/dotnet-trx/dotnet-trx.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/dotnet-trx/help.md b/src/dotnet-trx/help.md index 21d1839..3c0ddda 100644 --- a/src/dotnet-trx/help.md +++ b/src/dotnet-trx/help.md @@ -3,21 +3,22 @@ USAGE: trx [OPTIONS] OPTIONS: - DEFAULT - -h, --help Prints help information - --version Prints version information - -p, --path Optional base directory for *.trx files - discovery. Defaults to current directory - -o, --output Include test output - -r, --recursive True Recursively search for *.trx files - -v, --verbosity Quiet Output display verbosity: - - quiet: only failed tests are displayed - - normal: failed and skipped tests are - displayed - - verbose: failed, skipped and passed tests - are displayed - --no-exit-code Do not return a -1 exit code on test - failures - --gh-comment True Report as GitHub PR comment - --gh-summary True Report as GitHub step summary + DEFAULT + -h, --help Prints help information + --version Prints version information + --batch Do not output VT/ANSI formatting codes or progress status messages (emojis are still shown). No update check + -u, --no-updates No update check + -p, --path Optional base directory for *.trx files discovery. Defaults to current directory + -o, --output Include test output + -r, --recursive True Recursively search for *.trx files + -v, --verbosity Quiet Output display verbosity: + - quiet: only failed tests are displayed + - normal: failed and skipped tests are displayed + - verbose: failed, skipped and passed tests are displayed + --no-exit-code Do not return a -1 exit code on test failures + --gh-comment True Report as GitHub PR comment + --gh-summary True Report as GitHub step summary + --only-latest Only use the most recently modified TRX file + --only-files Specify specific TRX files to include (reads all files until the next -- flag) + --only-tests Specify one or more tests (until next -- flag) that are the only tests included in the output report rather than all the tests ```