Skip to content

Commit 9537ab2

Browse files
EvangelinkCopilot
andcommitted
Rename XUnitV3Project -> MTPProject in Helix Sdk (breaking)
xUnit v3 with the Microsoft.Testing.Platform runner (default for v3) uses the exact same self-hosting 'dotnet exec ... <assembly>.dll' execution model as MSTest 4.x, NUnit-MTP, TUnit, and any other MTP-based test framework. The only difference between the previous XUnitV3Project Helix item type and the proposed MTPProject was the reporter args. This commit drops the duplication by: * Renaming the item type XUnitV3Project to MTPProject, the task CreateXUnitV3WorkItems to CreateMTPWorkItems, and the folder/files xunitv3-runner -> mtp-runner / XUnitV3Runner -> MTPRunner. * Dropping the UseMicrosoftTestingPlatformRunner switch and the legacy '-xml/-noAutoReporters' code path. MTP is now the only supported execution mode for this Helix item type; users on legacy non-MTP runners hand-author <HelixWorkItem>. * Switching the generated reporter args to TRX ('--report-trx --report-trx-filename testResults.trx'), which is cross-framework and parsed natively by arcade's TRXFormat reporter. Microsoft.Testing.Extensions.TrxReport is required; MSTest.Sdk references it transitively and arcade's XUnitV3 SDK targets reference it implicitly under MTP mode. * Adding MSTestProject as a discoverability shim that folds into MTPProject before any other target runs. Migration: the only external in-tree consumer is dotnet/efcore's eng/helix.proj, which can rename '<XUnitV3Project ... />' to '<MTPProject ... />' as part of its next arcade SDK bump. Arcade's own tests/UnitTests.proj is updated in this commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 40ac312 commit 9537ab2

9 files changed

Lines changed: 291 additions & 215 deletions

File tree

