Skip to content

Commit 78985e2

Browse files
Copilotbaronfel
andcommitted
Add NETFRAMEWORK-only check for .NET runtime tasks and unit tests
- Added #if NETFRAMEWORK guard around runtime check in AcquireAndSetUpHost - Created unit tests with WindowsFullFrameworkOnlyFact attribute - Tests verify MSB4233 error is thrown with proper task name and assembly location - Added comments explaining why #if is needed alongside the test attribute Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com>
1 parent 3156a80 commit 78985e2

File tree

2 files changed

+168
-1
lines changed

2 files changed

+168
-1
lines changed
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.Globalization;
7+
using Microsoft.Build.BackEnd;
8+
using Microsoft.Build.Exceptions;
9+
using Microsoft.Build.Shared;
10+
using Microsoft.Build.UnitTests.BackEnd;
11+
using Shouldly;
12+
using Xunit;
13+
using Xunit.NetCore.Extensions;
14+
15+
#nullable disable
16+
17+
namespace Microsoft.Build.Engine.UnitTests.BackEnd
18+
{
19+
/// <summary>
20+
/// Tests for NodeProviderOutOfProcTaskHost, specifically error handling for unsupported runtime options.
21+
/// </summary>
22+
public class NodeProviderOutOfProcTaskHost_Tests : IDisposable
23+
{
24+
private IBuildComponent _nodeProviderComponent;
25+
private NodeProviderOutOfProcTaskHost _nodeProvider;
26+
private MockHost _mockHost;
27+
28+
public NodeProviderOutOfProcTaskHost_Tests()
29+
{
30+
_mockHost = new MockHost();
31+
_nodeProviderComponent = NodeProviderOutOfProcTaskHost.CreateComponent(BuildComponentType.OutOfProcTaskHostNodeProvider);
32+
_nodeProvider = _nodeProviderComponent as NodeProviderOutOfProcTaskHost;
33+
_nodeProvider.InitializeComponent(_mockHost);
34+
}
35+
36+
public void Dispose()
37+
{
38+
_nodeProvider?.ShutdownComponent();
39+
_mockHost = null;
40+
_nodeProvider = null;
41+
_nodeProviderComponent = null;
42+
}
43+
44+
/// <summary>
45+
/// Verify that attempting to acquire a task host with the .NET runtime flag throws InvalidProjectFileException
46+
/// with the MSB4233 error code and includes the task name and assembly location.
47+
/// This should only happen on .NET Framework builds of MSBuild.
48+
/// </summary>
49+
[WindowsFullFrameworkOnlyFact]
50+
public void AcquireAndSetUpHost_WithNETRuntime_ThrowsInvalidProjectFileException()
51+
{
52+
// This test uses HandshakeOptions which is internal to Microsoft.Build and only accessible
53+
// from test projects on NETFRAMEWORK builds due to InternalsVisibleTo. The WindowsFullFrameworkOnlyFact
54+
// attribute skips test execution on non-Framework builds, but we also need the #if to skip compilation.
55+
#if NETFRAMEWORK
56+
// Arrange
57+
HandshakeOptions hostContext = HandshakeOptions.TaskHost | HandshakeOptions.NET | HandshakeOptions.X64;
58+
string taskName = "MyNetTask";
59+
string taskLocation = "C:\\Path\\To\\MyTask.dll";
60+
string projectFile = "C:\\Project\\test.proj";
61+
int lineNumber = 42;
62+
int columnNumber = 10;
63+
64+
TaskHostConfiguration configuration = new TaskHostConfiguration(
65+
nodeId: 1,
66+
startupDirectory: Environment.CurrentDirectory,
67+
buildProcessEnvironment: new Dictionary<string, string>(),
68+
culture: CultureInfo.CurrentCulture,
69+
uiCulture: CultureInfo.CurrentUICulture,
70+
#if FEATURE_APPDOMAIN
71+
appDomainSetup: null,
72+
#endif
73+
lineNumberOfTask: lineNumber,
74+
columnNumberOfTask: columnNumber,
75+
projectFileOfTask: projectFile,
76+
continueOnError: false,
77+
taskName: taskName,
78+
taskLocation: taskLocation,
79+
isTaskInputLoggingEnabled: false,
80+
taskParameters: null,
81+
globalParameters: new Dictionary<string, string>(),
82+
warningsAsErrors: null,
83+
warningsNotAsErrors: null,
84+
warningsAsMessages: null);
85+
86+
// Act & Assert
87+
InvalidProjectFileException exception = Should.Throw<InvalidProjectFileException>(() =>
88+
{
89+
_nodeProvider.AcquireAndSetUpHost(hostContext, null, null, configuration);
90+
});
91+
92+
// Verify the exception contains the expected information
93+
exception.ProjectFile.ShouldBe(projectFile);
94+
exception.LineNumber.ShouldBe(lineNumber);
95+
exception.ColumnNumber.ShouldBe(columnNumber);
96+
exception.ErrorCode.ShouldBe("MSB4233");
97+
exception.Message.ShouldContain(taskName);
98+
exception.Message.ShouldContain(taskLocation);
99+
exception.Message.ShouldContain(".NET");
100+
exception.Message.ShouldContain("MSBuild 18.0");
101+
exception.Message.ShouldContain("Visual Studio 2026");
102+
#endif
103+
}
104+
105+
/// <summary>
106+
/// Verify that acquiring a task host with CLR4 runtime (without NET flag) does not throw the MSB4233 error.
107+
/// Note: This test may fail if the task host cannot actually be launched, but that's expected
108+
/// in this environment. The important part is that it doesn't throw the MSB4233 error.
109+
/// </summary>
110+
[WindowsFullFrameworkOnlyFact]
111+
public void AcquireAndSetUpHost_WithoutNETRuntime_DoesNotThrowMSB4233()
112+
{
113+
// This test uses HandshakeOptions which is internal to Microsoft.Build and only accessible
114+
// from test projects on NETFRAMEWORK builds due to InternalsVisibleTo. The WindowsFullFrameworkOnlyFact
115+
// attribute skips test execution on non-Framework builds, but we also need the #if to skip compilation.
116+
#if NETFRAMEWORK
117+
// Arrange
118+
HandshakeOptions hostContext = HandshakeOptions.TaskHost | HandshakeOptions.X64; // CLR4, no NET flag
119+
string taskName = "MyClr4Task";
120+
string taskLocation = "C:\\Path\\To\\MyTask.dll";
121+
string projectFile = "C:\\Project\\test.proj";
122+
123+
TaskHostConfiguration configuration = new TaskHostConfiguration(
124+
nodeId: 1,
125+
startupDirectory: Environment.CurrentDirectory,
126+
buildProcessEnvironment: new Dictionary<string, string>(),
127+
culture: CultureInfo.CurrentCulture,
128+
uiCulture: CultureInfo.CurrentUICulture,
129+
#if FEATURE_APPDOMAIN
130+
appDomainSetup: null,
131+
#endif
132+
lineNumberOfTask: 1,
133+
columnNumberOfTask: 1,
134+
projectFileOfTask: projectFile,
135+
continueOnError: false,
136+
taskName: taskName,
137+
taskLocation: taskLocation,
138+
isTaskInputLoggingEnabled: false,
139+
taskParameters: null,
140+
globalParameters: new Dictionary<string, string>(),
141+
warningsAsErrors: null,
142+
warningsNotAsErrors: null,
143+
warningsAsMessages: null);
144+
145+
// Act & Assert
146+
// We expect this to either succeed or fail with a different error (like node launch failure),
147+
// but NOT with MSB4233
148+
try
149+
{
150+
_nodeProvider.AcquireAndSetUpHost(hostContext, null, null, configuration);
151+
// If it succeeds (unlikely in test environment), that's fine
152+
}
153+
catch (InvalidProjectFileException ex)
154+
{
155+
// If it throws InvalidProjectFileException, it should NOT be MSB4233
156+
ex.ErrorCode.ShouldNotBe("MSB4233", $"Expected error other than MSB4233, but got: {ex.Message}");
157+
}
158+
catch (Exception)
159+
{
160+
// Other exceptions are fine for this test - we're only checking that MSB4233 isn't thrown
161+
}
162+
#endif
163+
}
164+
}
165+
}

src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,8 @@ internal static string GetMSBuildLocationFromHostContext(HandshakeOptions hostCo
486486
/// </summary>
487487
internal bool AcquireAndSetUpHost(HandshakeOptions hostContext, INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration)
488488
{
489-
// Check if .NET runtime is requested, which is not supported in this version
489+
#if NETFRAMEWORK
490+
// Check if .NET runtime is requested, which is not supported in .NET Framework version of MSBuild
490491
if ((hostContext & HandshakeOptions.NET) == HandshakeOptions.NET)
491492
{
492493
throw new Exceptions.InvalidProjectFileException(
@@ -500,6 +501,7 @@ internal bool AcquireAndSetUpHost(HandshakeOptions hostContext, INodePacketFacto
500501
null,
501502
null);
502503
}
504+
#endif
503505

504506
bool nodeCreationSucceeded;
505507
if (!_nodeContexts.ContainsKey(hostContext))

0 commit comments

Comments
 (0)