From 131498e469e0240953e0ab3796ba1f75f41d0b79 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Mon, 12 Jan 2026 04:56:31 -0800 Subject: [PATCH 1/3] Add ability to limit the trx files or only use the latest file --- readme.md | 10 ++++++++++ src/dotnet-trx/TrxCommand.cs | 33 ++++++++++++++++++++++++++++++++- src/dotnet-trx/help.md | 4 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 8db1b11..89a31a6 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,16 @@ 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 +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/TrxCommand.cs b/src/dotnet-trx/TrxCommand.cs index 37c27a0..23ed67a 100644 --- a/src/dotnet-trx/TrxCommand.cs +++ b/src/dotnet-trx/TrxCommand.cs @@ -109,6 +109,14 @@ 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; } + public override ValidationResult Validate() { // Validate, normalize and default path. @@ -150,8 +158,31 @@ public override int Execute(CommandContext context, TrxSettings settings) Status().Start("Discovering test results...", ctx => { + 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 Directory.EnumerateFiles(path, "*.trx", search).OrderByDescending(File.GetLastWriteTime)) + foreach (var trx in files) { ctx.Status($"Discovering test results in {Path.GetFileName(trx).EscapeMarkup()}..."); using var file = File.OpenRead(trx); diff --git a/src/dotnet-trx/help.md b/src/dotnet-trx/help.md index 21d1839..3195f67 100644 --- a/src/dotnet-trx/help.md +++ b/src/dotnet-trx/help.md @@ -20,4 +20,8 @@ OPTIONS: 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) ``` From 708d84749ed69aeb0627466ddbf0cf12f8a23706 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Tue, 13 Jan 2026 21:12:32 -0800 Subject: [PATCH 2/3] Add ability to limit output to only select tests with --only-tests --- readme.md | 4 ++++ src/dotnet-trx/TrxCommand.cs | 9 +++++++++ src/dotnet-trx/help.md | 3 +++ 3 files changed, 16 insertions(+) diff --git a/readme.md b/readme.md index 89a31a6..30c2f32 100644 --- a/readme.md +++ b/readme.md @@ -39,6 +39,10 @@ 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. diff --git a/src/dotnet-trx/TrxCommand.cs b/src/dotnet-trx/TrxCommand.cs index 23ed67a..58aaa39 100644 --- a/src/dotnet-trx/TrxCommand.cs +++ b/src/dotnet-trx/TrxCommand.cs @@ -117,6 +117,10 @@ public bool Skipped [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. @@ -190,6 +194,11 @@ public override int Execute(CommandContext context, TrxSettings settings) 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)) diff --git a/src/dotnet-trx/help.md b/src/dotnet-trx/help.md index 3195f67..492ddab 100644 --- a/src/dotnet-trx/help.md +++ b/src/dotnet-trx/help.md @@ -24,4 +24,7 @@ OPTIONS: 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 ``` From d0406e285d51005c5d31ce06f0c964f8454e0ac6 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Tue, 13 Jan 2026 21:38:52 -0800 Subject: [PATCH 3/3] Added batch mode (no ansi formatting) and documented / updates no-updates command. --- src/dotnet-trx/ConsoleMode.cs | 8 ++ src/dotnet-trx/Program.cs | 39 ++++++++-- src/dotnet-trx/TrxCommand.cs | 128 +++++++++++++++++++------------ src/dotnet-trx/dotnet-trx.csproj | 2 +- src/dotnet-trx/help.md | 42 +++++----- 5 files changed, 140 insertions(+), 79 deletions(-) create mode 100644 src/dotnet-trx/ConsoleMode.cs 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 58aaa39..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; } @@ -136,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; @@ -160,55 +171,17 @@ public override int Execute(CommandContext context, TrxSettings settings) var results = new List(); - Status().Start("Discovering test results...", ctx => + if (ConsoleMode.BatchMode) { - 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) + results = DiscoverResults(settings, path, search); + } + else + { + 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")) - { - 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); - } - } - - 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) { @@ -528,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), "> "); } @@ -586,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 492ddab..3c0ddda 100644 --- a/src/dotnet-trx/help.md +++ b/src/dotnet-trx/help.md @@ -3,28 +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 - --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 + 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 ```