Skip to content

Commit f555322

Browse files
authored
feat!: [junit] #153 Add properties support on test case level for test logger (#154)
* feat: [testlogger] Parse traits for all supported testing frameworks * feat: [junit] Add properties support for testcases * ci: Enable color * test: [junit] Add xunit acceptance tests
1 parent ce2c07e commit f555322

File tree

16 files changed

+360
-22
lines changed

16 files changed

+360
-22
lines changed

.github/workflows/dotnet.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ jobs:
4545
needs: [version]
4646
env:
4747
APP_BUILD_VERSION: ${{ needs.version.outputs.build_version }}
48+
# force color output for dotnet
49+
DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1"
50+
TERM: "xterm"
4851
steps:
4952
- uses: actions/checkout@v4
5053
- name: Setup .NET 8.0
@@ -55,6 +58,13 @@ jobs:
5558
uses: actions/setup-dotnet@v1
5659
with:
5760
dotnet-version: "3.1.x"
61+
- name: Configure NuGet cache
62+
uses: actions/cache@v3
63+
with:
64+
path: ~/.nuget/packages
65+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
66+
restore-keys: |
67+
${{ runner.os }}-nuget-
5868
- name: Package (debug)
5969
run: dotnet pack -p:PackageVersion=${{ env.APP_BUILD_VERSION }}
6070
- name: Package (release)

src/JUnit.Xml.TestLogger/JunitXmlSerializer.cs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,67 @@ private static string Indent(IReadOnlyCollection<TestResultMessage> messages)
176176
$"{Environment.NewLine}{indent}",
177177
messages.SelectMany(m =>
178178
m.Text.Split(new string[] { "\r", "\n" }, StringSplitOptions.None)
179-
.Where(x => !string.IsNullOrWhiteSpace(x))
180-
.Select(x => x.Trim())));
179+
.Where(x => !string.IsNullOrWhiteSpace(x))
180+
.Select(x => x.Trim())));
181181
}
182182

183-
private XElement CreateTestSuitesElement(
184-
List<TestResultInfo> results,
185-
TestRunConfiguration runConfiguration,
186-
List<TestMessageInfo> messages)
183+
private static IEnumerable<XElement> CreatePropertyElement(string name, params string[] values)
184+
{
185+
if (string.IsNullOrWhiteSpace(name))
186+
{
187+
throw new ArgumentException("message", nameof(name));
188+
}
189+
190+
foreach (var value in values)
191+
{
192+
yield return new XElement(
193+
"property",
194+
new XAttribute("name", name),
195+
new XAttribute("value", value));
196+
}
197+
}
198+
199+
private static XElement CreatePropertyElement(Trait trait)
200+
{
201+
return CreatePropertyElement(trait.Name, trait.Value).Single();
202+
}
203+
204+
private static XElement CreatePropertiesElement(TestResultInfo result)
205+
{
206+
var propertyElements = new HashSet<XNode>(result.Traits.Select(CreatePropertyElement));
207+
208+
#pragma warning disable CS0618 // Type or member is obsolete
209+
210+
// Required since TestCase.Properties is a superset of TestCase.Traits
211+
// Unfortunately not all NUnit properties are available as traits
212+
var traitProperties = result.Properties;
213+
214+
#pragma warning restore CS0618 // Type or member is obsolete
215+
216+
foreach (var traitProperty in traitProperties)
217+
{
218+
if (traitProperty.Key != "CustomProperty")
219+
{
220+
continue;
221+
}
222+
223+
var propertyDef = traitProperty.Value as string[];
224+
if (propertyDef == null || propertyDef.Length != 2)
225+
{
226+
continue;
227+
}
228+
229+
var propertyElement = CreatePropertyElement(propertyDef[0], propertyDef[1]);
230+
foreach (var element in propertyElement)
231+
{
232+
propertyElements.Add(element);
233+
}
234+
}
235+
236+
return propertyElements.Any() ? new XElement("properties", propertyElements.Distinct()) : null;
237+
}
238+
239+
private XElement CreateTestSuitesElement(List<TestResultInfo> results, TestRunConfiguration runConfiguration, List<TestMessageInfo> messages)
187240
{
188241
var assemblies = results.Select(x => x.AssemblyPath).Distinct().ToList();
189242
var testsuiteElements = assemblies
@@ -266,9 +319,7 @@ private XElement CreateTestCaseElement(TestResultInfo result)
266319

267320
// Ensure time value is never zero because gitlab treats 0 like its null. 0.1 micro
268321
// seconds should be low enough it won't interfere with anyone monitoring test duration.
269-
testcaseElement.SetAttributeValue(
270-
"time",
271-
Math.Max(0.0000001f, result.Duration.TotalSeconds).ToString("0.0000000", CultureInfo.InvariantCulture));
322+
testcaseElement.SetAttributeValue("time", Math.Max(0.0000001f, result.Duration.TotalSeconds).ToString("0.0000000", CultureInfo.InvariantCulture));
272323

273324
if (result.Outcome == TestOutcome.Failed)
274325
{
@@ -343,6 +394,8 @@ private XElement CreateTestCaseElement(TestResultInfo result)
343394
testcaseElement.Add(new XElement("system-err", new XCData(stdErr.ToString())));
344395
}
345396

397+
testcaseElement.Add(CreatePropertiesElement(result));
398+
346399
return testcaseElement;
347400
}
348401

