From 33916a7b45ae803e46e8e38510e97004d076f08f Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Tue, 20 Oct 2026 10:48:15 -0400 Subject: [PATCH] fix(dotnet): stop inferring publish and run on test projects Test projects evaluate to OutputType=Exe because Microsoft.NET.Test.Sdk sets it on .NET Core and MTP runners set it themselves, so every test project got publish and run targets. Gate both on the project not being a test project, and drop the unused isTest parameter from AddPublishTarget. Part of NXC-5119 --- .../technologies/dotnet/introduction.mdoc | 8 ++-- e2e/dotnet/src/dotnet.test.ts | 2 + .../TargetBuilderExecutableTargetsTests.cs | 48 +++++++++++++++++++ .../TestProjectDetectionTests.cs | 3 +- .../Utilities/TargetBuilder.Publish.cs | 1 - .../analyzer/Utilities/TargetBuilder.cs | 4 +- 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 packages/dotnet/analyzer.Tests/TargetBuilderExecutableTargetsTests.cs diff --git a/astro-docs/src/content/docs/technologies/dotnet/introduction.mdoc b/astro-docs/src/content/docs/technologies/dotnet/introduction.mdoc index d54b46199ee..3049e5ab572 100644 --- a/astro-docs/src/content/docs/technologies/dotnet/introduction.mdoc +++ b/astro-docs/src/content/docs/technologies/dotnet/introduction.mdoc @@ -154,7 +154,7 @@ To have Nx wrap it anyway, opt the target in: ### Test project detection -A project gets a `test` target, and loses its `pack` target, when either MSBuild property is `true`: +A project gets a `test` target, and loses its `pack`, `publish`, and `run` targets, when either MSBuild property is `true`: - `IsTestProject`, which `Microsoft.NET.Test.Sdk` sets. - `IsTestingPlatformApplication`, which [Microsoft.Testing.Platform](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-platform-intro) sets. @@ -164,7 +164,7 @@ These are the two properties the .NET SDK itself checks before it will run a pro Nx therefore agrees with `dotnet test` about what a test project is. Referencing a framework such as xunit or NUnit is not enough on its own, which is what lets a library expose test helpers and keep its `pack` target. -`publish` and `run` depend only on `OutputType`, so a runner built as an executable keeps both. +A test project built as an executable still gets no `publish` or `run` target. Both properties arrive with a restored package, so neither has a value before the project has been restored. @@ -256,10 +256,10 @@ Once a .NET project file has been identified, the targets are created with the c - `test` - Runs unit tests (for test projects) - `clean` - Removes build outputs - `restore` - Restores NuGet package dependencies. Not created unless you opt in with `"restore": true` -- `publish` - Publishes the application (for executable projects) +- `publish` - Publishes the application (for executable projects that aren't test projects) - `pack` - Creates a NuGet package (for library projects) - `watch` - Watches the project for changes and rebuilds (automatically marked as continuous) -- `run` - Runs the application (for executable projects) +- `run` - Runs the application (for executable projects that aren't test projects) Use `include` and `exclude` glob patterns on the plugin entry to scope inference. A [target defaults](/docs/reference/nx-json#target-defaults) `plugin` filter must use the exact identifier `@nx/dotnet`. diff --git a/e2e/dotnet/src/dotnet.test.ts b/e2e/dotnet/src/dotnet.test.ts index 07f8f6adf19..041875db2e2 100644 --- a/e2e/dotnet/src/dotnet.test.ts +++ b/e2e/dotnet/src/dotnet.test.ts @@ -71,6 +71,8 @@ describe('.NET Plugin', () => { expect(details.targets).toHaveProperty('build'); expect(details.targets).toHaveProperty('test'); + expect(details.targets).not.toHaveProperty('publish'); + expect(details.targets).not.toHaveProperty('run'); }); }); diff --git a/packages/dotnet/analyzer.Tests/TargetBuilderExecutableTargetsTests.cs b/packages/dotnet/analyzer.Tests/TargetBuilderExecutableTargetsTests.cs new file mode 100644 index 00000000000..fb580615ea8 --- /dev/null +++ b/packages/dotnet/analyzer.Tests/TargetBuilderExecutableTargetsTests.cs @@ -0,0 +1,48 @@ +using MsbuildAnalyzer.Models; +using MsbuildAnalyzer.Utilities; +using Xunit; + +namespace MsbuildAnalyzer.Tests; + +/// +/// Which projects get publish and run. Test projects evaluate to +/// OutputType=Exe, since Microsoft.NET.Test.Sdk sets it on .NET Core and MTP +/// runners set it themselves, but they are not applications. +/// +public class TargetBuilderExecutableTargetsTests +{ + private static readonly string WorkspaceRoot = Path.Combine(Path.GetTempPath(), "nx-dotnet-ws"); + + private static Dictionary BuildTargets(bool isExe, bool isTest) => + TargetBuilder.BuildTargets( + projectName: "MyProj", + fileName: "MyProj.csproj", + isTest: isTest, + isExe: isExe, + packageRefs: new List(), + properties: new Dictionary(), + projectDirectory: Path.Combine(WorkspaceRoot, "apps", "MyProj"), + workspaceRoot: WorkspaceRoot, + options: new PluginOptions(), + nxJson: null, + directoryBuildInputs: new List()); + + [Fact] + public void Executable_GetsPublishAndRun() + { + var targets = BuildTargets(isExe: true, isTest: false); + + Assert.Contains("publish", targets.Keys); + Assert.Contains("run", targets.Keys); + } + + [Fact] + public void ExecutableTestProject_GetsNoPublishOrRun() + { + var targets = BuildTargets(isExe: true, isTest: true); + + Assert.Contains("test", targets.Keys); + Assert.DoesNotContain("publish", targets.Keys); + Assert.DoesNotContain("run", targets.Keys); + } +} diff --git a/packages/dotnet/analyzer.Tests/TestProjectDetectionTests.cs b/packages/dotnet/analyzer.Tests/TestProjectDetectionTests.cs index 111a26b661f..5517bd6be7e 100644 --- a/packages/dotnet/analyzer.Tests/TestProjectDetectionTests.cs +++ b/packages/dotnet/analyzer.Tests/TestProjectDetectionTests.cs @@ -6,8 +6,7 @@ namespace MsbuildAnalyzer.Tests; /// /// What makes a project a test project, and so which projects get a test -/// target and lose pack. publish and run are decided by -/// OutputType alone, so an MTP runner keeps both. +/// target and lose pack, publish and run. /// /// The expectations here mirror the SDK's own gate in /// Microsoft.TestPlatform.ImportAfter.targets: diff --git a/packages/dotnet/analyzer/Utilities/TargetBuilder.Publish.cs b/packages/dotnet/analyzer/Utilities/TargetBuilder.Publish.cs index 9707de93436..4766ab56da9 100644 --- a/packages/dotnet/analyzer/Utilities/TargetBuilder.Publish.cs +++ b/packages/dotnet/analyzer/Utilities/TargetBuilder.Publish.cs @@ -11,7 +11,6 @@ private static void AddPublishTarget( Dictionary targets, string projectName, string fileName, - bool isTest, EvaluatedProperties properties, string projectDirectory, string workspaceRoot, diff --git a/packages/dotnet/analyzer/Utilities/TargetBuilder.cs b/packages/dotnet/analyzer/Utilities/TargetBuilder.cs index d349fe0f567..2d892798c4b 100644 --- a/packages/dotnet/analyzer/Utilities/TargetBuilder.cs +++ b/packages/dotnet/analyzer/Utilities/TargetBuilder.cs @@ -43,9 +43,9 @@ public static Dictionary BuildTargets( AddCleanTarget(targets, fileName, isTest, options); AddWatchTarget(targets, fileName, options); - if (isExe) + if (isExe && !isTest) { - AddPublishTarget(targets, projectName, fileName, isTest, properties, projectDirectory, workspaceRoot, options, productionInput, directoryBuildInputs); + AddPublishTarget(targets, projectName, fileName, properties, projectDirectory, workspaceRoot, options, productionInput, directoryBuildInputs); AddRunTarget(targets, fileName, options); }