Documentation/AzureDevOps/SendingJobsToHelix.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ The list of available Helix queues can be found on the [Helix homepage](https://
119119
# condition: succeeded() - defaults to succeeded()
120120
```
121121

122-
### XUnit v3
122+
### Microsoft.Testing.Platform (MTP)
123123

124-
XUnit v3 test projects are self-hosting executables and do not need an external console runner. Instead of `XUnitProjects`, use `XUnitV3Project` items directly in your Helix MSBuild project file (see [the SDK's readme](/src/Microsoft.DotNet.Helix/Sdk/Readme.md) for details). The `XUnitPublishTargetFramework`, `XUnitRuntimeTargetFramework`, and `XUnitRunnerVersion` parameters are not needed for v3 projects.
124+
Test projects that target [Microsoft.Testing.Platform](https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro) (MTP) are self-hosting executables and do not need an external console runner. This covers MSTest 4.x with the MTP runner, xUnit v3 with MTP (the default for v3), NUnit with the MTP runner, TUnit, and any custom MTP-based framework.
125+
126+
Instead of `XUnitProjects`, use `MTPProject` items (or `MSTestProject` as a discoverability alias for MSTest projects) directly in your Helix MSBuild project file (see [the SDK's readme](/src/Microsoft.DotNet.Helix/Sdk/Readme.md) for details). Each project must reference `Microsoft.Testing.Extensions.TrxReport`; this is implicit for `MSTest.Sdk` projects and for xUnit v3 projects built with `Microsoft.DotNet.Arcade.Sdk`'s XUnitV3 targets. The `XUnitPublishTargetFramework`, `XUnitRuntimeTargetFramework`, and `XUnitRunnerVersion` parameters are not needed for MTP projects.
125127

126128
## The More Complex Case
127129

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.Build.Framework;
10+
11+
namespace Microsoft.DotNet.Helix.Sdk
12+
{
13+
/// <summary>
14+
/// MSBuild custom task to create HelixWorkItems for test projects that target
15+
/// Microsoft.Testing.Platform (MTP). Such projects ship as self-hosting executables
16+
/// (a Main entry point is emitted by Microsoft.Testing.Platform.MSBuild) and can be
17+
/// run directly with 'dotnet exec'. This applies to MSTest 4.x, xUnit v3 with MTP,
18+
/// NUnit with MTP, TUnit, and any custom MTP-based test framework.
19+
/// </summary>
20+
public class CreateMTPWorkItems : BaseTask
21+
{
22+
/// <summary>
23+
/// An array of MTP project work items containing the following metadata:
24+
/// - [Required] PublishDirectory: the publish output directory of the test project
25+
/// - [Required] TargetPath: the output dll path
26+
/// - [Optional] Arguments: a string of arguments to be passed to the test executable
27+
/// *after* the auto-injected reporter flags
28+
/// The two required parameters are populated automatically by MTPRunner.targets when
29+
/// MTPProject.Identity is set to the path of the test csproj file.
30+
/// </summary>
31+
[Required]
32+
public ITaskItem[] MTPProjects { get; set; }
33+
34+
/// <summary>
35+
/// The path to the dotnet executable on the Helix agent. Defaults to "dotnet".
36+
/// </summary>
37+
public string PathToDotnet { get; set; } = "dotnet";
38+
39+
/// <summary>
40+
/// Boolean true if this is a posix shell, false if not.
41+
/// This does not need to be set by a user; it is automatically determined in
42+
/// Microsoft.DotNet.Helix.Sdk.MonoQueue.targets. Currently unused (the dotnet exec
43+
/// command is identical on every shell) but accepted for symmetry with the other
44+
/// Create*WorkItems tasks and to allow future shell-specific tweaks without an
45+
/// API break.
46+
/// </summary>
47+
[Required]
48+
public bool IsPosixShell { get; set; }
49+
50+
/// <summary>
51+
/// Optional timeout for all created work items.
52+
/// Accepts any value parseable by <see cref="TimeSpan.TryParse(string, out TimeSpan)"/>.
53+
/// Defaults to 5 minutes.
54+
/// </summary>
55+
public string MTPWorkItemTimeout { get; set; }
56+
57+
/// <summary>
58+
/// Optional name of the TRX file produced by Microsoft.Testing.Extensions.TrxReport.
59+
/// Defaults to "testResults.trx". Whatever value is used here is what the arcade
60+
/// Python TRXFormat parser will pick up from the work item's working directory.
61+
/// </summary>
62+
public string TrxReportFilename { get; set; } = "testResults.trx";
63+
64+
/// <summary>
65+
/// An array of ITaskItems of type HelixWorkItem.
66+
/// </summary>
67+
[Output]
68+
public ITaskItem[] MTPWorkItems { get; set; }
69+
70+
public override bool Execute()
71+
{
72+
ExecuteAsync().GetAwaiter().GetResult();
73+
return !Log.HasLoggedErrors;
74+
}
75+
76+
private async Task ExecuteAsync()
77+
{
78+
// De-duplicate inputs by (Identity, AdditionalProperties). This collapses
79+
// accidental duplicates such as a project listed under both MSTestProject and
80+
// MTPProject, while preserving legitimate same-Identity entries that differ in
81+
// AdditionalProperties (the canonical multi-TFM pattern).
82+
var deduped = MTPProjects
83+
.GroupBy(p => (p.ItemSpec, p.GetMetadata("AdditionalProperties") ?? string.Empty))
84+
.Select(g => g.First())
85+
.ToArray();
86+
87+
MTPWorkItems = (await Task.WhenAll(deduped.Select(PrepareWorkItem))).Where(wi => wi != null).ToArray();
88+
}
89+
90+
/// <summary>
91+
/// Prepares a HelixWorkItem for a single MTP test project.
92+
/// </summary>
93+
private async Task<ITaskItem> PrepareWorkItem(ITaskItem mtpProject)
94+
{
95+
// Forces this task to run asynchronously
96+
await Task.Yield();
97+
98+
if (!mtpProject.GetRequiredMetadata(Log, "PublishDirectory", out string publishDirectory))
99+
{
100+
return null;
101+
}
102+
if (!mtpProject.GetRequiredMetadata(Log, "TargetPath", out string targetPath))
103+
{
104+
return null;
105+
}
106+
107+
mtpProject.TryGetMetadata("Arguments", out string arguments);
108+
109+
string assemblyName = Path.GetFileName(targetPath);
110+
string assemblyBaseName = assemblyName;
111+
if (assemblyBaseName.EndsWith(".dll"))
112+
{
113+
assemblyBaseName = assemblyBaseName.Substring(0, assemblyBaseName.Length - 4);
114+
}
115+
116+
// MTP test apps are self-hosting executables. Run the assembly directly with
117+
// 'dotnet exec'. The reporter args below require the test project to reference
118+
// Microsoft.Testing.Extensions.TrxReport. MSTest.Sdk references it transitively;
119+
// xUnit v3 projects built with Microsoft.DotNet.Arcade.Sdk's XUnitV3 targets get
120+
// it implicitly as well. Other MTP-based frameworks must add the package.
121+
//
122+
// --results-directory . -> TRX is written next to the assembly, which is the
123+
// work item's cwd and is scanned by the arcade Python
124+
// reporter / EnableHelixJobMonitor.
125+
// --report-trx -> enable the TrxReport extension.
126+
// --report-trx-filename -> deterministic filename so the parser finds it.
127+
//
128+
// Note about the AzureDevOps reporter (Microsoft.Testing.Extensions.AzureDevOpsReport):
129+
// it activates only when '--report-azdo' is explicitly passed AND TF_BUILD=true.
130+
// We never pass '--report-azdo' here, so there is no risk of a duplicate Azure
131+
// DevOps test run on top of the one the Helix Sdk already opened. If a user has
132+
// wired '--report-azdo' into their test project they should remove it for Helix
133+
// runs (it conflicts with the Helix-managed run).
134+
string reporterArgs =
135+
$"--results-directory . --report-trx --report-trx-filename {TrxReportFilename}";
136+
137+
string command = $"{PathToDotnet} exec --roll-forward Major " +
138+
$"--runtimeconfig {assemblyBaseName}.runtimeconfig.json " +
139+
$"--depsfile {assemblyBaseName}.deps.json " +
140+
$"{assemblyName} {reporterArgs}" +
141+
(string.IsNullOrEmpty(arguments) ? "" : " " + arguments);
142+
143+
Log.LogMessage($"Creating MTP work item with properties Identity: {assemblyName}, PayloadDirectory: {publishDirectory}, Command: {command}");
144+
145+
TimeSpan timeout = TimeSpan.FromMinutes(5);
146+
if (!string.IsNullOrEmpty(MTPWorkItemTimeout))
147+
{
148+
if (!TimeSpan.TryParse(MTPWorkItemTimeout, out timeout))
149+
{
150+
Log.LogWarning($"Invalid value \"{MTPWorkItemTimeout}\" provided for MTPWorkItemTimeout; falling back to default value of \"00:05:00\" (5 minutes)");
151+
}
152+
}
153+
154+
var result = new Microsoft.Build.Utilities.TaskItem(assemblyName, new Dictionary<string, string>()
155+
{
156+
{ "Identity", assemblyName },
157+
{ "PayloadDirectory", publishDirectory },
158+
{ "Command", command },
159+
{ "Timeout", timeout.ToString() },
160+
});
161+
mtpProject.CopyMetadataTo(result);
162+
return result;
163+
}
164+
}
165+
}

src/Microsoft.DotNet.Helix/Sdk/CreateXUnitV3WorkItems.cs

Lines changed: 0 additions & 144 deletions
This file was deleted.

src/Microsoft.DotNet.Helix/Sdk/Readme.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,17 +331,31 @@ Given a local folder `$(TestFolder)` containing `runtests.cmd`, this will run `r
331331
</PropertyGroup>
332332
333333
<!--
334-
XUnit v3 Runner
335-
Enabling this will create one work item for each xunit v3 test project specified.
336-
XUnit v3 tests are self-hosting executables and do not need an external console runner.
337-
This is enabled by specifying one or more XUnitV3Project items.
334+
Microsoft.Testing.Platform (MTP) Runner
335+
Enabling this will create one Helix work item per MTP-based test project. This
336+
covers MSTest 4.x with the MTP runner, xUnit v3 with MTP (the default for v3),
337+
NUnit with the MTP runner, TUnit, and any custom MTP-based framework.
338+
339+
Each test project must reference Microsoft.Testing.Extensions.TrxReport so that
340+
results can be reported as a TRX file (which arcade's reporter consumes natively).
341+
Projects built with MSTest.Sdk, or with Microsoft.DotNet.Arcade.Sdk's XUnitV3
342+
targets, get this reference implicitly.
343+
344+
For MSTest projects you can use the discoverability alias <MSTestProject .../>;
345+
it is folded into @(MTPProject) before any other target runs.
338346
-->
339347
<ItemGroup>
340-
<XUnitV3Project Include="..\tests\bar.Tests.csproj"/>
348+
<!-- xUnit v3 / NUnit-MTP / TUnit / custom MTP -->
349+
<MTPProject Include="..\tests\bar.Tests.csproj"/>
350+
351+
<!-- MSTest (folded into MTPProject) -->
352+
<MSTestProject Include="..\tests\baz.Tests.csproj"/>
341353
</ItemGroup>
342354
<PropertyGroup>
343-
<!-- Whether to use the Microsoft Testing Platform runner. Defaults to true. -->
344-
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
355+
<!-- Optional: customize the TRX filename produced by Microsoft.Testing.Extensions.TrxReport. Defaults to testResults.trx. -->
356+
<MTPTrxReportFilename>testResults.trx</MTPTrxReportFilename>
357+
<!-- Optional: per-work-item timeout (TimeSpan format). Defaults to 5 minutes. -->
358+
<MTPWorkItemTimeout>00:05:00</MTPWorkItemTimeout>
345359
</PropertyGroup>
346360
347361
<ItemGroup>

src/Microsoft.DotNet.Helix/Sdk/tools/Microsoft.DotNet.Helix.Sdk.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<UsingTask TaskName="FindDotNetCliPackage" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
4747
<UsingTask TaskName="GetHelixWorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
4848
<UsingTask TaskName="CreateXUnitWorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
49-
<UsingTask TaskName="CreateXUnitV3WorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
49+
<UsingTask TaskName="CreateMTPWorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
5050
<UsingTask TaskName="CreateXHarnessAndroidWorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
5151
<UsingTask TaskName="CreateXHarnessAppleWorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />
5252
<UsingTask TaskName="CreateTestsForWorkItems" AssemblyFile="$(MicrosoftDotNetHelixSdkTasksAssembly)" Runtime="NET" Architecture="*" TaskFactory="$(MicrosoftDotNetHelixSdkTasksFactory)" />

src/Microsoft.DotNet.Helix/Sdk/tools/xunitv3-runner/XUnitV3Runner.props renamed to src/Microsoft.DotNet.Helix/Sdk/tools/mtp-runner/MTPRunner.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33

44
<PropertyGroup>
5-
<_HelixMonoQueueTargets>$(_HelixMonoQueueTargets);$(MSBuildThisFileDirectory)XUnitV3Runner.targets</_HelixMonoQueueTargets>
5+
<_HelixMonoQueueTargets>$(_HelixMonoQueueTargets);$(MSBuildThisFileDirectory)MTPRunner.targets</_HelixMonoQueueTargets>
66
</PropertyGroup>
77

88
</Project>

0 commit comments

Comments
 (0)