src/TestLogger/Extensions/MSTestAdapter.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,24 @@ public List<TestResultInfo> TransformResults(List<TestResultInfo> results, List<
2626
// Preserving method because result display name was empty
2727
}
2828
else if (method != displayName)
29-
{
29+
{
3030
result.Method = displayName;
3131
}
32+
33+
// Parse property traits
34+
var properties = new List<KeyValuePair<string, object>>();
35+
foreach (var property in result.TestCase.Properties)
36+
{
37+
switch (property.Id)
38+
{
39+
case "Microsoft.VisualStudio.TestTools.UnitTesting.TestContext.TestProperty":
40+
var propertyValue = result.TestCase.GetPropertyValue(property) as string[];
41+
properties.Add(new KeyValuePair<string, object>("CustomProperty", propertyValue));
42+
break;
43+
}
44+
}
45+
46+
result.Properties = properties;
3247
}
3348

3449
return results;

src/TestLogger/Extensions/NUnitTestAdapter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public List<TestResultInfo> TransformResults(List<TestResultInfo> results, List<
3838
case "NUnit.TestCategory":
3939
properties.Add(new KeyValuePair<string, object>(property.Id, result.TestCase.GetPropertyValue(property)));
4040
break;
41+
case "NUnit.Category":
42+
properties.Add(new KeyValuePair<string, object>("CustomProperty", result.TestCase.GetPropertyValue(property)));
43+
break;
4144
}
4245
}
4346

src/TestLogger/Extensions/XunitTestAdapter.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ public List<TestResultInfo> TransformResults(
5656
result.Method += displayName.Substring(i);
5757
}
5858

59+
var properties = new List<KeyValuePair<string, object>>();
60+
61+
// Parse test traits via Trait decorator
62+
foreach (var property in result.TestCase.Properties)
63+
{
64+
switch (property.Id)
65+
{
66+
case "Xunit.Trait":
67+
var propertyValue = result.TestCase.GetPropertyValue(property) as string[];
68+
69+
properties.Add(new KeyValuePair<string, object>("CustomProperty", propertyValue));
70+
break;
71+
}
72+
}
73+
74+
result.Properties = properties;
75+
5976
transformedResults.Add(result);
6077
}
6178

test/JUnit.Xml.TestLogger.AcceptanceTests/JUnit.Xml.TestLogger.AcceptanceTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
<TestAssets Include="$(MSBuildThisFileDirectory)../assets/JUnit.Xml.TestLogger.NetCore.Tests/JUnit.Xml.TestLogger.NetCore.Tests.csproj" />
2929
<TestAssets Include="$(MSBuildThisFileDirectory)../assets/JUnit.Xml.TestLogger.NetMulti.Tests/JUnit.Xml.TestLogger.NetMulti.Tests.csproj" />
30+
<TestAssets Include="$(MSBuildThisFileDirectory)../assets/JUnit.Xml.TestLogger.XUnit.NetCore.Tests/JUnit.Xml.TestLogger.XUnit.NetCore.Tests.csproj" />
3031
</ItemGroup>
3132
<Target Name="TestTarget" BeforeTargets="Build">
3233
<Message Importance="High" Text="... Building test assets" />

test/JUnit.Xml.TestLogger.AcceptanceTests/JUnitTestLoggerAcceptanceTests.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ public static void SuiteInitialize(TestContext context)
3737

3838
// Enable reporting of internal properties in the adapter using runsettings
3939
_ = DotnetTestFixture
40-
.Create()
41-
.WithBuild()
42-
.Execute(AssetName, loggerArgs, collectCoverage: false, "test-results.xml");
40+
.Create()
41+
.WithBuild()
42+
.Execute(AssetName, loggerArgs, collectCoverage: false, "test-results.xml");
4343
}
4444

4545
[TestMethod]
@@ -123,6 +123,31 @@ public void TestResultFileShouldContainErrordOut()
123123
Assert.IsTrue(node.Value.Contains("{33F5FD22-6F40-499D-98E4-481D87FAEAA1}"));
124124
}
125125

126+
[TestMethod]
127+
public void TestResultFileShouldContainNUnitCategoryAsProperty()
128+
{
129+
var tesuites = this.resultsXml.XPathSelectElement("/testsuites/testsuite");
130+
var testcase = tesuites
131+
.Nodes()
132+
.FirstOrDefault(n =>
133+
{
134+
var element = n as XElement;
135+
return element.Attribute("classname")?.Value == "JUnit.Xml.TestLogger.NetFull.Tests.UnitTest1" &&
136+
element.Attribute("name")?.Value == "WithProperties";
137+
});
138+
Assert.IsNotNull(testcase);
139+
140+
var properties = (testcase as XElement)
141+
.Nodes()
142+
.FirstOrDefault(n => (n as XElement)?.Name == "properties") as XElement;
143+
Assert.IsNotNull(properties);
144+
Assert.AreEqual(2, properties.Nodes().Count());
145+
var propertyElements = properties.Nodes().ToList();
146+
147+
Assert.AreEqual("Property name", (propertyElements[0] as XElement).Attribute("name").Value);
148+
Assert.AreEqual("Property value 1", (propertyElements[0] as XElement).Attribute("value").Value);
149+
}
150+
126151
[TestMethod]
127152
public void LoggedXmlValidatesAgainstXsdSchema()
128153
{

test/JUnit.Xml.TestLogger.AcceptanceTests/JUnitTestLoggerFormatOptionsAcceptanceTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ public void FailureBodyFormat_Verbose_ShouldStartWithMessage()
9898
Assert.IsTrue(body.Trim().StartsWith(message.Trim()));
9999
}
100100

101-
Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml));
101+
var validator = new JunitXmlValidator();
102+
Assert.IsTrue(validator.IsValid(resultsXml));
102103
}
103104

