Skip to content
Merged
119 changes: 119 additions & 0 deletions src/Samples/TestExitCodeOverride/TestExitCodeOverride.proj
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<Project>
<!--
Manual test project for ExitCodeOverriddenToIndicateErrors feature.

Both targets run a command that exits with code 0 but produces output
in canonical error format, which MSBuild parses as an error. This causes
ExitCode to be overridden from 0 to -1 and triggers the new error messages.

Usage (from repo root, after building with .\build.cmd):
artifacts\bin\bootstrap\core\dotnet.exe build src\Samples\TestExitCodeOverride\TestExitCodeOverride.proj /t:TestExec -v:diag
artifacts\bin\bootstrap\core\dotnet.exe build src\Samples\TestExitCodeOverride\TestExitCodeOverride.proj /t:TestToolTask -v:diag
artifacts\bin\bootstrap\core\dotnet.exe build src\Samples\TestExitCodeOverride\TestExitCodeOverride.proj /t:TestXamlToolTask -v:diag (Windows only)
artifacts\bin\bootstrap\core\dotnet.exe build src\Samples\TestExitCodeOverride\TestExitCodeOverride.proj /t:TestAll -v:diag

Expected results:
TestExec -> MSB3077: The command "..." exited with return value 0, but errors were detected ...
TestToolTask -> (low-importance) "The command exited with return value 0, but errors were detected ..."
plus MSB6006 error from the base ToolTask.HandleTaskExecutionErrors
TestXamlToolTask -> MSB3725: The command "..." exited with return value 0, but errors were detected ...
(Windows only, uses XamlTaskFactory with cmd.exe)
-->

<!-- ============================================================ -->
<!-- Test 1: Exec task -->
<!-- The echo command outputs a canonical error and exits 0. -->
<!-- Exec.HandleTaskExecutionErrors logs MSB3077. -->
<!-- ============================================================ -->
<Target Name="TestExec">
<Message Text="=== Test 1: Exec task — canonical error on stdout, exit code 0 ===" Importance="high" />
<Exec Command="echo file.cs(1,1): error ERR001: This is a fake error from a tool that exited successfully"
ContinueOnError="true" />
<Message Text="=== Test 1 complete. Look for MSB3077 above. ===" Importance="high" />
</Target>

<!-- ============================================================ -->
<!-- Test 2: Basic ToolTask (inline task, no HandleTaskExecutionErrors override) -->
<!-- Uses RoslynCodeTaskFactory to define a ToolTask derivative -->
<!-- that does NOT override HandleTaskExecutionErrors. -->
<!-- Base class logs low-importance message + MSB6006 error. -->
<!-- ============================================================ -->
<UsingTask TaskName="BasicToolTask"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<Task>
<Reference Include="$(MSBuildToolsPath)\Microsoft.Build.Utilities.Core.dll" />
<Code Type="Class" Language="cs">
<![CDATA[
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class BasicToolTask : ToolTask
{
protected override string ToolName =>
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'\"";
}
]]>
</Code>
</Task>
</UsingTask>

<Target Name="TestToolTask">
<Message Text="=== Test 2: Basic ToolTask — canonical error on stdout, exit code 0 ===" Importance="high" />
<BasicToolTask ContinueOnError="true" />
<Message Text="=== Test 2 complete. Look for MSB6006 above (and 'exited with return value 0' at -v:diag). ===" Importance="high" />
</Target>

<!-- ============================================================ -->
<!-- Test 3: XamlDataDrivenToolTask (via XamlTaskFactory) -->
<!-- Uses XamlTaskFactory to define a task backed by cmd.exe. -->
<!-- The command template echoes a canonical error and exits 0. -->
<!-- XamlDataDrivenToolTask.HandleTaskExecutionErrors logs -->
<!-- MSB3725. -->
<!-- ============================================================ -->
<UsingTask TaskName="XamlToolTask"
TaskFactory="XamlTaskFactory"
AssemblyName="Microsoft.Build.Tasks.Core">
<Task>
<![CDATA[
<ProjectSchemaDefinitions xmlns="clr-namespace:Microsoft.Build.Framework.XamlTypes;assembly=Microsoft.Build.Framework"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Rule Name="XamlToolTask" ToolName="cmd.exe">
<StringProperty Name="CommandLineTemplate" />
</Rule>
</ProjectSchemaDefinitions>
]]>
</Task>
</UsingTask>

