Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 31 additions & 39 deletions scripts/create-windows-html-report/create-windows-html-report.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using System.Text;
using System.Xml;

using Xamarin.Utils;

public class Program {
static string GetOutcomeColor (string outcome)
{
Expand Down Expand Up @@ -120,50 +122,40 @@ public static int Main (string [] args)
foreach (var trx in trxFiles) {
var name = trx.Name;
var path = trx.TestResults;
string? outcome;
var messageLines = new List<string> ();

try {
var xml = new XmlDocument ();
xml.Load (path);
outcome = xml.SelectSingleNode ("/*[local-name() = 'TestRun']/*[local-name() = 'ResultSummary']")?.Attributes? ["outcome"]?.Value;
if (outcome is null) {
outcome = $"Could not find outcome in trx file {path}";
} else {
var failedTests = xml.SelectNodes ("/*[local-name() = 'TestRun']/*[local-name() = 'Results']/*[local-name() = 'UnitTestResult'][@outcome != 'Passed']")?.Cast<XmlNode> ();
if (failedTests?.Any () == true) {
messageLines.Add (" <ul>");
foreach (var node in failedTests) {
var testName = node.Attributes? ["testName"]?.Value ?? "<unknown test name>";
var testOutcome = node.Attributes? ["outcome"]?.Value ?? "<unknown test outcome>";
var testMessage = node.SelectSingleNode ("*[local-name() = 'Output']/*[local-name() = 'ErrorInfo']/*[local-name() = 'Message']")?.InnerText;

var testId = node.Attributes? ["testId"]?.Value;
if (!string.IsNullOrEmpty (testId)) {
var testMethod = xml.SelectSingleNode ($"/*[local-name() = 'TestRun']/*[local-name() = 'TestDefinitions']/*[local-name() = 'UnitTest'][@id='{testId}']/*[local-name() = 'TestMethod']");
var className = testMethod?.Attributes? ["className"]?.Value ?? string.Empty;
if (!string.IsNullOrEmpty (className))
testName = className + "." + testName;
}

if (string.IsNullOrEmpty (testMessage)) {
messageLines.Add ($" <li>{testName} (<span style='color: {GetOutcomeColor (testOutcome)}'>{testOutcome}</span>)</li>");
} else if (testMessage.Split ('\n').Length == 1) {
messageLines.Add ($" <li>{testName} (<span style='color: {GetOutcomeColor (testOutcome)}'>{testOutcome}</span>): {FormatHtml (testMessage)}</li>");
} else {
messageLines.Add ($" <li>{testName} (<span style='color: {GetOutcomeColor (testOutcome)}'>{testOutcome}</span>)");
messageLines.Add ($" <div class='pdiv' style='margin-left: 20px;'>");
messageLines.Add (FormatHtml (testMessage));
messageLines.Add ($" </div>");
messageLines.Add ($" </li>");
}
if (TrxParser.TryParseTrxFile (path, out var failedTests, out var outcome, out allTestsSucceeded, out var ex)) {
if (failedTests?.Any () == true) {
messageLines.Add (" <ul>");
foreach (var ft in failedTests) {
var testName = ft.Name;
var testOutcome = ft.Outcome;
var testMessage = ft.Message;

if (string.IsNullOrEmpty (testMessage)) {
messageLines.Add ($" <li>{testName} (<span style='color: {GetOutcomeColor (testOutcome)}'>{testOutcome}</span>)</li>");
} else if (testMessage.Split ('\n').Length == 1) {
messageLines.Add ($" <li>{testName} (<span style='color: {GetOutcomeColor (testOutcome)}'>{testOutcome}</span>): {FormatHtml (testMessage)}</li>");
} else {
messageLines.Add ($" <li>{testName} (<span style='color: {GetOutcomeColor (testOutcome)}'>{testOutcome}</span>)");
messageLines.Add ($" <div class='pdiv' style='margin-left: 20px;'>");
messageLines.Add (FormatHtml (testMessage));
messageLines.Add ($" </div>");
messageLines.Add ($" </li>");
}
messageLines.Add (" </ul>");
allTestsSucceeded = false;
} else if (outcome != "Completed" && outcome != "Passed") {
messageLines.Add ($" Failed to find any test failures in the trx file {path}");
}
messageLines.Add (" </ul>");
} else if (outcome != "Completed" && outcome != "Passed") {
messageLines.Add ($" Failed to find any test failures in the trx file {path}");
}
} else {
outcome = "Failed to parse test results";
if (ex is not null)
messageLines.Add ($"<div>{FormatHtml (ex.ToString ())}</div>");
allTestsSucceeded = false;
}

try {
var htmlPath = Path.ChangeExtension (path, "html");
if (File.Exists (htmlPath)) {
var relativeHtmlPath = Path.GetRelativePath (outputDirectory, htmlPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="../../tests/common/ParseTrxFile.cs" />
</ItemGroup>
</Project>
77 changes: 77 additions & 0 deletions tests/common/ParseTrxFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
namespace Xamarin.Utils;

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Xml;

#nullable enable

public class TrxParser {
public class TrxTestResult {
public required string Name;
public required string Outcome;
public required string Message;

public bool Passed { get => Outcome == "Passed"; }
}

public static bool TryParseTrxFile (string path, [NotNullWhen (true)] out IList<TrxTestResult>? failedTests, [NotNullWhen (true)] out string? outcome, out bool allTestsSucceeded, out Exception? exception)
{
allTestsSucceeded = false;
failedTests = null;
outcome = null;
exception = null;

if (!File.Exists (path))
return false;

var rv = new List<TrxTestResult> ();
try {
var xml = new XmlDocument ();
xml.Load (path);
outcome = xml.SelectSingleNode ("/*[local-name() = 'TestRun']/*[local-name() = 'ResultSummary']")?.Attributes? ["outcome"]?.Value;
if (outcome is null) {
outcome = $"Could not find outcome in trx file {path}";
} else {
var failedTestsQuery = xml.SelectNodes ("/*[local-name() = 'TestRun']/*[local-name() = 'Results']/*[local-name() = 'UnitTestResult'][@outcome != 'Passed']")?.Cast<XmlNode> ();
if (failedTestsQuery?.Any () == true) {
foreach (var node in failedTestsQuery) {
var testName = node.Attributes? ["testName"]?.Value ?? "<unknown test name>";
var testOutcome = node.Attributes? ["outcome"]?.Value ?? "<unknown test outcome>";
var testMessage = node.SelectSingleNode ("*[local-name() = 'Output']/*[local-name() = 'ErrorInfo']/*[local-name() = 'Message']")?.InnerText ?? "";

var testId = node.Attributes? ["testId"]?.Value;
if (!string.IsNullOrEmpty (testId)) {
var testMethod = xml.SelectSingleNode ($"/*[local-name() = 'TestRun']/*[local-name() = 'TestDefinitions']/*[local-name() = 'UnitTest'][@id='{testId}']/*[local-name() = 'TestMethod']");
var className = testMethod?.Attributes? ["className"]?.Value ?? string.Empty;
if (!string.IsNullOrEmpty (className))
testName = className + "." + testName;
}

rv.Add (new TrxTestResult () {
Name = testName,
Outcome = testOutcome,
Message = testMessage,
});
}
allTestsSucceeded = false;
} else if (outcome != "Completed" && outcome != "Passed") {
// failed, but no test failures?
allTestsSucceeded = false;
} else {
allTestsSucceeded = true;
}
}
failedTests = rv;
return true;
} catch (Exception e) {
outcome = "Failed to parse test results";
exception = e;
allTestsSucceeded = false;
return false;
}
}
}
25 changes: 25 additions & 0 deletions tests/xharness/Jenkins/Reports/MarkdownReportWriter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.DotNet.XHarness.iOS.Shared.Logging;
using Xharness.Jenkins.TestTasks;

#nullable enable
Expand Down Expand Up @@ -30,6 +32,15 @@ void WriteFailedTestsDetails (IEnumerable<ITestTask> failedTests, StreamWriter w
if (test.KnownFailure is not null)
writer.Write ($" Known issue: [{test.KnownFailure.HumanMessage}]({test.KnownFailure.IssueLink})");
writer.WriteLine ();
var trxLog = test.Logs.FirstOrDefault (v => v.Description == LogType.TrxLog.ToString ());
if (trxLog is not null && Xamarin.Utils.TrxParser.TryParseTrxFile (trxLog.FullPath, out var failedTrxTests, out var outcome, out var allTestsSucceeded, out var exception)) {
foreach (var failedTrxTest in failedTrxTests.OrderBy (v => v.Name).Take (3)) {
writer.WriteLine ($" * {failedTrxTest.Name.Cap (64)}: {failedTrxTest.Message.Cap (128)}");
}
if (failedTrxTests.Count > 3) {
writer.WriteLine ($" * ... and {failedTrxTests.Count - 3} more");
}
}
}
continue;
}
Expand Down Expand Up @@ -93,4 +104,18 @@ public void Write (IList<ITestTask> allTasks, StreamWriter writer)
writer.Flush ();
}
}

static class StringExtensions {
[return: NotNullIfNotNull (nameof (value))]
public static string? Cap (this string? value, int length)
{
if (string.IsNullOrEmpty (value))
return value;

if (value.Length <= length)
return value;

return value [0..length] + "...";
}
}
}
3 changes: 3 additions & 0 deletions tests/xharness/xharness.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
<Link>ApplePlatform.cs</Link>
</Compile>
<Compile Include="IAppBundleInformationParserExtensions.cs" />
<Compile Include="..\common\ParseTrxFile.cs">
<Link>ParseTrxFile.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\tools\common\SdkVersions.cs">
Expand Down
40 changes: 25 additions & 15 deletions tools/devops/automation/scripts/TestResults.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -392,26 +392,36 @@ class ParallelTestsResults {
$stringBuilder.AppendLine($this.GetDownloadLinks($r))
$stringBuilder.AppendLine("")
} else {
# create a detail per test result with the name of the test and will contain the exact summary
$stringBuilder.AppendLine("<summary>$($result.Failed) tests failed, $($result.Passed) tests passed.</summary>")
$stringBuilder.AppendLine("<details>")
$addSummary = $true
$startLine = 0
if (Test-Path -Path $r.ResultsPath -PathType Leaf) {
$stringBuilder.AppendLine("")
$foundTests = $false
foreach ($line in Get-Content -Path $r.ResultsPath)
{
if (-not $foundTests) {
$foundTests = $line.Contains("## Failed tests")
} else {
if (-not [string]::IsNullOrEmpty($line)) {
$stringBuilder.AppendLine("$line") # the extra space is needed for the multiline list item
}
$resultLines = Get-Content -Path $r.ResultsPath
for ($i = 0; $i -lt $resultLines.Length; $i++) {
$line = $resultLines[$i]
if ($line.Contains ("<details>")) {
$startLine = $i
$addSummary = $false
break
} elseif ($line.Contains("## Failed tests")) {
$startLine = $i + 1
break
}
}
} else {
$stringBuilder.AppendLine(" Test has no summary file.")
$resultLines = @("Test has no summary file.")
}
$stringBuilder.AppendLine("</details>")

if ($addSummary) {
$stringBuilder.AppendLine("<summary>$($result.Failed) tests failed, $($result.Passed) tests passed.</summary>")
$stringBuilder.AppendLine("<details>")
}
for ($i = $startLine; $i -lt $resultLines.Length; $i++) {
$stringBuilder.AppendLine($resultLines[$i])
}
if ($addSummary) {
$stringBuilder.AppendLine("</details>")
}

$stringBuilder.AppendLine("")
$stringBuilder.AppendLine($this.GetDownloadLinks($r))
$stringBuilder.AppendLine("")
Expand Down
Loading