File: BackEnd\MSBuildClient_Tests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
 
using System;
using System.IO;
using System.Threading;
using Microsoft.Build.Experimental;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
 
namespace Microsoft.Build.UnitTests.BackEnd
{
    /// <summary>
    /// Tests for the <see cref="MSBuildClient"/> fallback behaviour.
    /// </summary>
    public class MSBuildClient_Tests
    {
        private readonly ITestOutputHelper _output;
 
        public MSBuildClient_Tests(ITestOutputHelper output)
        {
            _output = output;
        }
 
        /// <summary>
        /// When the configured msbuild executable does not exist, launching the server fails.
        /// <see cref="MSBuildClient.Execute"/> must return a recoverable exit type rather than
        /// letting an exception escape — it is the host's contract that any failure inside
        /// Execute routes through <see cref="MSBuildClientExitResult"/> so callers (e.g.
        /// <c>MSBuildClientApp</c>) can fall back to in-process execution.
        /// </summary>
        /// <remarks>
        /// Regression coverage for the .NET 10.0.300 / Aspire timeout: when
        /// <c>DOTNET_CLI_USE_MSBUILD_SERVER=true</c> is honoured but the server child cannot
        /// start (e.g. the apphost can't find the .NET runtime), <see cref="MSBuildClient.Execute"/>
        /// must not propagate a <see cref="System.TimeoutException"/> or any other exception.
        /// Pre-fix, an uncaught <c>TimeoutException</c> from <c>NamedPipeClientStream.Connect</c>
        /// escaped past <c>MSBuildClientApp</c> and crashed the CLI; this test locks in the
        /// no-exception-escape contract by simply calling <c>Execute</c> outside any try/catch
        /// and asserting on the structured result.
        /// </remarks>
        [Fact]
        public void Execute_WithUnreachableServer_DoesNotPropagateException()
        {
            // Isolate from any real MSBuild server / DOTNET_CLI_USE_MSBUILD_SERVER state on
            // the dev or CI machine. Without this the named-mutex check in MSBuildClient can
            // observe a warm server from another test/run, which makes the assertions below
            // non-deterministic.
            using TestEnvironment env = TestEnvironment.Create(_output);
            env.SetEnvironmentVariable("DOTNET_CLI_USE_MSBUILD_SERVER", null);
            env.SetEnvironmentVariable("MSBUILDUSESERVER", null);
 
            string[] commandLine = ["dummy.proj"];
            string nonexistentMsBuild = Path.Combine(
                Path.GetTempPath(),
                "msbuildclient-tests-" + Guid.NewGuid().ToString("N"),
                "MSBuild.dll");
 
            MSBuildClient client = new MSBuildClient(commandLine, nonexistentMsBuild);
 
            // The whole point of the regression fix: this must NOT throw. xUnit fails the
            // test with the offending stack if any exception escapes, which is the
            // primary regression contract being verified.
            MSBuildClientExitResult result = client.Execute(CancellationToken.None);
 
            result.ShouldNotBeNull();
 
            // The unreachable-server path must produce one of the recoverable failure types
            // so that MSBuildClientApp can fall back to in-process execution. Crucially,
            // ServerBusy is excluded here: ServerBusy is the "another client is racing for
            // the launch mutex" path and is not what an unreachable server should produce —
            // accepting it would mask a real regression where the failure was misclassified.
            result.MSBuildClientExitType.ShouldBeOneOf(
                MSBuildClientExitType.LaunchError,
                MSBuildClientExitType.UnableToConnect,
                MSBuildClientExitType.UnknownServerState);
 
            // No server child was successfully launched, so the diagnostic helper should not
            // have observed an exit code. (Once issue #13718 lands and the diagnostic helper
            // is plumbed through every connect failure, this gives MSBuildClientApp the right
            // signal to pick the generic "server unavailable" message rather than the more
            // specific "crashed with exit code N" one.)
            result.ServerProcessExitCode.ShouldBeNull();
        }
    }
}