104105
[TestMethod]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Spekt Contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace JUnit.Xml.TestLogger.AcceptanceTests
5+
{
6+
using System.IO;
7+
using System.Linq;
8+
using System.Xml.Linq;
9+
using System.Xml.XPath;
10+
using global::TestLogger.Fixtures;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting;
12+
13+
/// <summary>
14+
/// Acceptance tests evaluate the most recent output of the build.ps1 script, NOT the most
15+
/// recent build performed by visual studio or dotnet.build
16+
///
17+
/// These acceptance tests look at the specific structure and contents of the produced Xml,
18+
/// when running using the xUnit vstest runner.
19+
/// </summary>
20+
[TestClass]
21+
public class JUnitTestLoggerXunitAcceptanceTests
22+
{
23+
private const string AssetName = "JUnit.Xml.TestLogger.XUnit.NetCore.Tests";
24+
private readonly string resultsFile;
25+
private readonly XDocument resultsXml;
26+
27+
public JUnitTestLoggerXunitAcceptanceTests()
28+
{
29+
this.resultsFile = Path.Combine(AssetName.ToAssetDirectoryPath(), "test-results.xml");
30+
this.resultsXml = XDocument.Load(this.resultsFile);
31+
}
32+
33+
[ClassInitialize]
34+
public static void SuiteInitialize(TestContext context)
35+
{
36+
var loggerArgs = "junit;LogFilePath=test-results.xml";
37+
38+
// Enable reporting of internal properties in the adapter using runsettings
39+
_ = DotnetTestFixture
40+
.Create()
41+
.WithBuild()
42+
.Execute(AssetName, loggerArgs, collectCoverage: false, "test-results.xml");
43+
}
44+
45+
[TestMethod]
46+
public void LoggedXmlValidatesAgainstXsdSchema()
47+
{
48+
var validator = new JunitXmlValidator();
49+
var result = validator.IsValid(File.ReadAllText(this.resultsFile));
50+
Assert.IsTrue(result);
51+
}
52+
53+
[TestMethod]
54+
public void TestResultFileShouldContainXUnitTraitAsProperty()
55+
{
56+
var properties = this.resultsXml.XPathSelectElement(
57+
"/testsuites/testsuite//testcase[@classname=\"NUnit.Xml.TestLogger.Tests2.ApiTest\"]/properties");
58+
Assert.IsNotNull(properties);
59+
Assert.AreEqual(2, properties.Nodes().Count());
60+
foreach (XElement node in properties.Nodes())
61+
{
62+
Assert.IsNotNull(node);
63+
if (node.Attribute("name").Value == "SomeProp")
64+
{
65+
Assert.AreEqual("SomeVal", node.Attribute("value").Value);
66+
}
67+
else if (node.Attribute("name").Value == "Category")
68+
{
69+
Assert.AreEqual("ApiTest", node.Attribute("value").Value);
70+
}
71+
else
72+
{
73+
Assert.Fail($"Unexpted property found");
74+
}
75+
}
76+
}
77+
}
78+
}

test/TestLogger.UnitTests/Extensions/MSTestAdapterTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,25 @@ public void TransformShouldNoitOverwriteMethodEmptyValues(string displayName)
5555
Assert.AreEqual(1, transformedResults.Count);
5656
Assert.AreEqual(Method, transformedResults.Single().Method);
5757
}
58+
59+
[TestMethod]
60+
public void TransformResultShouldAddProperties()
61+
{
62+
var testResults = new List<TestResultInfo>
63+
{
64+
new TestResultInfoBuilder("namespace", "type", Method)
65+
.WithProperty("Microsoft.VisualStudio.TestTools.UnitTesting.TestContext.TestProperty", new[] { "c1", "c2" })
66+
.Build(),
67+
};
68+
69+
var sut = new MSTestAdapter();
70+
71+
var transformedResults = sut.TransformResults(testResults, new List<TestMessageInfo>());
72+
73+
Assert.AreEqual(1, transformedResults.Count);
74+
Assert.AreEqual(1, transformedResults[0].Properties.Count);
75+
Assert.AreEqual(Method, transformedResults.Single().Method);
76+
CollectionAssert.AreEquivalent(new[] { "c1", "c2" }, (string[])transformedResults[0].Properties.Single(p => p.Key == "CustomProperty").Value);
77+
}
5878
}
5979
}

0 commit comments

Comments
 (0)