Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
34 changes: 33 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,36 @@ jobs:
run: dotnet build -p:ContinuousIntegrationBuild=True --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
run: dotnet test --no-build --configuration Release --verbosity normal

aot-compatibility:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
global-json-file: "./global.json"

- name: Restore dependencies
run: dotnet restore VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest/VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest.csproj -r linux-x64

- name: Publish AOT smoke test
run: dotnet publish VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest/VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest.csproj -c Release -r linux-x64 --no-restore

- name: Run AOT smoke test
run: ./VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest/bin/Release/net10.0/linux-x64/publish/VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest

- name: Verify nupkg TFMs
shell: bash
run: |
dotnet restore VSTestPlaylistTools.TrxToPlaylistConverter/VSTestPlaylistTools.TrxToPlaylistConverter.csproj
dotnet pack VSTestPlaylistTools.TrxToPlaylistConverter/VSTestPlaylistTools.TrxToPlaylistConverter.csproj -c Release -o /tmp/pack-out --no-restore
Comment thread
BenjaminMichaelis marked this conversation as resolved.
NUPKG=$(ls /tmp/pack-out/*.nupkg | head -1)
unzip -q "$NUPKG" -d /tmp/nupkg-contents
test -d /tmp/nupkg-contents/lib/netstandard2.1 || (echo "Missing lib/netstandard2.1" && exit 1)
test -d /tmp/nupkg-contents/lib/net8.0 || (echo "Missing lib/net8.0" && exit 1)
test -d /tmp/nupkg-contents/lib/net10.0 || (echo "Missing lib/net10.0" && exit 1)
echo "nupkg TFM check passed: netstandard2.1, net8.0, net10.0 all present"
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
</ItemGroup>
</Project>
</Project>
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ A collection of .NET libraries and tools for creating, parsing, and manipulating
- **[VSTestPlaylistTools](https://www.nuget.org/packages/VSTestPlaylistTools)** - Playlist V1/V2 types, builders/parsers, and unified loading utilities
- **[VSTestPlaylistTools.TrxToPlaylistConverter](https://www.nuget.org/packages/VSTestPlaylistTools.TrxToPlaylistConverter)** - Library for converting TRX test results to V1 playlists

## AOT compatibility status

Both `VSTestPlaylistTools` and `VSTestPlaylistTools.TrxToPlaylistConverter` are **fully AOT-compatible**:

- Both multi-target `netstandard2.1`, `net8.0`, and `net10.0`
- Both enable `IsAotCompatible=true` on their `net8.0+` targets (AOT support requires .NET 8 or later)
- All `XmlSerializer` usage has been replaced with manual `XmlWriter`/`XmlReader` throughout both packages
- Both packages pass full AOT publishing and smoke testing in CI

## Quick Start

### Installing the CLI Tool
Expand Down
118 changes: 118 additions & 0 deletions VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using PlaylistV1 = VS.TestPlaylistTools.PlaylistV1;
using PlaylistV2 = VS.TestPlaylistTools.PlaylistV2;
using VSTestPlaylistTools.TrxToPlaylist;

// --- V1: object creation and round-trip serialization ---

PlaylistV1.PlaylistRoot v1Playlist = PlaylistV1.PlaylistV1Builder.Create(["Ns.Class.Method1", "Ns.Class.Method2"]);

if (v1Playlist.Tests.Count != 2)
{
Console.Error.WriteLine($"FAIL: Expected 2 tests, got {v1Playlist.Tests.Count}");
return 1;
}

string v1Xml = PlaylistV1.PlaylistV1Builder.ToXmlString(v1Playlist);

if (!v1Xml.Contains("Ns.Class.Method1") || !v1Xml.Contains("Ns.Class.Method2"))
{
Console.Error.WriteLine("FAIL: V1 XML missing expected test names");
return 2;
}

PlaylistV1.PlaylistRoot v1Roundtrip = PlaylistV1.PlaylistV1Parser.FromString(v1Xml);

if (v1Roundtrip.Tests.Count != 2)
{
Console.Error.WriteLine($"FAIL: V1 round-trip expected 2 tests, got {v1Roundtrip.Tests.Count}");
return 3;
}

// --- V2: object creation and round-trip serialization ---

PlaylistV2.PlaylistRoot v2Playlist = PlaylistV2.PlaylistV2Builder.Create([
PlaylistV2.BooleanRule.Any("Includes",
PlaylistV2.BooleanRule.All(
PlaylistV2.PropertyRule.Solution(),
PlaylistV2.BooleanRule.Any(
PlaylistV2.BooleanRule.All(
PlaylistV2.PropertyRule.Project("MyProject"),
PlaylistV2.BooleanRule.Any(
PlaylistV2.PropertyRule.Class("MyClass")
)
)
)
)
)
]);

string v2Xml = PlaylistV2.PlaylistV2Builder.ToXmlString(v2Playlist);

if (!v2Xml.Contains("MyProject") || !v2Xml.Contains("MyClass"))
{
Console.Error.WriteLine("FAIL: V2 XML missing expected values");
return 4;
}

PlaylistV2.PlaylistRoot v2Roundtrip = PlaylistV2.PlaylistV2Parser.FromString(v2Xml);

if (v2Roundtrip.Rules.Count != 1)
{
Console.Error.WriteLine($"FAIL: V2 round-trip expected 1 root rule, got {v2Roundtrip.Rules.Count}");
return 5;
}

// --- Converter: exercise TRX parsing path with a real TRX file ---

string trxContent = """
<?xml version="1.0" encoding="UTF-8"?>
<TestRun id="run1" name="TestRun" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<TestDefinitions>
<UnitTest name="TestMethod1" id="test1">
<TestMethod className="MyNamespace.MyClass" name="TestMethod1" />
</UnitTest>
<UnitTest name="TestMethod2" id="test2">
<TestMethod className="MyNamespace.MyClass" name="TestMethod2" />
</UnitTest>
</TestDefinitions>
<Results>
<UnitTestResult testId="test1" testName="TestMethod1" outcome="Passed" />
<UnitTestResult testId="test2" testName="TestMethod2" outcome="Failed" />
</Results>
</TestRun>
""";

string trxPath = Path.GetTempFileName() + ".trx";
try
{
File.WriteAllText(trxPath, trxContent);
var converter2 = new TrxToPlaylistConverter();
string playlistXml = converter2.ConvertTrxToPlaylistXml(trxPath, TrxLib.TestOutcome.Failed);
if (!playlistXml.Contains("MyClass.TestMethod2"))
{
Console.Error.WriteLine($"FAIL: TRX parse smoke test - expected MyClass.TestMethod2 in output, got: {playlistXml}");
return 7;
}
}
finally
{
File.Delete(trxPath);
}

// --- Converter: exercise code path (FileNotFoundException expected) ---

var converter = new TrxToPlaylistConverter();

try
{
converter.ConvertTrxToPlaylist("nonexistent-file-that-does-not-exist.trx");
Console.Error.WriteLine("FAIL: Expected FileNotFoundException was not thrown");
return 6;
}
catch (FileNotFoundException)
{
// Expected
Comment thread
BenjaminMichaelis marked this conversation as resolved.
}

Console.WriteLine("AOT smoke test passed.");
return 0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<ILLinkTreatWarningsAsErrors>true</ILLinkTreatWarningsAsErrors>
<!-- This project is only published via dotnet publish -r <RID>; exclude from dotnet test -->
<IsTestProject>false</IsTestProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\VSTestPlaylistTools.TrxToPlaylistConverter\VSTestPlaylistTools.TrxToPlaylistConverter.csproj"
SetTargetFramework="TargetFramework=net10.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,4 @@ public void ConvertMultipleTrxToPlaylistXml_TwoFiles_ReturnsValidXml()

#endregion
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="coverlet.collector" />
<PackageReference Include="IntelliTect.Multitool" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="TrxLib" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,8 @@ public PlaylistRoot ConvertTrxToPlaylist(string trxFilePath, params TrxLib.TestO
if (!File.Exists(trxFilePath))
throw new FileNotFoundException($"TRX file not found: {trxFilePath}", trxFilePath);

// Parse the TRX file using TrxLib
var testRun = TrxLib.TrxParser.Parse(new FileInfo(trxFilePath));

// Get the results
List<TrxLib.TestResult> allResults = [.. testRun];

// Filter by outcome if specified
IEnumerable<TrxLib.TestResult> filteredResults = allResults;
if (outcomes != null && outcomes.Length > 0)
{
filteredResults = allResults.Where(r => outcomes.Contains(r.Outcome));
}

// Extract test names
List<string> testNames = filteredResults.Select(r => r.FullyQualifiedTestName).ToList();

// Create a playlist
TrxLib.TestResultSet allResults = TrxLib.TrxParser.Parse(new FileInfo(trxFilePath));
List<string> testNames = FilterResults(allResults, outcomes).Select(r => r.FullyQualifiedTestName).ToList();
return PlaylistV1Builder.Create(testNames);
}

Expand All @@ -49,8 +34,8 @@ public PlaylistRoot ConvertTrxToPlaylist(string trxFilePath, params TrxLib.TestO
/// <returns>A PlaylistRoot object containing the filtered and de-duplicated tests.</returns>
public PlaylistRoot ConvertMultipleTrxToPlaylist(IEnumerable<string> trxFilePaths, params TrxLib.TestOutcome[] outcomes)
{
if (trxFilePaths == null)
throw new ArgumentNullException(nameof(trxFilePaths));
if (trxFilePaths == null)
throw new ArgumentNullException(nameof(trxFilePaths));

string[] filesArray = trxFilePaths.ToArray();
if (filesArray.Length == 0)
Expand All @@ -67,24 +52,10 @@ public PlaylistRoot ConvertMultipleTrxToPlaylist(IEnumerable<string> trxFilePath
if (!File.Exists(trxFilePath))
throw new FileNotFoundException($"TRX file not found: {trxFilePath}", trxFilePath);

// Parse the TRX file using TrxLib
var testRun = TrxLib.TrxParser.Parse(new FileInfo(trxFilePath));
TrxLib.TestResultSet allResults = TrxLib.TrxParser.Parse(new FileInfo(trxFilePath));

// Get the results
List<TrxLib.TestResult> allResults = [.. testRun];

// Filter by outcome if specified
IEnumerable<TrxLib.TestResult> filteredResults = allResults;
if (outcomes != null && outcomes.Length > 0)
{
filteredResults = allResults.Where(r => outcomes.Contains(r.Outcome));
}

// Add test names to the set (duplicates are automatically ignored)
foreach (var result in filteredResults)
{
foreach (TrxLib.TestResult result in FilterResults(allResults, outcomes))
uniqueTestNames.Add(result.FullyQualifiedTestName);
}
}

// Create a playlist with the de-duplicated test names
Expand Down Expand Up @@ -144,5 +115,13 @@ public string ConvertMultipleTrxToPlaylistXml(IEnumerable<string> trxFilePaths,
PlaylistRoot playlist = ConvertMultipleTrxToPlaylist(trxFilePaths, outcomes);
return PlaylistV1Builder.ToXmlString(playlist);
}

private static IEnumerable<TrxLib.TestResult> FilterResults(IEnumerable<TrxLib.TestResult> results, TrxLib.TestOutcome[] outcomes)
{
if (outcomes == null || outcomes.Length == 0)
return results;

return results.Where(r => outcomes.Contains(r.Outcome));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk" TreatAsLocalProperty="PublishAot">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFrameworks>netstandard2.1;net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>false</PublishAot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="TrxLib" />
</ItemGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<PackageReference Include="Microsoft.SourceLink.GitHub">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="TrxLib" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions VSTestPlaylistTools.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Project Path="trx-to-vsplaylist.Tests/trx-to-vsplaylist.Tests.csproj" />
<Project Path="trx-to-vsplaylist/trx-to-vsplaylist.csproj" />
<Project Path="VSTestPlaylistTools.Tests/VSTestPlaylistTools.Tests.csproj" />
<Project Path="VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest/VSTestPlaylistTools.TrxToPlaylistConverter.AotSmokeTest.csproj" />
<Project Path="VSTestPlaylistTools.TrxToPlaylistConverter.Tests/VSTestPlaylistTools.TrxToPlaylistConverter.Tests.csproj" />
<Project Path="VSTestPlaylistTools.TrxToPlaylistConverter/VSTestPlaylistTools.TrxToPlaylistConverter.csproj" />
<Project Path="VSTestPlaylistTools/VSTestPlaylistTools.csproj" />
Expand Down
3 changes: 0 additions & 3 deletions VSTestPlaylistTools/PlaylistV1/AddElement.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Xml.Serialization;

namespace VS.TestPlaylistTools.PlaylistV1
{
/// <summary>
Expand All @@ -10,7 +8,6 @@ public class AddElement
/// <summary>
/// The fully qualified name of the test.
/// </summary>
[XmlAttribute("Test")]
public string? Test { get; set; }

/// <summary>
Expand Down
18 changes: 10 additions & 8 deletions VSTestPlaylistTools/PlaylistV1/PlaylistRoot.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
using System.Xml;
using System.Xml.Serialization;

namespace VS.TestPlaylistTools.PlaylistV1
{
/// <summary>
/// Represents a Visual Studio Test Playlist Version 1.0
/// </summary>
[XmlRoot("Playlist")]
public class PlaylistRoot : IPlaylistRoot
{
/// <summary>
/// The version of the playlist format. Always "1.0" for V1 playlists.
/// </summary>
[XmlAttribute("Version")]
public string Version
Comment thread
BenjaminMichaelis marked this conversation as resolved.
{
get => _version;
Expand All @@ -28,7 +25,6 @@ public string Version
/// <summary>
/// The collection of tests to be included in this playlist.
/// </summary>
[XmlElement("Add")]
public List<AddElement> Tests { get; set; } = [];

/// <summary>
Expand Down Expand Up @@ -104,11 +100,17 @@ public void Serialize(TextWriter writer)
Indent = true
});

XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
namespaces.Add(string.Empty, string.Empty);
xmlWriter.WriteStartElement("Playlist");
xmlWriter.WriteAttributeString("Version", _version);

XmlSerializer serializer = new XmlSerializer(typeof(PlaylistRoot));
serializer.Serialize(xmlWriter, this, namespaces);
foreach (AddElement test in Tests)
{
xmlWriter.WriteStartElement("Add");
xmlWriter.WriteAttributeString("Test", test.Test);
xmlWriter.WriteEndElement();
}

xmlWriter.WriteEndElement();
}
}
}
Loading
Loading