File: MuxLogger_Tests.cs
Web Access
Project: ..\..\..\src\Utilities.UnitTests\Microsoft.Build.Utilities.UnitTests.csproj (Microsoft.Build.Utilities.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.IO;
using System.Threading;
using System.Xml;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
using MuxLogger = Microsoft.Build.Utilities.MuxLogger;
 
#nullable disable
 
namespace Microsoft.VisualStudio.Build.UnitTest
{
    /// <summary>
    /// Tests for the MuxLogger.
    /// </summary>
    public class MuxLogger_Tests
    {
        /// <summary>
        /// Verifies that an empty build with no loggers causes no exceptions.
        /// </summary>
        [Fact]
        public void EmptyBuildWithNoLoggers()
        {
            BuildManager buildManager = BuildManager.DefaultBuildManager;
            MuxLogger muxLogger = new MuxLogger();
            BuildParameters parameters = new BuildParameters();
            parameters.Loggers = new ILogger[] { muxLogger };
            buildManager.BeginBuild(parameters);
            buildManager.EndBuild();
        }
 
        /// <summary>
        /// Verifies that a simple build with no loggers causes no exceptions.
        /// </summary>
        [Fact]
        public void SimpleBuildWithNoLoggers()
        {
            string projectBody = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Message Text='Foo'/>
        <Error Text='Error'/>
    </Target>
</Project>
");
            using ProjectFromString projectFromString = new(projectBody);
            ProjectInstance project = (projectFromString.Project).CreateProjectInstance();
 
            BuildManager buildManager = BuildManager.DefaultBuildManager;
            MuxLogger muxLogger = new MuxLogger();
            BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection);
            parameters.Loggers = new ILogger[] { muxLogger };
            buildManager.Build(parameters, new BuildRequestData(project, Array.Empty<string>(), null));
        }
 
        /// <summary>
        /// Verifies that attempting to register a logger before a build has started is invalid.
        /// </summary>
        [Fact]
        public void RegisteringLoggerBeforeBuildStartedThrows()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                MuxLogger muxLogger = new MuxLogger();
                muxLogger.RegisterLogger(1, new MockLogger());
            });
        }
        /// <summary>
        /// Verifies that building with a logger attached to the mux logger is equivalent to building with the logger directly.
        /// </summary>
        [Fact]
        public void BuildWithMuxLoggerEquivalentToNormalLogger()
        {
            string projectBody = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Message Text='Foo'/>
        <Error Text='Error'/>
    </Target>
</Project>
");
 
            BuildManager buildManager = BuildManager.DefaultBuildManager;
 
            // Build with a 'normal' logger
            MockLogger mockLogger2 = new MockLogger();
            mockLogger2.LogBuildFinished = false;
            using ProjectCollection projectCollection = new ProjectCollection();
            using ProjectFromString projectFromString = new(projectBody, null, ObjectModelHelpers.MSBuildDefaultToolsVersion, projectCollection);
            ProjectInstance project = projectFromString.Project.CreateProjectInstance();
            BuildParameters parameters = new BuildParameters(projectCollection);
            parameters.Loggers = new ILogger[] { mockLogger2 };
            buildManager.Build(parameters, new BuildRequestData(project, Array.Empty<string>(), null));
 
            // Build with the mux logger
            MuxLogger muxLogger = new MuxLogger();
            muxLogger.Verbosity = LoggerVerbosity.Normal;
            using var collection = new ProjectCollection();
            using ProjectFromString projectFromString1 = new(projectBody, null, ObjectModelHelpers.MSBuildDefaultToolsVersion, collection);
            project = projectFromString1.Project.CreateProjectInstance();
            parameters = new BuildParameters(collection);
            parameters.Loggers = new ILogger[] { muxLogger };
            buildManager.BeginBuild(parameters);
            MockLogger mockLogger = new MockLogger();
            mockLogger.LogBuildFinished = false;
 
            try
            {
                BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, Array.Empty<string>(), null));
                muxLogger.RegisterLogger(submission.SubmissionId, mockLogger);
                submission.Execute();
            }
            finally
            {
                buildManager.EndBuild();
            }
 
            mockLogger2.BuildFinishedEvents.Count.ShouldBeGreaterThan(0);
            mockLogger.BuildFinishedEvents.Count.ShouldBe(mockLogger2.BuildFinishedEvents.Count);
            mockLogger.BuildFinishedEvents[0].Succeeded.ShouldBe(mockLogger2.BuildFinishedEvents[0].Succeeded);
 
            // This test was changed to not compare new lines because of https://github.com/dotnet/msbuild/issues/10493
            // It will need to be changed once we fix the root cause of the issue
            mockLogger.FullLog.Replace(Environment.NewLine, "").ShouldBe(mockLogger2.FullLog.Replace(Environment.NewLine, ""));
        }
 
        /// <summary>
        /// Verifies correctness of a simple build with one logger.
        /// </summary>
        [Fact]
        public void OneSubmissionOneLogger()
        {
            string projectBody = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Message Text='Foo'/>
        <Error Text='Error'/>
    </Target>
</Project>
");
            using ProjectFromString projectFromString = new(projectBody);
            ProjectInstance project = (projectFromString.Project).CreateProjectInstance();
 
            BuildManager buildManager = BuildManager.DefaultBuildManager;
            MuxLogger muxLogger = new MuxLogger();
            BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection);
            parameters.Loggers = new ILogger[] { muxLogger };
            buildManager.BeginBuild(parameters);
            MockLogger mockLogger = new MockLogger();
 
            try
            {
                BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, Array.Empty<string>(), null));
 
                muxLogger.RegisterLogger(submission.SubmissionId, mockLogger);
                submission.Execute();
            }
            finally
            {
                buildManager.EndBuild();
            }
 
            mockLogger.AssertLogContains("Foo");
            mockLogger.AssertLogContains("Error");
            mockLogger.ErrorCount.ShouldBe(1);
            mockLogger.AssertNoWarnings();
        }
 
        /// <summary>
        /// Verifies correctness of a two submissions in a single build using separate loggers.
        /// </summary>
        [Fact]
        public void TwoSubmissionsWithSeparateLoggers()
        {
            string projectBody1 = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Message Text='Foo'/>
        <Error Text='Error'/>
    </Target>
</Project>
");
 
            string projectBody2 = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Message Text='Bar'/>
        <Warning Text='Warning'/>
    </Target>
</Project>
");
 
            using ProjectFromString projectFromString1 = new(projectBody1);
            using ProjectFromString projectFromString2 = new(projectBody2);
            ProjectInstance project1 = projectFromString1.Project.CreateProjectInstance();
            ProjectInstance project2 = projectFromString2.Project.CreateProjectInstance();
 
            BuildManager buildManager = BuildManager.DefaultBuildManager;
            MuxLogger muxLogger = new MuxLogger();
            BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection);
            parameters.Loggers = new ILogger[] { muxLogger };
            MockLogger mockLogger1 = new MockLogger();
            MockLogger mockLogger2 = new MockLogger();
            buildManager.BeginBuild(parameters);
 
            try
            {
                BuildSubmission submission1 = buildManager.PendBuildRequest(new BuildRequestData(project1, Array.Empty<string>(), null));
                muxLogger.RegisterLogger(submission1.SubmissionId, mockLogger1);
                submission1.Execute();
 
                BuildSubmission submission2 = buildManager.PendBuildRequest(new BuildRequestData(project2, Array.Empty<string>(), null));
                muxLogger.RegisterLogger(submission2.SubmissionId, mockLogger2);
                submission2.Execute();
            }
            finally
            {
                buildManager.EndBuild();
            }
 
            mockLogger1.AssertLogContains("Foo");
            mockLogger1.AssertLogContains("Error");
            mockLogger1.AssertLogDoesntContain("Bar");
            mockLogger1.AssertLogDoesntContain("Warning");
            mockLogger1.ErrorCount.ShouldBe(1);
            mockLogger1.WarningCount.ShouldBe(0);
 
            mockLogger2.AssertLogDoesntContain("Foo");
            mockLogger2.AssertLogDoesntContain("Error");
            mockLogger2.AssertLogContains("Bar");
            mockLogger2.AssertLogContains("Warning");
            mockLogger2.ErrorCount.ShouldBe(0);
            mockLogger2.WarningCount.ShouldBe(1);
        }
 
        /// <summary>
        /// Verifies correctness of a simple build with one logger.
        /// </summary>
        [Fact]
        public void OneSubmissionTwoLoggers()
        {
            string projectBody = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Message Text='Foo'/>
        <Error Text='Error'/>
    </Target>
</Project>
");
            using ProjectFromString projectFromString = new(projectBody);
            ProjectInstance project = (projectFromString.Project).CreateProjectInstance();
 
            BuildManager buildManager = BuildManager.DefaultBuildManager;
            MuxLogger muxLogger = new MuxLogger();
            BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection);
            parameters.Loggers = new ILogger[] { muxLogger };
            MockLogger mockLogger1 = new MockLogger();
            MockLogger mockLogger2 = new MockLogger();
            buildManager.BeginBuild(parameters);
            try
            {
                BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, Array.Empty<string>(), null));
 
                muxLogger.RegisterLogger(submission.SubmissionId, mockLogger1);
                muxLogger.RegisterLogger(submission.SubmissionId, mockLogger2);
                submission.Execute();
            }
            finally
            {
                buildManager.EndBuild();
            }
 
            mockLogger1.AssertLogContains("Foo");
            mockLogger1.AssertLogContains("Error");
            Assert.Equal(1, mockLogger1.ErrorCount);
            mockLogger1.AssertNoWarnings();
 
            mockLogger2.AssertLogContains("Foo");
            mockLogger2.AssertLogContains("Error");
            mockLogger2.ErrorCount.ShouldBe(1);
            mockLogger2.AssertNoWarnings();
 
            mockLogger2.FullLog.ShouldBe(mockLogger1.FullLog);
        }
 
        /// <summary>
        /// Verifies correctness of a simple build with one logger.
        /// </summary>
        [Fact]
        public void RegisteringLoggerDuringBuildThrowsException()
        {
            string projectBody = ObjectModelHelpers.CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
    <Target Name='Test'>
        <Exec Command='Sleep 1' />
    </Target>
</Project>
");
            using ProjectFromString projectFromString = new(projectBody);
            ProjectInstance project = (projectFromString.Project).CreateProjectInstance();
 
            BuildManager buildManager = BuildManager.DefaultBuildManager;
            MuxLogger muxLogger = new MuxLogger();
            BuildParameters parameters = new BuildParameters(ProjectCollection.GlobalProjectCollection);
            AutoResetEvent projectStartedEvent = new AutoResetEvent(false);
            parameters.Loggers = new ILogger[] { muxLogger, new EventingLogger(projectStartedEvent) };
            MockLogger mockLogger = new MockLogger();
            buildManager.BeginBuild(parameters);
 
            Should.Throw<InvalidOperationException>(() =>
            {
                try
                {
                    BuildSubmission submission = buildManager.PendBuildRequest(new BuildRequestData(project, Array.Empty<string>(), null));
 
                    submission.ExecuteAsync(null, null);
                    projectStartedEvent.WaitOne();
 
                    // This call should throw an InvalidOperationException
                    muxLogger.RegisterLogger(submission.SubmissionId, mockLogger);
                }
                finally
                {
                    buildManager.EndBuild();
                }
            });
        }
 
        /// <summary>
        /// A logger which signals an event when it gets a project started message.
        /// </summary>
        private sealed class EventingLogger : ILogger
        {
            /// <summary>
            /// The event source
            /// </summary>
            private IEventSource _eventSource;
 
            /// <summary>
            /// The event handler
            /// </summary>
            private ProjectStartedEventHandler _eventHandler;
 
            /// <summary>
            /// The event to signal.
            /// </summary>
            private readonly AutoResetEvent _projectStartedEvent;
 
            /// <summary>
            /// Constructor.
            /// </summary>
            public EventingLogger(AutoResetEvent projectStartedEvent)
            {
                _projectStartedEvent = projectStartedEvent;
            }
 
            /// <summary>
            /// Verbosity accessor.
            /// </summary>
            public LoggerVerbosity Verbosity
            {
                get => LoggerVerbosity.Normal;
                set { }
            }
 
            /// <summary>
            /// Parameters accessor.
            /// </summary>
            public string Parameters
            {
                get => null;
                set { }
            }
 
            /// <summary>
            /// Initialize the logger.
            /// </summary>
            public void Initialize(IEventSource eventSource)
            {
                _eventSource = eventSource;
                _eventHandler = ProjectStarted;
                _eventSource.ProjectStarted += _eventHandler;
            }
 
            /// <summary>
            /// Shut down the logger.
            /// </summary>
            public void Shutdown() => _eventSource.ProjectStarted -= _eventHandler;
 
            /// <summary>
            /// Event handler which signals the event.
            /// </summary>
            private void ProjectStarted(object sender, ProjectStartedEventArgs e) => _projectStartedEvent.Set();
        }
    }
}