<Target Name="TestXamlToolTask" Condition="'$(OS)' == 'Windows_NT'">
<Message Text="=== Test 3: XamlDataDrivenToolTask — canonical error on stdout, exit code 0 ===" Importance="high" />
<XamlToolTask CommandLineTemplate="echo file.cs(1,1): error ERR003: Fake error from XamlDataDrivenToolTask"
EchoOff="true"
ContinueOnError="true" />
<Message Text="=== Test 3 complete. Look for MSB3725 above. ===" Importance="high" />
</Target>

<!-- ============================================================ -->
<!-- Run all tests -->
<!-- ============================================================ -->
<Target Name="TestAll" DependsOnTargets="TestExec;TestToolTask;TestXamlToolTask">
<Message Text="=== All tests complete ===" Importance="high" />
</Target>

</Project>
13 changes: 12 additions & 1 deletion src/Tasks.UnitTests/Exec_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
67 changes: 67 additions & 0 deletions src/Tasks.UnitTests/XamlDataDrivenToolTask_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -382,4 +388,65 @@ public void SquareBracketEscaping()
logger.AssertLogContains("echo 14) [[notaproperty]] end");
}
}

/// <summary>
/// Tests for XamlDataDrivenToolTask error handling using direct task instantiation.
/// </summary>
public class ErrorHandlingTests
{
private readonly ITestOutputHelper _output;

public ErrorHandlingTests(ITestOutputHelper output)
{
_output = output;
}

/// <summary>
/// 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).
/// </summary>
[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);
}
}

/// <summary>
/// A concrete subclass of XamlDataDrivenToolTask for direct unit testing
/// without requiring a project file or XamlTaskFactory.
/// </summary>
internal sealed class TestableXamlDataDrivenToolTask : XamlDataDrivenToolTask
{
public TestableXamlDataDrivenToolTask()
: base(Array.Empty<string>(), null)
{
}

protected override string ToolName => "unused";
}
}
5 changes: 5 additions & 0 deletions src/Tasks/Exec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
14 changes: 11 additions & 3 deletions src/Tasks/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@
<value>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.</value>
<comment>{StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized.</comment>
</data>
<data name="Exec.MissingCommandError">
<value>MSB3072: The "Exec" task needs a command to execute.</value>
<comment>{StrBegin="MSB3072: "}LOCALIZATION: "Exec" should not be localized.</comment>
</data>
<data name="Exec.CommandFailed">
<value>MSB3073: The command "{0}" exited with code {1}.</value>
<comment>{StrBegin="MSB3073: "}</comment>
Expand All @@ -412,9 +416,9 @@
<value>MSB3076: The regular expression "{0}" that was supplied is invalid. {1}</value>
<comment>{StrBegin="MSB3076: "}</comment>
</data>
<data name="Exec.MissingCommandError">
<value>MSB3072: The "Exec" task needs a command to execute.</value>
<comment>{StrBegin="MSB3072: "}LOCALIZATION: "Exec" should not be localized.</comment>
<data name="Exec.CommandExitedZeroWithErrors">
<value>MSB3077: The command "{0}" exited with return value 0, but errors were detected during execution. ExitCode was set to -1.</value>
<comment>{StrBegin="MSB3077: "}</comment>
</data>
<data name="Exec.InvalidWorkingDirectory">
<value>The working directory "{0}" does not exist.</value>
Expand Down Expand Up @@ -2319,6 +2323,10 @@
<value>MSB3724: Unable to create Xaml task. Duplicate property name '{0}'.</value>
<comment>{StrBegin="MSB3724: "}</comment>
</data>
<data name="Xaml.CommandExitedZeroWithErrors">
<value>MSB3725: The command "{0}" exited with return value 0, but errors were detected during execution. ExitCode was set to -1.</value>
<comment>{StrBegin="MSB3725: "}</comment>
</data>

<!--
The XslTransformation message bucket is: MSB3701-3710.
Expand Down
10 changes: 10 additions & 0 deletions src/Tasks/Resources/xlf/Strings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Tasks/Resources/xlf/Strings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Tasks/Resources/xlf/Strings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading