diff --git a/src/Samples/TestExitCodeOverride/TestExitCodeOverride.proj b/src/Samples/TestExitCodeOverride/TestExitCodeOverride.proj
new file mode 100644
index 00000000000..d24d13b3ae8
--- /dev/null
+++ b/src/Samples/TestExitCodeOverride/TestExitCodeOverride.proj
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
+ System.Runtime.InteropServices.OSPlatform.Windows) ? "cmd.exe" : "sh";
+
+ protected override string GenerateFullPathToTool() =>
+ System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
+ System.Runtime.InteropServices.OSPlatform.Windows)
+ ? System.IO.Path.Combine(
+ System.Environment.GetFolderPath(System.Environment.SpecialFolder.System), "cmd.exe")
+ : "/bin/sh";
+
+ protected override string GenerateCommandLineCommands() =>
+ System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
+ System.Runtime.InteropServices.OSPlatform.Windows)
+ ? "/C echo file.cs(1,1): error ERR002: Fake error from basic ToolTask"
+ : "-c \"echo 'file.cs(1,1): error ERR002: Fake error from basic ToolTask'\"";
+}
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tasks.UnitTests/Exec_Tests.cs b/src/Tasks.UnitTests/Exec_Tests.cs
index 2f0f9e1ace3..3a820e75421 100644
--- a/src/Tasks.UnitTests/Exec_Tests.cs
+++ b/src/Tasks.UnitTests/Exec_Tests.cs
@@ -246,7 +246,18 @@ public void LoggedErrorsCauseFailureDespiteExitCode0()
Assert.False(result);
// Exitcode is set to -1
Assert.Equal(-1, exec.ExitCode);
- ((MockEngine)exec.BuildEngine).AssertLogContains("MSB3073");
+
+ MockEngine engine = (MockEngine)exec.BuildEngine;
+
+ // Should log the special "exited zero with errors" message
+ engine.AssertLogContains("MSB3077");
+
+ // Should not log other failure-related error codes
+ engine.AssertLogDoesntContain("MSB3073");
+ engine.AssertLogDoesntContain("MSB6006");
+
+ // Errors: canonical error from tool output + MSB3077 from HandleTaskExecutionErrors
+ engine.Errors.ShouldBe(2);
}
[Fact]
diff --git a/src/Tasks.UnitTests/XamlDataDrivenToolTask_Tests.cs b/src/Tasks.UnitTests/XamlDataDrivenToolTask_Tests.cs
index cefb96e614a..060a42bb9a3 100644
--- a/src/Tasks.UnitTests/XamlDataDrivenToolTask_Tests.cs
+++ b/src/Tasks.UnitTests/XamlDataDrivenToolTask_Tests.cs
@@ -5,8 +5,14 @@
using System.IO;
using System.Reflection;
using Microsoft.Build.Evaluation;
+using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
+using Microsoft.Build.Tasks;
+using Microsoft.Build.Tasks.Xaml;
+using Microsoft.Build.Utilities;
+using Shouldly;
using Xunit;
+using Xunit.Abstractions;
#nullable disable
@@ -382,4 +388,65 @@ public void SquareBracketEscaping()
logger.AssertLogContains("echo 14) [[notaproperty]] end");
}
}
+
+ ///
+ /// Tests for XamlDataDrivenToolTask error handling using direct task instantiation.
+ ///
+ public class ErrorHandlingTests
+ {
+ private readonly ITestOutputHelper _output;
+
+ public ErrorHandlingTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ ///
+ /// Tests that when a XamlDataDrivenTask's tool exits with code 0 but canonical errors
+ /// were logged during execution, the special "exited zero with errors" message (MSB3725)
+ /// is used instead of the normal command failure message (MSB3721).
+ ///
+ [Fact]
+ public void ExitCodeZeroWithLoggedErrors_LogsMSB3725()
+ {
+ var cmdLine = NativeMethodsShared.IsWindows
+ ? "echo myfile(88,37): error AB1234: thisisacanonicalerror"
+ : "echo \"myfile(88,37): error AB1234: thisisacanonicalerror\"";
+
+ var task = new TestableXamlDataDrivenToolTask();
+ task.BuildEngine = new MockEngine(_output);
+ task.CommandLineTemplate = cmdLine;
+ task.EchoOff = true;
+
+ bool result = task.Execute();
+
+ result.ShouldBeFalse();
+
+ MockEngine engine = (MockEngine)task.BuildEngine;
+
+ // Should log the special "exited zero with errors" message, not the normal command failure
+ engine.AssertLogContains("MSB3725");
+
+ // Should not log other error messages related to XamlDataDrivenToolTask command failure
+ engine.AssertLogDoesntContain("MSB3721");
+ engine.AssertLogDoesntContain("MSB3722");
+
+ // Errors: canonical error from tool output + MSB3725 from HandleTaskExecutionErrors
+ engine.Errors.ShouldBe(2);
+ }
+ }
+
+ ///
+ /// A concrete subclass of XamlDataDrivenToolTask for direct unit testing
+ /// without requiring a project file or XamlTaskFactory.
+ ///
+ internal sealed class TestableXamlDataDrivenToolTask : XamlDataDrivenToolTask
+ {
+ public TestableXamlDataDrivenToolTask()
+ : base(Array.Empty(), null)
+ {
+ }
+
+ protected override string ToolName => "unused";
+ }
}
diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs
index cf87ebedec1..d59c05f4d04 100644
--- a/src/Tasks/Exec.cs
+++ b/src/Tasks/Exec.cs
@@ -325,10 +325,15 @@ protected override bool HandleTaskExecutionErrors()
{
Log.LogErrorWithCodeFromResources("Exec.CommandFailedAccessDenied", commandForLog, ExitCode);
}
+ else if (ExitCodeOverriddenToIndicateErrors)
+ {
+ Log.LogErrorWithCodeFromResources("Exec.CommandExitedZeroWithErrors", commandForLog);
+ }
else
{
Log.LogErrorWithCodeFromResources("Exec.CommandFailed", commandForLog, ExitCode);
}
+
return false;
}
diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx
index d387956f108..fb697866cd8 100644
--- a/src/Tasks/Resources/Strings.resx
+++ b/src/Tasks/Resources/Strings.resx
@@ -397,6 +397,10 @@
MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again.
{StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized.
+
+ MSB3072: The "Exec" task needs a command to execute.
+ {StrBegin="MSB3072: "}LOCALIZATION: "Exec" should not be localized.
+
MSB3073: The command "{0}" exited with code {1}.
{StrBegin="MSB3073: "}
@@ -412,9 +416,9 @@
MSB3076: The regular expression "{0}" that was supplied is invalid. {1}
{StrBegin="MSB3076: "}
-
- MSB3072: The "Exec" task needs a command to execute.
- {StrBegin="MSB3072: "}LOCALIZATION: "Exec" should not be localized.
+
+ MSB3077: The command "{0}" exited with return value 0, but errors were detected during execution. ExitCode was set to -1.
+ {StrBegin="MSB3077: "}
The working directory "{0}" does not exist.
@@ -2319,6 +2323,10 @@
MSB3724: Unable to create Xaml task. Duplicate property name '{0}'.
{StrBegin="MSB3724: "}
+
+ MSB3725: The command "{0}" exited with return value 0, but errors were detected during execution. ExitCode was set to -1.
+ {StrBegin="MSB3725: "}
+