From a7101fc4f20ec71cdd4327f3b6e9eaa1dcd7311d Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Thu, 4 Jul 2024 02:53:55 -0300 Subject: [PATCH] Sort test results by name, improve duration calculation By sorting tests by name we make it easier to find visually those that belong logically together. While doing this change, we switch to adding the duration of each individual test counted test run, which is more precise than adding the overal test results file duration which can include a full run from a previous trx (i.e. a later one run partially only failed tests, for example). --- src/dotnet-trx/TrxCommand.cs | 162 +++++++++++++++++++---------------- 1 file changed, 88 insertions(+), 74 deletions(-) diff --git a/src/dotnet-trx/TrxCommand.cs b/src/dotnet-trx/TrxCommand.cs index dde899c..992afbb 100644 --- a/src/dotnet-trx/TrxCommand.cs +++ b/src/dotnet-trx/TrxCommand.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Xml.Linq; using Devlooped.Web; using Humanizer; @@ -15,7 +16,7 @@ namespace Devlooped; -public partial class TrxCommand : Command +public partial class TrxCommand : AsyncCommand { public class TrxSettings : CommandSettings { @@ -46,14 +47,14 @@ public class TrxSettings : CommandSettings public bool? Version { get; init; } } - public override int Execute(CommandContext context, TrxSettings settings) + public override async Task ExecuteAsync(CommandContext context, TrxSettings settings) { var path = settings.Path ?? Directory.GetCurrentDirectory(); if (!Path.IsPathFullyQualified(path)) path = Path.Combine(Directory.GetCurrentDirectory(), path); if (File.Exists(path)) - path = new FileInfo(path).DirectoryName; + path = new FileInfo(path).DirectoryName!; else path = Path.GetFullPath(path); @@ -74,99 +75,112 @@ public override int Execute(CommandContext context, TrxSettings settings) """); - // Process from newest files to oldest - foreach (var trx in Directory.EnumerateFiles(path, "*.trx", search).OrderByDescending(File.GetLastWriteTime)) - { - using var file = File.OpenRead(trx); - // Clears namespaces - var doc = HtmlDocument.Load(file, new HtmlReaderSettings { CaseFolding = Sgml.CaseFolding.None }); + var results = new List(); - foreach (var result in doc.CssSelectElements("UnitTestResult").OrderBy(x => x.Attribute("testName")?.Value)) + Status().Start("Discovering test results...", ctx => + { + // 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)) { - 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)) - continue; + ctx.Status($"Discovering test results in {Path.GetFileName(trx)}..."); + 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)) + continue; - var test = result.Attribute("testName")!.Value; - string? output = settings.Output ? result.CssSelectElement("StdOut")?.Value : default; + results.Add(result); + } + } - switch (result.Attribute("outcome")?.Value) - { - case "Passed": - passed++; - MarkupLine($":check_mark_button: {test}"); - if (output == null) - details.AppendLine($":white_check_mark: {test}"); - else - details.AppendLine( - $""" + ctx.Status("Sorting tests by name..."); + results.Sort(new Comparison((x, y) => x.Attribute("testName")!.Value.CompareTo(y.Attribute("testName")!.Value))); + }); + + foreach (var result in results) + { + 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)) + continue; + + var test = result.Attribute("testName")!.Value; + var elapsed = TimeSpan.Parse(result.Attribute("duration")!.Value); + var output = settings.Output ? result.CssSelectElement("StdOut")?.Value : default; + + switch (result.Attribute("outcome")?.Value) + { + case "Passed": + passed++; + duration += elapsed; + MarkupLine($":check_mark_button: {test}"); + if (output == null) + details.AppendLine($":white_check_mark: {test}"); + else + details.AppendLine( + $"""
:white_check_mark: {test} """) - .AppendLineIndented(output, "> > ") - .AppendLine( - """ + .AppendLineIndented(output, "> > ") + .AppendLine( + """
"""); - break; - case "Failed": - failed++; - MarkupLine($":cross_mark: {test}"); - details.AppendLine( - $""" + break; + case "Failed": + failed++; + duration += elapsed; + MarkupLine($":cross_mark: {test}"); + details.AppendLine( + $"""
:x: {test} """); - WriteError(path, failures, result, details); - if (output != null) - details.AppendLineIndented(output, "> > "); - details.AppendLine().AppendLine("
").AppendLine(); - break; - case "NotExecuted": - if (!settings.Skipped) - break; - - skipped++; - var reason = result.CssSelectElement("Output > ErrorInfo > Message")?.Value; - Markup($"[dim]:white_question_mark: {test}[/]"); - details.Append($":grey_question: {test}"); - - if (reason != null) - { - Markup($"[dim] => {reason}[/]"); - details.Append($" => {reason}"); - } - - WriteLine(); - details.AppendLine(); + WriteError(path, failures, result, details); + if (output != null) + details.AppendLineIndented(output, "> > "); + details.AppendLine().AppendLine("").AppendLine(); + break; + case "NotExecuted": + if (!settings.Skipped) break; - default: - break; - } - if (output != null) - { - Write(new Panel($"[dim]{output.ReplaceLineEndings()}[/]") + skipped++; + var reason = result.CssSelectElement("Output > ErrorInfo > Message")?.Value; + Markup($"[dim]:white_question_mark: {test}[/]"); + details.Append($":grey_question: {test}"); + + if (reason != null) { - Border = BoxBorder.None, - Padding = new Padding(5, 0, 0, 0), - }); - } + Markup($"[dim] => {reason}[/]"); + details.Append($" => {reason}"); + } + + WriteLine(); + details.AppendLine(); + break; + default: + break; } - var times = doc.CssSelectElement("Times"); - if (times == null) - continue; - - var start = DateTime.Parse(times.Attribute("start")!.Value); - var finish = DateTime.Parse(times.Attribute("finish")!.Value); - duration += finish - start; + if (output != null) + { + Write(new Panel($"[dim]{output.ReplaceLineEndings()}[/]") + { + Border = BoxBorder.None, + Padding = new Padding(5, 0, 0, 0), + }); + } } details.AppendLine().AppendLine("");