File: BackEnd\TaskHost_MultiThreadableTask_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.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Tasks;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.Build.Engine.UnitTests.BackEnd
{
    /// <summary>
    /// Tests that IMultiThreadableTask implementations always have a usable TaskEnvironment,
    /// even when explicitly instantiated or run in the out-of-proc task host.
    /// </summary>
    public class TaskHost_MultiThreadableTask_Tests : IDisposable
    {
        private readonly ITestOutputHelper _output;
        private readonly TestEnvironment _env;
        private readonly string _testProjectsDir;
 
        public TaskHost_MultiThreadableTask_Tests(ITestOutputHelper output)
        {
            _output = output;
            _env = TestEnvironment.Create(output);
            _testProjectsDir = _env.CreateFolder().Path;
        }
 
        public void Dispose()
        {
            _env.Dispose();
        }
 
        /// <summary>
        /// A subclass of a built-in IMultiThreadableTask (MakeDir) should inherit the
        /// non-null default TaskEnvironment. This covers the scenario where someone
        /// derives from a built-in task and explicitly instantiates it.
        /// </summary>
        [Fact]
        public void ExplicitlyInstantiated_InheritedTask_HasNonNullTaskEnvironment()
        {
            var task = new DerivedMakeDirTask();
            IMultiThreadableTask multiThreadable = task;
 
            multiThreadable.TaskEnvironment.ShouldNotBeNull();
        }
 
        /// <summary>
        /// When a task that inherits from a built-in IMultiThreadableTask (MakeDir) runs in
        /// the out-of-proc task host (via TaskHostFactory), the inherited default TaskEnvironment
        /// should be usable. The task accesses TaskEnvironment.ProjectDirectory in Execute() —
        /// without the default it would NRE.
        /// </summary>
        [Fact]
        public void InheritedTask_InTaskHost_HasUsableTaskEnvironment()
        {
            string projectContent = $"""
                <Project>
                    <UsingTask TaskName="DerivedMakeDirTask"
                               AssemblyFile="{Assembly.GetExecutingAssembly().Location}"
                               TaskFactory="TaskHostFactory" />
 
                    <Target Name="TestTarget">
                        <DerivedMakeDirTask Directories="does-not-matter" />
                    </Target>
                </Project>
                """;
 
            string projectFile = Path.Combine(_testProjectsDir, "TaskEnvTest.proj");
            File.WriteAllText(projectFile, projectContent);
 
            var logger = new MockLogger(_output);
            var buildParameters = new BuildParameters
            {
                Loggers = [logger],
                DisableInProcNode = false,
                EnableNodeReuse = false,
            };
 
            var buildRequestData = new BuildRequestData(
                projectFile,
                new Dictionary<string, string?>(),
                null,
                ["TestTarget"],
                null);
 
            var result = BuildManager.DefaultBuildManager.Build(buildParameters, buildRequestData);
 
            _output.WriteLine(logger.FullLog);
 
            result.OverallResult.ShouldBe(BuildResultCode.Success);
 
            // Verify the task actually ran in the task host
            TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, "DerivedMakeDirTask");
 
            // Verify the task was able to read ProjectDirectory without NRE
            logger.FullLog.ShouldContain("TaskEnvironment.ProjectDirectory=");
        }
    }
 
    /// <summary>
    /// Task that inherits from the built-in MakeDir (which implements IMultiThreadableTask).
    /// Does NOT declare its own TaskEnvironment — it relies on the inherited default from MakeDir.
    /// Overrides Execute() to log TaskEnvironment.ProjectDirectory, proving the default works.
    /// </summary>
    public class DerivedMakeDirTask : MakeDir
    {
        public override bool Execute()
        {
            string projectDir = TaskEnvironment.ProjectDirectory;
            Log.LogMessage(MessageImportance.High, $"TaskEnvironment.ProjectDirectory={projectDir}");
            return true;
        }
    }
}