File: XMake_BinlogSwitch_Tests.cs
Web Access
Project: ..\..\..\src\MSBuild.UnitTests\Microsoft.Build.CommandLine.UnitTests.csproj (Microsoft.Build.CommandLine.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.CommandLine.Experimental;
using Microsoft.Build.Execution;
using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
 
#nullable disable
 
namespace Microsoft.Build.UnitTests
{
    /// <summary>
    /// Tests for MSBUILD_LOGGING_ARGS environment variable functionality.
    /// </summary>
    public class XMakeBinlogSwitchTests : IDisposable
    {
        private readonly ITestOutputHelper _output;
        private readonly TestEnvironment _env;
 
        public XMakeBinlogSwitchTests(ITestOutputHelper output)
        {
            _output = output;
            _env = TestEnvironment.Create(output);
        }
 
        public void Dispose() => _env.Dispose();
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS with -bl creates a binary log.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarWithBinaryLogger()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
            string binlogPath = Path.Combine(directory.Path, "test.binlog");
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath}");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created at {binlogPath}");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS with multiple -bl switches creates multiple binary logs.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarWithMultipleBinaryLoggers()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
            string binlogPath1 = Path.Combine(directory.Path, "test1.binlog");
            string binlogPath2 = Path.Combine(directory.Path, "test2.binlog");
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath1} -bl:{binlogPath2}");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            File.Exists(binlogPath1).ShouldBeTrue($"First binary log should have been created at {binlogPath1}");
            File.Exists(binlogPath2).ShouldBeTrue($"Second binary log should have been created at {binlogPath2}");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS with {} placeholder generates unique filenames.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarWithWildcardPlaceholder()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            // Use {} placeholder for unique filename generation
            string binlogPattern = Path.Combine(directory.Path, "build-{}.binlog");
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPattern}");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            // Find the generated binlog file (should have unique characters instead of {})
            string[] binlogFiles = Directory.GetFiles(directory.Path, "build-*.binlog");
            binlogFiles.Length.ShouldBe(1, $"Expected exactly one binlog file to be created in {directory.Path}");
 
            // The filename should not contain {} - it should have been replaced with unique characters
            binlogFiles[0].ShouldNotContain("{}");
            binlogFiles[0].ShouldContain("build-");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS with multiple {} placeholders generates unique filenames with each placeholder replaced.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarWithMultipleWildcardPlaceholders()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            // Use multiple {} placeholders for unique filename generation
            string binlogPattern = Path.Combine(directory.Path, "build-{}-test-{}.binlog");
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPattern}");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            // Find the generated binlog file (should have unique characters instead of {})
            string[] binlogFiles = Directory.GetFiles(directory.Path, "build-*-test-*.binlog");
            binlogFiles.Length.ShouldBe(1, $"Expected exactly one binlog file to be created in {directory.Path}");
 
            // The filename should not contain {} - both placeholders should have been replaced
            binlogFiles[0].ShouldNotContain("{}");
            binlogFiles[0].ShouldContain("build-");
            binlogFiles[0].ShouldContain("-test-");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS ignores unsupported arguments and continues with valid ones.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarIgnoresUnsupportedArguments()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
            string binlogPath = Path.Combine(directory.Path, "test.binlog");
 
            // Set env var with mixed valid and invalid arguments
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath} -maxcpucount:4 -verbosity:detailed");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            // Binary log should still be created (valid argument)
            File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created at {binlogPath}");
 
            // Warning should appear for invalid arguments
            output.ShouldContain("MSB1070");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS works with /noautoresponse.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarWorksWithNoAutoResponse()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
            string binlogPath = Path.Combine(directory.Path, "test.binlog");
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath}");
 
            // Use /noautoresponse - MSBUILD_LOGGING_ARGS should still work
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\" /noautoresponse", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created even with /noautoresponse");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS_LEVEL=message emits diagnostics as messages instead of warnings.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarLevelMessageSuppressesWarnings()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4");
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS_LEVEL", "message");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            output.ShouldNotContain("MSB1070");
        }
 
        /// <summary>
        /// Test that MSBUILD_LOGGING_ARGS emits warnings by default when MSBUILD_LOGGING_ARGS_LEVEL is not set.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarDefaultLevelEmitsWarnings()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            // Set env var with invalid argument, but do NOT set MSBUILD_LOGGING_ARGS_LEVEL
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            // Warning SHOULD appear when level is not set (default behavior)
            output.ShouldContain("MSB1070");
        }
 
        /// <summary>
        /// Test that empty or whitespace MSBUILD_LOGGING_ARGS is ignored.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarEmptyIsIgnored()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "   ");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
        }
 
        /// <summary>
        /// Test that -check switch is allowed in MSBUILD_LOGGING_ARGS.
        /// </summary>
        [Fact]
        public void LoggingArgsEnvVarAllowsCheckSwitch()
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-check");
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            output.ShouldNotContain("MSB1070");
        }
 
        /// <summary>
        /// Test that only logging-related switches are allowed.
        /// </summary>
        [Theory]
        [InlineData("-bl")]
        [InlineData("-bl:test.binlog")]
        [InlineData("-binarylogger")]
        [InlineData("-binarylogger:test.binlog")]
        [InlineData("/bl")]
        [InlineData("/bl:test.binlog")]
        [InlineData("--bl")]
        [InlineData("-check")]
        [InlineData("/check")]
        public void LoggingArgsEnvVarAllowedSwitches(string switchArg)
        {
            CommandLineParser parser = new();
            _ = _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg);
 
            CommandLineSwitches switches = new();
            List<BuildManager.DeferredBuildMessage> deferredBuildMessages = new();
            parser.GatherLoggingArgsEnvironmentVariableSwitches(ref switches, deferredBuildMessages, "test");
 
            switches.HaveErrors().ShouldBeFalse($"Switch {switchArg} should be allowed");
        }
 
        /// <summary>
        /// Test that non-logging switches are rejected.
        /// </summary>
        [Theory]
        [InlineData("-property:A=1")]
        [InlineData("-target:Build")]
        [InlineData("-verbosity:detailed")]
        [InlineData("-maxcpucount:4")]
        [InlineData("/p:A=1")]
        [InlineData("-restore")]
        [InlineData("-nologo")]
        public void LoggingArgsEnvVarDisallowedSwitches(string switchArg)
        {
            var directory = _env.CreateFolder();
            string content = ObjectModelHelpers.CleanupFileContents("<Project><Target Name='t'><Message Text='Hello'/></Target></Project>");
            var projectPath = directory.CreateFile("my.proj", content).Path;
 
            _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg);
 
            string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output);
            successfulExit.ShouldBeTrue(output);
 
            output.ShouldContain("MSB1070");
        }
    }
}