|
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
using Microsoft.Build.Logging;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.Build.UnitTests.ObjectModelHelpers;
#nullable disable
namespace Microsoft.Build.UnitTests.BackEnd
{
/// <summary>
/// The test fixture for the BuildManager
/// </summary>
public class BuildManager_Tests : IDisposable
{
/// <summary>
/// The mock logger for testing.
/// </summary>
private readonly MockLogger _logger;
/// <summary>
/// The standard build manager for each test.
/// </summary>
private readonly BuildManager _buildManager;
/// <summary>
/// The build parameters.
/// </summary>
private readonly BuildParameters _parameters;
/// <summary>
/// The project collection used.
/// </summary>
private readonly ProjectCollection _projectCollection;
private readonly TestEnvironment _env;
private readonly ITestOutputHelper _output;
/// <summary>
/// The transient state corresponding to setting MSBUILDINPROCENVCHECK to 1.
/// </summary>
private readonly TransientTestState _inProcEnvCheckTransientEnvironmentVariable;
/// <summary>
/// SetUp
/// </summary>
public BuildManager_Tests(ITestOutputHelper output)
{
_output = output;
// Ensure that any previous tests which may have been using the default BuildManager do not conflict with us.
BuildManager.DefaultBuildManager.Dispose();
_logger = new MockLogger(output);
_parameters = new BuildParameters
{
ShutdownInProcNodeOnBuildFinish = true,
Loggers = new ILogger[] { _logger },
EnableNodeReuse = false
};
_buildManager = new BuildManager();
_projectCollection = new ProjectCollection(globalProperties: null, _parameters.Loggers, ToolsetDefinitionLocations.Default);
_env = TestEnvironment.Create(output);
_inProcEnvCheckTransientEnvironmentVariable = _env.SetEnvironmentVariable("MSBUILDINPROCENVCHECK", "1");
}
/// <summary>
/// TearDown
/// </summary>
public void Dispose()
{
try
{
_buildManager.Dispose();
_projectCollection.Dispose();
}
finally
{
_env.Dispose();
}
}
/// <summary>
/// Check that we behave reasonably when passed a null ProjectCollection
/// </summary>
[Fact]
public void BuildParametersWithNullCollection()
{
Assert.Throws<ArgumentNullException>(() => { new BuildParameters(null); });
}
/// <summary>
/// A simple successful build.
/// </summary>
[Fact]
public void SimpleBuild()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectEvaluationFinishedEventArgs evalFinishedEvent = _logger.EvaluationFinishedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(evalFinishedEvent.Properties);
Assert.True(properties.TryGetValue("InitialProperty1", out string propertyValue));
Assert.Equal("InitialProperty1", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty2", out propertyValue));
Assert.Equal("InitialProperty2", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty3", out propertyValue));
Assert.Equal("InitialProperty3", propertyValue);
}
[Fact]
public void SimpleP2PBuildInProc()
{
var newParameters = _parameters.Clone();
newParameters.DisableInProcNode = false;
newParameters.MaxNodeCount = 1;
SimpleP2PBuild(newParameters);
}
[Fact]
public void SimpleP2PBuildOutOfProc()
{
var newParameters = _parameters.Clone();
newParameters.DisableInProcNode = true;
newParameters.MaxNodeCount = 3;
SimpleP2PBuild(newParameters);
}
private void SimpleP2PBuild(BuildParameters buildParameters)
{
var graph = Helpers.CreateProjectGraph(
_env,
new Dictionary<int, int[]>
{
{1, new[] {2, 3}}
},
createProjectFile:
(env, projectNumber, references, targets, defaultTargets, content) =>
{
return Helpers.CreateProjectFile(
env,
projectNumber,
references,
targets,
"ActualBuild",
@"<Target Name='ActualBuild'>
<MSBuild Projects='@(ProjectReference)'/>
</Target>");
});
var result = _buildManager.Build(
buildParameters,
new BuildRequestData(
graph.GraphRoots.FirstOrDefault()
.ProjectInstance.FullPath,
new Dictionary<string, string>(),
MSBuildConstants.CurrentToolsVersion,
Array.Empty<string>(),
null));
result.OverallResult.ShouldBe(BuildResultCode.Success);
_logger.AllBuildEvents.OfType<ProjectStartedEventArgs>()
.Count()
.ShouldBe(3);
_logger.AllBuildEvents.OfType<ProjectEvaluationFinishedEventArgs>()
.Count()
.ShouldBe(3);
_logger.AllBuildEvents.OfType<ProjectEvaluationStartedEventArgs>()
.Count()
.ShouldBe(3);
_logger.AllBuildEvents.OfType<ProjectEvaluationFinishedEventArgs>()
.Count()
.ShouldBe(3);
}
[Fact]
public void GraphBuildOptionsDefaults()
{
var options = new GraphBuildOptions();
options.Build.ShouldBeTrue();
}
/// <summary>
/// A simple successful graph build.
/// </summary>
[Fact]
[ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")]
public void SimpleGraphBuild()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
GraphBuildRequestData data = GetGraphBuildRequestData(contents);
GraphBuildResult result = _buildManager.Build(_parameters, data);
result.OverallResult.ShouldBe(BuildResultCode.Success);
_logger.AssertLogContains("[success]");
_logger.ProjectStartedEvents.Count.ShouldBe(1);
ProjectEvaluationFinishedEventArgs evalFinishedEvent = _logger.EvaluationFinishedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(evalFinishedEvent.Properties);
properties.TryGetValue("InitialProperty1", out string propertyValue).ShouldBeTrue();
propertyValue.ShouldBe("InitialProperty1", StringCompareShould.IgnoreCase);
properties.TryGetValue("InitialProperty2", out propertyValue).ShouldBeTrue();
propertyValue.ShouldBe("InitialProperty2", StringCompareShould.IgnoreCase);
properties.TryGetValue("InitialProperty3", out propertyValue).ShouldBeTrue();
propertyValue.ShouldBe("InitialProperty3", StringCompareShould.IgnoreCase);
}
#if FEATURE_CODETASKFACTORY
/// <summary>
/// Verify that the environment between two msbuild calls to the same project are stored
/// so that on the next call we get access to them
/// </summary>
[Fact]
public void VerifyEnvironmentSavedBetweenCalls()
{
string contents1 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<UsingTask TaskName='SetEnvv' TaskFactory='CodeTaskFactory' AssemblyFile='$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll' >
<Task>
<Code Language='cs'>
System.Environment.SetEnvironmentVariable(""MOO"", ""When the dawn comes, tonight will be a memory too"");
</Code>
</Task>
</UsingTask>
<Target Name='SetEnv'>
<SetEnvv/>
</Target>
<Target Name='Message1'>
<Exec Command='echo What does a cat say : " + (NativeMethodsShared.IsWindows ? "%MOO%" : "$MOO") + @"' />
</Target>
</Project>
");
using ProjectFromString projectFromString = new(contents1, null, null, _projectCollection);
Project project = projectFromString.Project;
project.FullPath = _env.CreateFile(".proj").Path;
project.Save();
string contents2 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='Build' >
<MSBuild Targets='SetEnv' Projects='" + project.FullPath + "'/>" +
"<MSBuild Targets='Message1' Projects='" + project.FullPath + "'/>" +
@"</Target>
</Project>
");
ProjectInstance instance = CreateProjectInstance(contents2, null, _projectCollection, true);
BuildRequestData data = new BuildRequestData(instance, new[] { "Build" }, _projectCollection.HostServices);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("What does a cat say : When the dawn comes, tonight will be a memory too");
}
#endif
/// <summary>
/// Verify if idle nodes are shutdown when BuildManager.ShutdownAllNodes is evoked.
/// The final number of nodes has to be less or equal the number of nodes already in
/// the system before this method was called.
/// </summary>
#if RUNTIME_TYPE_NETCORE
[Theory(Skip = "https://github.com/dotnet/msbuild/issues/1975")]
#else
[Theory(Skip = "https://github.com/dotnet/msbuild/issues/2057")]
#endif
[InlineData(8, false)]
public void ShutdownNodesAfterParallelBuild(int numberOfParallelProjectsToBuild, bool enbaleDebugComm)
{
// This test has previously been failing silently. With the addition of TestEnvironment the
// failure is now noticed (worker node is crashing with "Pipe is broken" exception. See #2057:
// https://github.com/dotnet/msbuild/issues/2057
_env.ClearTestInvariants();
// Communications debug log enabled, picked up by TestEnvironment
if (enbaleDebugComm)
{
_env.SetEnvironmentVariable("MSBUILDDEBUGCOMM", "1");
}
using var projectCollection = new ProjectCollection();
// Get number of MSBuild processes currently instantiated
int numberProcsOriginally = (new List<Process>(Process.GetProcessesByName("MSBuild"))).Count;
_output.WriteLine($"numberProcsOriginally = {numberProcsOriginally}");
// Generate a theoretically unique directory to put our dummy projects in.
string shutdownProjectDirectory = Path.Combine(Path.GetTempPath(), String.Format(CultureInfo.InvariantCulture, "VSNodeShutdown_{0}_UnitTest", Process.GetCurrentProcess().Id));
// Create the dummy projects we'll be "building" as our excuse to connect to and shut down
// all the nodes.
ProjectInstance rootProject = GenerateDummyProjects(shutdownProjectDirectory, numberOfParallelProjectsToBuild, projectCollection);
// Build the projects.
var buildParameters = new BuildParameters(projectCollection)
{
OnlyLogCriticalEvents = true,
MaxNodeCount = numberOfParallelProjectsToBuild,
EnableNodeReuse = true,
DisableInProcNode = true,
SaveOperatingEnvironment = false,
Loggers = new List<ILogger> { new MockLogger(_output) }
};
// Tell the build manager to not disturb process wide state
var requestData = new BuildRequestData(rootProject, new[] { "Build" }, null);
// Use a separate BuildManager for the node shutdown build, so that we don't have
// to worry about taking dependencies on whether or not the existing ones have already
// disappeared.
using var shutdownManager = new BuildManager("IdleNodeShutdown");
shutdownManager.Build(buildParameters, requestData);
// Number of nodes after the build has to be greater than the original number
int numberProcsAfterBuild = (new List<Process>(Process.GetProcessesByName("MSBuild"))).Count;
_output.WriteLine($"numberProcsAfterBuild = {numberProcsAfterBuild}");
Assert.True(numberProcsOriginally < numberProcsAfterBuild, $"Expected '{numberProcsOriginally}' < '{numberProcsAfterBuild}'");
// Shutdown all nodes
shutdownManager.ShutdownAllNodes();
// Wait until all processes shut down
Thread.Sleep(3000);
// Number of nodes after the shutdown has to be smaller or equal the original number
int numberProcsAfterShutdown = (new List<Process>(Process.GetProcessesByName("MSBuild"))).Count;
_output.WriteLine($"numberProcsAfterShutdown = {numberProcsAfterShutdown}");
Assert.True(numberProcsAfterShutdown <= numberProcsOriginally);
// Delete directory with the dummy project
if (Directory.Exists(shutdownProjectDirectory))
{
FileUtilities.DeleteWithoutTrailingBackslash(shutdownProjectDirectory, true /* recursive delete */);
}
}
/// <summary>
/// A simple successful build, out of process only.
/// </summary>
[Fact]
public void SimpleBuildOutOfProcess()
{
RunOutOfProcBuild(_ => _env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1"));
}
/// <summary>
/// A simple successful build, out of process only. Triggered by setting build parameters' DisableInProcNode to true.
/// </summary>
[Fact]
public void DisableInProcNode()
{
RunOutOfProcBuild(buildParameters => buildParameters.DisableInProcNode = true);
}
/// <summary>
/// Runs a build and verifies it happens out of proc by checking the process ID.
/// </summary>
/// <param name="buildParametersModifier">Runs a test out of proc.</param>
private void RunOutOfProcBuild(Action<BuildParameters> buildParametersModifier)
{
const string contents = @"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<ItemGroup>
<InitialProperty Include='InitialProperty2'/>
<InitialProperty Include='InitialProperty3'/>
</ItemGroup>
<Target Name='test' Returns='@(InitialProperty)'>
<ItemGroup>
<InitialProperty Include='$([System.Diagnostics.Process]::GetCurrentProcess().Id)'/>
</ItemGroup>
<Message Text='[success]'/>
</Target>
</Project>
";
// Need to set this env variable to enable Process.GetCurrentProcess().Id in the project file.
_env.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", "1");
Project project = CreateProject(CleanupFileContents(contents), MSBuildDefaultToolsVersion, _projectCollection, false);
var data = new BuildRequestData(project.CreateProjectInstance(), Array.Empty<string>(), _projectCollection.HostServices);
var customparameters = new BuildParameters { EnableNodeReuse = false, Loggers = new ILogger[] { _logger } };
buildParametersModifier(customparameters);
BuildResult result = _buildManager.Build(customparameters, data);
TargetResult targetresult = result.ResultsByTarget["test"];
ITaskItem[] item = targetresult.Items;
Assert.Equal(BuildResultCode.Success, result.OverallResult);
Assert.Equal(3, item.Length);
Assert.True(int.TryParse(item[2].ItemSpec, out int processId), $"Process ID passed from the 'test' target is not a valid integer (actual is '{item[2].ItemSpec}')");
Assert.NotEqual(Process.GetCurrentProcess().Id, processId); // "Build is expected to be out-of-proc. In fact it was in-proc."
}
[Fact]
public void RequestedResultsAreSatisfied()
{
const string contents = @"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<UnrequestedProperty>IsUnrequested</UnrequestedProperty>
<RequestedProperty>IsRequested</RequestedProperty>
<UpdatedProperty>Stale</UpdatedProperty>
</PropertyGroup>
<ItemGroup>
<AnItem Include='Item1' UnexpectedMetadatum='Unexpected' />
<AnItem Include='Item2'/>
</ItemGroup>
<Target Name='test' Returns='@(ItemWithMetadata)'>
<ItemGroup>
<AnItem Include='$([System.Diagnostics.Process]::GetCurrentProcess().Id)' />
<ItemWithMetadata Metadatum1='m1' Metadatum2='m2' Include='ItemFromTarget' />
</ItemGroup>
<PropertyGroup>
<NewProperty>FunValue</NewProperty>
<UpdatedProperty>Updated</UpdatedProperty>
</PropertyGroup>
<Message Text='[success]'/>
</Target>
<Target Name='other' Returns='@(ItemWithMetadata)' DependsOnTargets='test' />
</Project>
";
// Need to set this env variable to enable Process.GetCurrentProcess().Id in the project file.
_env.SetEnvironmentVariable("MSBUILDENABLEALLPROPERTYFUNCTIONS", "1");
Project project = CreateProject(CleanupFileContents(contents), MSBuildDefaultToolsVersion,
_projectCollection, false);
var requestedProjectState = new RequestedProjectState
{
ItemFilters = new Dictionary<string, List<string>>
{
{"AnItem", null},
{"ItemWithMetadata", new List<string> {"Metadatum1"}},
},
PropertyFilters = new List<string> { "NewProperty", "RequestedProperty" },
};
var data = new BuildRequestData(project.CreateProjectInstance(), new[] { "test", "other" },
_projectCollection.HostServices, BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild, null,
requestedProjectState);
var customparameters = new BuildParameters
{
EnableNodeReuse = false,
Loggers = new ILogger[] { _logger },
DisableInProcNode = true,
};
BuildResult result = _buildManager.Build(customparameters, data);
result.OverallResult.ShouldBe(BuildResultCode.Success);
result.ProjectStateAfterBuild.ShouldNotBeNull();
result.ProjectStateAfterBuild.Properties.ShouldNotContain(p => p.Name == "UnrequestedProperty");
result.ProjectStateAfterBuild.Properties.ShouldContain(p => p.Name == "NewProperty");
result.ProjectStateAfterBuild.GetPropertyValue("NewProperty").ShouldBe("FunValue");
result.ProjectStateAfterBuild.Properties.ShouldContain(p => p.Name == "RequestedProperty");
result.ProjectStateAfterBuild.GetPropertyValue("RequestedProperty").ShouldBe("IsRequested");
result.ProjectStateAfterBuild.Items.Count.ShouldBe(4);
result.ProjectStateAfterBuild.GetItems("ItemWithMetadata").ShouldHaveSingleItem();
result.ProjectStateAfterBuild.GetItems("ItemWithMetadata").First().DirectMetadataCount.ShouldBe(1);
result.ProjectStateAfterBuild.GetItems("ItemWithMetadata").First().GetMetadataValue("Metadatum1")
.ShouldBe("m1");
result.ProjectStateAfterBuild.GetItems("ItemWithMetadata").First().GetMetadataValue("Metadatum2")
.ShouldBeNullOrEmpty();
result.ProjectStateAfterBuild.GetItems("AnItem").Count.ShouldBe(3);
result.ProjectStateAfterBuild.GetItems("AnItem").ShouldContain(p => p.EvaluatedInclude == "Item2");
result.ProjectStateAfterBuild.GetItemsByItemTypeAndEvaluatedInclude("AnItem", "Item1")
.ShouldHaveSingleItem();
result.ProjectStateAfterBuild.GetItemsByItemTypeAndEvaluatedInclude("AnItem", "Item1").First()
.GetMetadataValue("UnexpectedMetadatum").ShouldBe("Unexpected");
}
/// <summary>
/// Make sure when we are doing an in-process build that even if the environment variable MSBUILDFORWARDPROPERTIESFROMCHILD is set that we still
/// get all of the initial properties.
/// </summary>
[Fact]
public void InProcForwardPropertiesFromChild()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
_env.SetEnvironmentVariable("MSBuildForwardPropertiesFromChild", "InitialProperty2;IAMNOTREAL");
BuildRequestData data = GetBuildRequestData(contents);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectEvaluationFinishedEventArgs evalFinishedEvent = _logger.EvaluationFinishedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(evalFinishedEvent.Properties);
Assert.True(properties.TryGetValue("InitialProperty1", out string propertyValue));
Assert.Equal("InitialProperty1", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty2", out propertyValue));
Assert.Equal("InitialProperty2", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty3", out propertyValue));
Assert.Equal("InitialProperty3", propertyValue);
}
/// <summary>
/// Make sure when we are doing an in-process build that even if the environment variable MsBuildForwardAllPropertiesFromChild is set that we still
/// get all of the initial properties.
/// </summary>
[Fact]
public void InProcMsBuildForwardAllPropertiesFromChild()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
_env.SetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild", "InitialProperty2;IAMNOTREAL");
BuildRequestData data = GetBuildRequestData(contents);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectEvaluationFinishedEventArgs evalFinishedEvent = _logger.EvaluationFinishedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(evalFinishedEvent.Properties);
Assert.True(properties.TryGetValue("InitialProperty1", out string propertyValue));
Assert.Equal("InitialProperty1", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty2", out propertyValue));
Assert.Equal("InitialProperty2", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty3", out propertyValue));
Assert.Equal("InitialProperty3", propertyValue);
}
/// <summary>
/// Make sure when we launch a child node and set MsBuildForwardAllPropertiesFromChild that we get all of our properties. This needs to happen
/// even if the msbuildforwardpropertiesfromchild is set to something.
/// </summary>
[Fact]
public void MsBuildForwardAllPropertiesFromChildLaunchChildNode()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
_env.SetEnvironmentVariable("MsBuildForwardAllPropertiesFromChild", "InitialProperty2;IAMNOTREAL");
_env.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "Something");
var project = CreateProject(contents, null, _projectCollection, false);
var data = new BuildRequestData(project.FullPath, new Dictionary<string, string>(), MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectEvaluationFinishedEventArgs evalFinishedEvent = _logger.EvaluationFinishedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(evalFinishedEvent.Properties);
Assert.True(properties.TryGetValue("InitialProperty1", out string propertyValue));
Assert.Equal("InitialProperty1", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty2", out propertyValue));
Assert.Equal("InitialProperty2", propertyValue);
Assert.True(properties.TryGetValue("InitialProperty3", out propertyValue));
Assert.Equal("InitialProperty3", propertyValue);
}
/// <summary>
/// Make sure when if the environment variable MsBuildForwardPropertiesFromChild is set to a value and
/// we launch a child node that we get only that value.
/// </summary>
#if RUNTIME_TYPE_NETCORE
[Fact(Skip = "https://github.com/dotnet/msbuild/issues/1976")]
#else
[Fact]
#endif
public void OutOfProcNodeForwardCertainproperties()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
_env.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "InitialProperty3;IAMNOTREAL");
_env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
// ProjectEvaluationFinished automatically and always forwards all properties, so we'd
// end up with all ~136 properties. Since this test is explicitly testing forwarding specific
// properties on ProjectStarted, turn off the new behavior.
_env.SetEnvironmentVariable("MSBUILDLOGPROPERTIESANDITEMSAFTEREVALUATION", "0");
var project = CreateProject(contents, null, _projectCollection, false);
var data = new BuildRequestData(project.FullPath, new Dictionary<string, string>(),
MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
// We need to recreate build parameters to ensure proper capturing of newly set environment variables
BuildParameters parameters = new BuildParameters
{
ShutdownInProcNodeOnBuildFinish = true,
Loggers = new ILogger[] { _logger },
EnableNodeReuse = false
};
BuildResult result = _buildManager.Build(parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties);
Assert.Single(properties);
Assert.True(properties.TryGetValue("InitialProperty3", out string propertyValue));
Assert.Equal("InitialProperty3", propertyValue);
}
/// <summary>
/// Make sure when if the environment variable MsBuildForwardPropertiesFromChild is set to a value and
/// we launch a child node that we get only that value. Also, make sure that when a project is pulled from the results cache
/// and we have a list of properties to serialize that we do not crash. This is to prevent a regression of 826594
/// </summary>
#if RUNTIME_TYPE_NETCORE
[Fact(Skip = "https://github.com/dotnet/msbuild/issues/1976")]
#else
[Fact]
#endif
public void OutOfProcNodeForwardCertainpropertiesAlsoGetResultsFromCache()
{
string tempProject = _env.CreateFile(".proj").Path;
string contents = CleanupFileContents($@"
<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
<Target Name='Build'>
<MsBuild Projects='{tempProject}' Targets='BuildA'/>
<MsBuild Projects='{tempProject}' Targets='BuildA'/>
</Target>
</Project>
");
string projectContents = CleanupFileContents(@"
<Project ToolsVersion='msbuilddefaulttoolsversion' DefaultTargets='Build' xmlns='msbuildnamespace'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='BuildA'>
<Message Text='BuildA' Importance='High'/>
<Message Text='[success]'/>
</Target>
</Project>
");
File.WriteAllText(tempProject, projectContents);
_env.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "InitialProperty3;IAMNOTREAL");
_env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
_env.SetEnvironmentVariable("MSBUILDLOGPROPERTIESANDITEMSAFTEREVALUATION", "0");
var project = CreateProject(contents, null, _projectCollection, false);
var data = new BuildRequestData(project.FullPath, new Dictionary<string, string>(),
MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
// We need to recreate build parameters to ensure proper capturing of newly set environment variables
BuildParameters parameters = new BuildParameters
{
ShutdownInProcNodeOnBuildFinish = true,
Loggers = new ILogger[] { _logger },
EnableNodeReuse = false
};
BuildResult result = _buildManager.Build(parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Equal(3, _logger.ProjectStartedEvents.Count);
ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[1];
// After conversion to xunit, this test sometimes fails at this assertion.
// Related to shared state that the test touches that's getting handled
// differently in xunit?
Assert.NotNull(projectStartedEvent.Properties);
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties);
Assert.NotNull(properties);
Assert.Single(properties);
Assert.True(properties.TryGetValue("InitialProperty3", out string propertyValue));
Assert.Equal("InitialProperty3", propertyValue);
projectStartedEvent = _logger.ProjectStartedEvents[2];
properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties);
(properties == null || properties.Count == 0).ShouldBeTrue();
}
/// <summary>
/// Make sure when if the environment variable MsBuildForwardPropertiesFromChild is set to empty and
/// we launch a child node that we get no properties
/// </summary>
[Fact]
public void ForwardNoPropertiesLaunchChildNode()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
_env.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", "");
_env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
var project = CreateProject(contents, null, _projectCollection, false);
var data = new BuildRequestData(project.FullPath, new Dictionary<string, string>(),
MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties);
(properties == null || properties.Count == 0).ShouldBeTrue();
}
/// <summary>
/// We want to pass the toolsets from the parent to the child nodes so that any custom toolsets
/// defined on the parent are also available on the child nodes for tasks which use the global project
/// collection
/// </summary>
#if RUNTIME_TYPE_NETCORE
[Fact(Skip = "https://github.com/dotnet/msbuild/issues/933")]
#else
[Fact]
#endif
public void VerifyCustomToolSetsPropagated()
{
string netFrameworkDirectory = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45);
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<UsingTask TaskName='VerifyGlobalProjectCollection' TaskFactory='CodeTaskFactory' AssemblyFile='$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll'>
<Task>
<Using Namespace='Microsoft.Build.Evaluation'/>
<Reference Include='$(MSBuildToolsPath)\Microsoft.Build.dll'/>
<Code Type='Method'>
<![CDATA[
public override bool Execute()
{
bool foundToolSet = false;
foreach(Toolset t in ProjectCollection.GlobalProjectCollection.Toolsets)
{
if(t.ToolsVersion.Equals(""CustomToolSet"", StringComparison.OrdinalIgnoreCase))
{
foundToolSet = true;
break;
}
}
Log.LogMessage(MessageImportance.High, ""foundToolset:"" + foundToolSet.ToString());
return foundToolSet;
}
]]>
</Code>
</Task>
</UsingTask>
<Target Name='Build'>
<VerifyGlobalProjectCollection/>
</Target>
</Project>");
_env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
using var projectCollection = new ProjectCollection();
var newToolSet = new Toolset("CustomToolSet", "c:\\SomePath", projectCollection, null);
projectCollection.AddToolset(newToolSet);
var project = CreateProject(contents, null, projectCollection, false);
var data = new BuildRequestData(project.FullPath, new Dictionary<string, string>(),
MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
var customParameters = new BuildParameters(projectCollection) { Loggers = new ILogger[] { _logger } };
BuildResult result = _buildManager.Build(customParameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
}
/// <summary>
/// When a child node is launched by default we should not send any properties.
/// we launch a child node that we get no properties
/// </summary>
[Fact]
public void ForwardNoPropertiesLaunchChildNodeDefault()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
_env.SetEnvironmentVariable("MsBuildForwardPropertiesFromChild", null);
_env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
var project = CreateProject(contents, null, _projectCollection, false);
var data = new BuildRequestData(project.FullPath, new Dictionary<string, string>(),
MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
Assert.Single(_logger.ProjectStartedEvents);
ProjectStartedEventArgs projectStartedEvent = _logger.ProjectStartedEvents[0];
Dictionary<string, string> properties = ExtractProjectStartedPropertyList(projectStartedEvent.Properties);
(properties == null || properties.Count == 0).ShouldBeTrue();
}
/// <summary>
/// A simple failing build.
/// </summary>
[Fact]
public void SimpleBuildWithFailure()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Error Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertLogContains("[errormessage]");
}
[Fact]
public void DeferredMessageShouldBeLogged()
{
string contents = CleanupFileContents(@"
<Project>
<Target Name='Build'>
<Message Text='[Message]' Importance='high'/>
<Warning Text='[Warn]'/>
</Target>
</Project>
");
MockLogger logger;
const string highMessage = "deferred[High]";
const string normalMessage = "deferred[Normal]";
const string lowMessage = "deferred[Low]";
using (var buildManagerSession = new Helpers.BuildManagerSession(
_env,
deferredMessages: new[]
{
new BuildManager.DeferredBuildMessage(highMessage, MessageImportance.High),
new BuildManager.DeferredBuildMessage(normalMessage, MessageImportance.Normal),
new BuildManager.DeferredBuildMessage(lowMessage, MessageImportance.Low)
}))
{
var result = buildManagerSession.BuildProjectFile(_env.CreateFile("build.proj", contents).Path);
result.OverallResult.ShouldBe(BuildResultCode.Success);
logger = buildManagerSession.Logger;
}
logger.AssertLogContains("[Warn]");
logger.AssertLogContains("[Message]");
logger.AssertLogContains(highMessage);
logger.AssertLogContains(normalMessage);
logger.AssertLogContains(lowMessage);
var deferredMessages = logger.BuildMessageEvents.Where(e => e.Message.StartsWith("deferred")).ToArray();
deferredMessages.Length.ShouldBe(3);
deferredMessages[0].Message.ShouldBe(highMessage);
deferredMessages[0].Importance.ShouldBe(MessageImportance.High);
deferredMessages[1].Message.ShouldBe(normalMessage);
deferredMessages[1].Importance.ShouldBe(MessageImportance.Normal);
deferredMessages[2].Message.ShouldBe(lowMessage);
deferredMessages[2].Importance.ShouldBe(MessageImportance.Low);
logger.BuildStartedEvents.Count.ShouldBe(1);
logger.BuildFinishedEvents.Count.ShouldBe(1);
logger.ProjectStartedEvents.Count.ShouldBe(1);
logger.ProjectFinishedEvents.Count.ShouldBe(1);
logger.TargetStartedEvents.Count.ShouldBe(1);
logger.TargetFinishedEvents.Count.ShouldBe(1);
logger.TaskStartedEvents.Count.ShouldBe(2);
logger.TaskFinishedEvents.Count.ShouldBe(2);
}
/// <summary>
/// A build with a message, error and warning, verify that
/// we only get errors, warnings, and project started and finished when OnlyLogCriticalEvents is true
/// </summary>
[Fact]
public void SimpleBuildWithFailureAndWarningOnlyLogCriticalEventsTrue()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[Message]' Importance='high'/>
<Warning Text='[warn]'/>
<Error Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
_parameters.OnlyLogCriticalEvents = true;
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertLogContains("[errormessage]");
_logger.AssertLogContains("[warn]");
_logger.AssertLogDoesntContain("[message]");
Assert.Single(_logger.BuildStartedEvents);
Assert.Single(_logger.BuildFinishedEvents);
Assert.Single(_logger.ProjectStartedEvents);
Assert.Single(_logger.ProjectFinishedEvents);
Assert.Empty(_logger.TargetStartedEvents);
Assert.Empty(_logger.TargetFinishedEvents);
Assert.Empty(_logger.TaskStartedEvents);
Assert.Empty(_logger.TaskFinishedEvents);
}
/// <summary>
/// A build with a message, error and warning, verify that
/// we only get errors, warnings, messages, task and target messages OnlyLogCriticalEvents is false
/// </summary>
[Fact]
public void SimpleBuildWithFailureAndWarningOnlyLogCriticalEventsFalse()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[message]' Importance='high'/>
<Warning Text='[warn]'/>
<Error Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
_parameters.OnlyLogCriticalEvents = false;
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertLogContains("[errormessage]");
_logger.AssertLogContains("[warn]");
_logger.AssertLogContains("[message]");
Assert.Single(_logger.BuildStartedEvents);
Assert.Single(_logger.BuildFinishedEvents);
Assert.Single(_logger.ProjectStartedEvents);
Assert.Single(_logger.ProjectFinishedEvents);
Assert.Single(_logger.TargetStartedEvents);
Assert.Single(_logger.TargetFinishedEvents);
Assert.Equal(3, _logger.TaskStartedEvents.Count);
Assert.Equal(3, _logger.TaskFinishedEvents.Count);
}
/// <summary>
/// Submitting a synchronous build request before calling BeginBuild yields an InvalidOperationException.
/// </summary>
[Fact]
public void BuildRequestWithoutBegin()
{
BuildRequestData data = new BuildRequestData("foo", new Dictionary<string, string>(), "2.0", Array.Empty<string>(), null);
Should.Throw<InvalidOperationException>(() => _buildManager.BuildRequest(data));
}
/// <summary>
/// Submitting a synchronous graph build request before calling BeginBuild yields an InvalidOperationException.
/// </summary>
[Fact]
public void GraphBuildRequestWithoutBegin()
{
GraphBuildRequestData data = new GraphBuildRequestData("foo", new Dictionary<string, string>(), Array.Empty<string>(), null);
Should.Throw<InvalidOperationException>(() => _buildManager.BuildRequest(data));
}
/// <summary>
/// Pending a build request before calling BeginBuild yields an InvalidOperationException.
/// </summary>
[Fact]
public void PendBuildRequestWithoutBegin()
{
BuildRequestData data = new BuildRequestData("foo", new Dictionary<string, string>(), "2.0", Array.Empty<string>(), null);
Should.Throw<InvalidOperationException>(() => _buildManager.PendBuildRequest(data));
}
/// <summary>
/// Pending a build request before calling BeginBuild yields an InvalidOperationException.
/// </summary>
[Fact]
public void PendGraphBuildRequestWithoutBegin()
{
GraphBuildRequestData data = new GraphBuildRequestData("foo", new Dictionary<string, string>(), Array.Empty<string>(), null);
Should.Throw<InvalidOperationException>(() => _buildManager.PendBuildRequest(data));
}
/// <summary>
/// Calling EndBuild before BeginBuild yields an InvalidOperationException.
/// </summary>
[Fact]
public void EndWithoutBegin()
{
Assert.Throws<InvalidOperationException>(() =>
{
_buildManager.EndBuild();
});
}
[Fact]
public void DisposeAfterUse()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
</Project>
");
var project = CreateProject(contents, null, _projectCollection, false);
var globalProperties = new Dictionary<string, string>();
var targets = Array.Empty<string>();
var brd = new BuildRequestData(project.FullPath, globalProperties, null, targets, new HostServices());
using (var bm = new BuildManager())
{
bm.Build(new BuildParameters(), brd);
}
}
[Fact]
public void DisposeWithoutUse()
{
var bm = new BuildManager();
bm.Dispose();
}
/// <summary>
/// Calling BeginBuild after BeginBuild has already been called yields an InvalidOperationException.
/// </summary>
[Fact]
public void OverlappingBegin()
{
try
{
_buildManager.BeginBuild(new BuildParameters());
Assert.Throws<InvalidOperationException>(() => _buildManager.BeginBuild(new BuildParameters()));
}
finally
{
// Call EndBuild to get us back into a state that approximates reasonable
_buildManager.EndBuild();
}
}
/// <summary>
/// Starting and ending a build without submitting any requests is valid.
/// </summary>
[Fact]
public void EmptyBuild()
{
_buildManager.BeginBuild(_parameters);
_buildManager.EndBuild();
Assert.Equal(0, _logger.ErrorCount);
Assert.Equal(0, _logger.WarningCount);
}
/// <summary>
/// Calling EndBuild after it has already been called yields an InvalidOperationException.
/// </summary>
[Fact]
public void ExtraEnds()
{
_buildManager.BeginBuild(new BuildParameters());
_buildManager.EndBuild();
Assert.Throws<InvalidOperationException>(() => _buildManager.EndBuild());
}
/// <summary>
/// Pending a request after EndBuild has been called yields an InvalidOperationException.
/// </summary>
[Fact]
public void PendBuildRequestAfterEnd()
{
BuildRequestData data = new BuildRequestData("foo", new Dictionary<string, string>(), "2.0", Array.Empty<string>(), null);
_buildManager.BeginBuild(new BuildParameters());
_buildManager.EndBuild();
Should.Throw<InvalidOperationException>(() => _buildManager.PendBuildRequest(data));
}
/// <summary>
/// Pending a request after EndBuild has been called yields an InvalidOperationException.
/// </summary>
[Fact]
public void PendGraphBuildRequestAfterEnd()
{
GraphBuildRequestData data = new GraphBuildRequestData("foo", new Dictionary<string, string>(), Array.Empty<string>(), null);
_buildManager.BeginBuild(new BuildParameters());
_buildManager.EndBuild();
Should.Throw<InvalidOperationException>(() => _buildManager.PendBuildRequest(data));
}
/// <summary>
/// Attempting a synchronous build when a build is in progress yields an InvalidOperationException.
/// </summary>
[Fact]
public void BuildDuringBuild()
{
try
{
BuildRequestData data =
new BuildRequestData("foo", new Dictionary<string, string>(), "2.0", Array.Empty<string>(), null);
_buildManager.BeginBuild(new BuildParameters());
Assert.Throws<InvalidOperationException>(() => { _buildManager.Build(new BuildParameters(), data); });
}
finally
{
// Call EndBuild to get us back into a state that approximates reasonable
_buildManager.EndBuild();
}
}
/// <summary>
/// A sequential build.
/// </summary>
[Fact]
public void EndBuildBlocks()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(1)) + @"'/>
<Message Text='[success 1]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission1 = _buildManager.PendBuildRequest(data);
submission1.ExecuteAsync(null, null);
Assert.False(submission1.IsCompleted);
_buildManager.EndBuild();
Assert.True(submission1.IsCompleted);
_logger.AssertLogContains("[success 1]");
}
/// <summary>
/// Validate that EndBuild can be called during a submission completion callback.
/// </summary>
[Fact]
public void EndBuildCalledWithinSubmissionCallback()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[success 1]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission1 = _buildManager.PendBuildRequest(data);
using var callbackFinished = new AutoResetEvent(false);
submission1.ExecuteAsync(submission =>
{
_buildManager.EndBuild();
callbackFinished.Set();
}, null);
// Wait for the build to finish
Assert.True(callbackFinished.WaitOne(5000)); // "Build is hung."
// EndBuild should now have been called, so invoking it again should give us an invalid operation error.
Assert.Throws<InvalidOperationException>(() => _buildManager.EndBuild());
}
/// <summary>
/// A sequential build.
/// </summary>
[Fact]
public void SequentialBuild()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[success 1]'/>
</Target>
</Project>
");
string contents2 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[success 2]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
BuildRequestData data2 = GetBuildRequestData(contents2);
_buildManager.BeginBuild(_parameters);
BuildResult result = _buildManager.BuildRequest(data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
BuildResult result2 = _buildManager.BuildRequest(data2);
Assert.Equal(BuildResultCode.Success, result2.OverallResult);
_buildManager.EndBuild();
_logger.AssertLogContains("[success 1]");
_logger.AssertLogContains("[success 2]");
}
/// <summary>
/// A sequential build.
/// </summary>
[Fact]
public void OverlappingBuildSubmissions()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(500)) + @"'/>
<Message Text='[success 1]'/>
</Target>
</Project>
");
string contents2 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[success 2]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
BuildRequestData data2 = GetBuildRequestData(contents2);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission1 = _buildManager.PendBuildRequest(data);
submission1.ExecuteAsync(null, null);
BuildResult result2 = _buildManager.BuildRequest(data2);
submission1.WaitHandle.WaitOne();
BuildResult result = submission1.BuildResult;
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Success, result2.OverallResult);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success 1]");
_logger.AssertLogContains("[success 2]");
}
/// <summary>
/// If two overlapping submissions are executed against the same project, with at least one
/// target involved that skipped, make sure that the second one successfully completes
/// (retrieved from the cache).
/// </summary>
[Fact]
public void OverlappingIdenticalBuildSubmissions()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test' Condition='false' />
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
var data2 = new BuildRequestData(data.ProjectInstance, data.TargetNames.ToArray(), data.HostServices);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission1 = _buildManager.PendBuildRequest(data);
BuildSubmission submission2 = _buildManager.PendBuildRequest(data2);
submission2.ExecuteAsync(null, null);
submission1.ExecuteAsync(null, null);
submission1.WaitHandle.WaitOne();
submission2.WaitHandle.WaitOne();
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Success, submission1.BuildResult.OverallResult);
Assert.Equal(BuildResultCode.Success, submission2.BuildResult.OverallResult);
}
/// <summary>
/// With two overlapping submissions, the first of which skips a target and the second
/// of which should not, ensure that the second submission does not, in fact, skip
/// the target. (E.g. despite the fact that the target results are in the cache already
/// as 'skipped', ensure that we retry execution in case conditions have changed.)
/// </summary>
[Fact]
public void OverlappingBuildSubmissions_OnlyOneSucceeds()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='A' DependsOnTargets='SetProp;MaySkip;UnsetProp' />
<Target Name='SetProp'>
<PropertyGroup>
<ShouldSkip>true</ShouldSkip>
</PropertyGroup>
</Target>
<Target Name='MaySkip' Condition='!$(ShouldSkip)'>
<Error Text='[ERROR]' />
</Target>
<Target Name='UnsetProp'>
<PropertyGroup>
<ShouldSkip>false</ShouldSkip>
</PropertyGroup>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, new[] { "A" });
var data2 = new BuildRequestData(data.ProjectInstance, new[] { "MaySkip" }, data.HostServices);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission1 = _buildManager.PendBuildRequest(data);
BuildSubmission submission2 = _buildManager.PendBuildRequest(data2);
submission1.ExecuteAsync(null, null);
submission2.ExecuteAsync(null, null);
submission1.WaitHandle.WaitOne();
submission2.WaitHandle.WaitOne();
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Success, submission1.BuildResult.OverallResult);
Assert.Equal(BuildResultCode.Failure, submission2.BuildResult.OverallResult);
}
/// <summary>
/// Calling EndBuild with an unexecuted submission.
/// </summary>
[Fact]
public void EndWithUnexecutedSubmission()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(20)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
_buildManager.BeginBuild(_parameters);
_buildManager.PendBuildRequest(data);
_buildManager.EndBuild();
}
/// <summary>
/// A canceled build with a submission which is not executed yet.
/// </summary>
[Fact]
public void CancelledBuildWithUnexecutedSubmission()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(20)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
_buildManager.BeginBuild(_parameters);
_buildManager.PendBuildRequest(data);
_buildManager.CancelAllSubmissions();
_buildManager.EndBuild();
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously: needs to be async for xunit's timeout system
/// <summary>
/// A canceled build
/// </summary>
[Fact(Timeout = 20_000)]
public async System.Threading.Tasks.Task CancelledBuild()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
Console.WriteLine("Starting CancelledBuild test that is known to hang.");
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(60)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildParameters parameters = new()
{
ShutdownInProcNodeOnBuildFinish = true,
Loggers = new ILogger[] { _logger, new MockLogger(printEventsToStdout: true) },
EnableNodeReuse = false
};
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
Console.WriteLine("CancelledBuild: beginning build");
_buildManager.BeginBuild(_parameters);
Console.WriteLine("CancelledBuild: build begun");
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
Console.WriteLine("CancelledBuild: pend build returned");
asyncResult.ExecuteAsync(null, null);
Console.WriteLine("CancelledBuild: ExecuteAsync called");
_buildManager.CancelAllSubmissions();
Console.WriteLine("CancelledBuild: submissions cancelled");
// This test intermittently hangs. This timeout is designed to prevent that, turning a hang into a failure.
// Todo: Investigate why this test sometimes hangs.
asyncResult.WaitHandle.WaitOne(TimeSpan.FromSeconds(10)).ShouldBeTrue();
asyncResult.IsCompleted.ShouldBeTrue("Failing to complete by this point indicates a hang.");
BuildResult result = asyncResult.BuildResult;
_buildManager.EndBuild();
Console.WriteLine("CancelledBuild: build ended");
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
_logger.AssertLogDoesntContain("[errormessage]");
}
/// <summary>
/// A canceled build which waits for the task to get started before canceling. Because it is a 2.0 task, we should
/// wait until the task finishes normally (cancellation not supported.)
/// </summary>
[Fact]
public void CancelledBuildWithDelay20()
{
if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null)
{
return;
}
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='2.0'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(5)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
_buildManager.BeginBuild(_parameters);
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
asyncResult.ExecuteAsync(null, null);
Thread.Sleep(500);
_buildManager.CancelAllSubmissions();
asyncResult.WaitHandle.WaitOne();
BuildResult result = asyncResult.BuildResult;
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
_logger.AssertLogDoesntContain("[errormessage]");
}
#if !NO_MSBUILDTASKHOST
// Run this test only if we expect MSBuildTaskHost to have been produced, which requires that MSBuildTaskHost.csproj
// be built with full-framework MSBuild (so that it can target .NET 3.5).
/// <summary>
/// A canceled build which waits for the task to get started before canceling. Because it is a 2.0 task, we should
/// wait until the task finishes normally (cancellation not supported.)
/// </summary>
[Fact]
public void CancelledBuildInTaskHostWithDelay20()
{
if (FrameworkLocationHelper.PathToDotNetFrameworkV20 == null)
{
return;
}
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<UsingTask TaskName='Microsoft.Build.Tasks.Exec' AssemblyName='Microsoft.Build.Tasks.v3.5, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' TaskFactory='TaskHostFactory' Runtime='CLR2' />
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(10)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
_buildManager.BeginBuild(_parameters);
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
asyncResult.ExecuteAsync(null, null);
Thread.Sleep(500);
_buildManager.CancelAllSubmissions();
asyncResult.WaitHandle.WaitOne();
BuildResult result = asyncResult.BuildResult;
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
_logger.AssertLogDoesntContain("[errormessage]");
// Task host should not have exited prematurely
_logger.AssertLogDoesntContain("MSB4217");
// Task host should have been successfully found and run
_logger.AssertLogDoesntContain("MSB4216");
}
#endif
/// <summary>
/// A canceled build which waits for the task to get started before canceling. Because it is a 12.. task, we should
/// cancel the task and exit out after a short period wherein we wait for the task to exit cleanly.
/// </summary>
[Fact]
public void CancelledBuildWithDelay40()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(20)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
_buildManager.BeginBuild(_parameters);
Stopwatch sw = Stopwatch.StartNew();
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
asyncResult.ExecuteAsync(null, null);
Thread.Sleep(500);
_buildManager.CancelAllSubmissions();
asyncResult.WaitHandle.WaitOne();
BuildResult result = asyncResult.BuildResult;
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
_logger.AssertLogDoesntContain("[errormessage]");
// The build should bail out immediately after executing CancelAllSubmissions, build stalling for a longer time
// is very unexpected.
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10));
}
/// <summary>
/// A canceled build which waits for the task to get started before canceling. Because it is a 12.. task, we should
/// cancel the task and exit out after a short period wherein we wait for the task to exit cleanly.
///
/// This test also exercises the possibility of CancelAllSubmissions being executed after EndBuild -
/// which can happen even if they are synchronously executed in expected order - the CancelAllSubmissions is internally
/// asynchronous and hence part of the execution can happen after EndBuild.
/// </summary>
[Fact]
public void CancelledBuildWithDelay40_WithThreatSwap()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(20)) + @"'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
_buildManager.BeginBuild(_parameters);
Stopwatch sw = Stopwatch.StartNew();
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
asyncResult.ExecuteAsync(null, null);
Thread.Sleep(500);
// Simulate the case where CancelAllSubmissions is called after EndBuild or its internal queued task is swapped
// and executed after EndBuild starts execution.
System.Threading.Tasks.Task.Delay(500).ContinueWith(t => _buildManager.CancelAllSubmissions());
_buildManager.EndBuild();
asyncResult.WaitHandle.WaitOne();
BuildResult result = asyncResult.BuildResult;
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
_logger.AssertLogDoesntContain("[errormessage]");
// The build should bail out immediately after executing CancelAllSubmissions, build stalling for a longer time
// is very unexpected.
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10));
}
/// <summary>
/// A canceled build which waits for the task to get started before canceling. Because it is a 12.0 task, we should
/// cancel the task and exit out after a short period wherein we wait for the task to exit cleanly.
/// </summary>
[Fact]
public void CancelledBuildInTaskHostWithDelay40()
{
string contents = CleanupFileContents(@$"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<UsingTask TaskName='Microsoft.Build.Tasks.Exec' AssemblyName='Microsoft.Build.Tasks.Core, Version=msbuildassemblyversion, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' TaskFactory='TaskHostFactory' />
<Target Name='test'>
<Exec Command='{Helpers.GetSleepCommand(TimeSpan.FromSeconds(10))}'/>
<Message Text='[errormessage]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
_buildManager.BeginBuild(_parameters);
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
asyncResult.ExecuteAsync(null, null);
Thread.Sleep(500);
_buildManager.CancelAllSubmissions();
asyncResult.WaitHandle.WaitOne();
BuildResult result = asyncResult.BuildResult;
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
_logger.AssertLogDoesntContain("[errormessage]");
// Task host should not have exited prematurely
_logger.AssertLogDoesntContain("MSB4217");
}
/// <summary>
/// This test verifies that builds of the same project instance in sequence are permitted.
/// </summary>
[Fact]
public void SequentialBuildsOfTheSameProjectAllowed()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='target1'>
<Message Text='text'/>
</Target>
<Target Name='target2'>
<Message Text='text'/>
</Target>
</Project>
");
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildResult result1 = _buildManager.BuildRequest(new BuildRequestData(instance, new[] { "target1" }));
BuildResult result2 = _buildManager.BuildRequest(new BuildRequestData(instance, new[] { "target2" }));
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Success, result1.OverallResult);
Assert.True(result1.HasResultsForTarget("target1")); // "Results for target1 missing"
Assert.Equal(BuildResultCode.Success, result2.OverallResult);
Assert.True(result2.HasResultsForTarget("target2")); // "Results for target2 missing"
}
/// <summary>
/// This test verifies that overlapping builds of the same project are allowed.
/// </summary>
[Fact]
public void OverlappingBuildsOfTheSameProjectDifferentTargetsAreAllowed()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='target1'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(3)) + @"'/>
<Message Text='text'/>
</Target>
<Target Name='target2'>
<Message Text='text'/>
</Target>
</Project>
");
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission = _buildManager.PendBuildRequest(new BuildRequestData(instance, new[] { "target1" }));
submission.ExecuteAsync(null, null);
BuildResult result2 = _buildManager.BuildRequest(new BuildRequestData(project.CreateProjectInstance(), new[] { "target2" }));
submission.WaitHandle.WaitOne();
var result1 = submission.BuildResult;
Assert.Equal(BuildResultCode.Success, result1.OverallResult);
Assert.True(result1.HasResultsForTarget("target1")); // "Results for target1 missing"
Assert.Equal(BuildResultCode.Success, result2.OverallResult);
Assert.True(result2.HasResultsForTarget("target2")); // "Results for target2 missing"
_buildManager.EndBuild();
}
/// <summary>
/// This test verifies that overlapping builds of the same project are allowed.
/// </summary>
[Fact]
public void OverlappingBuildsOfTheSameProjectSameTargetsAreAllowed()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='target1'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(3)) + @"'/>
<Message Text='text'/>
</Target>
<Target Name='target2'>
<Message Text='text'/>
</Target>
</Project>
");
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildSubmission submission = _buildManager.PendBuildRequest(new BuildRequestData(instance, new[] { "target1" }));
submission.ExecuteAsync(null, null);
BuildResult result2 = _buildManager.BuildRequest(new BuildRequestData(project.CreateProjectInstance(), new[] { "target1" }));
submission.WaitHandle.WaitOne();
var result1 = submission.BuildResult;
Assert.Equal(BuildResultCode.Success, result1.OverallResult);
Assert.True(result1.HasResultsForTarget("target1")); // "Results for target1 missing"
Assert.Equal(BuildResultCode.Success, result2.OverallResult);
Assert.True(result2.HasResultsForTarget("target1")); // "Results for target1 (second call) missing"
_buildManager.EndBuild();
}
/// <summary>
/// This test verifies that the out-of-proc node won't lock the directory containing the target project.
/// </summary>
#if RUNTIME_TYPE_NETCORE
[Fact(Skip = "https://github.com/dotnet/msbuild/issues/933")]
#else
[Fact]
#endif
public void OutOfProcNodeDoesntLockWorkingDirectory()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
var projectFolder = _env.CreateFolder();
string projectFile = _env.CreateFile(projectFolder, ".proj").Path;
File.WriteAllText(projectFile, contents);
_env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
var data = new BuildRequestData(projectFile, new Dictionary<string, string>(), MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
_buildManager.Build(_parameters, data);
}
/// <summary>
/// Retrieving a ProjectInstance from the BuildManager stores it in the cache
/// </summary>
[Fact]
public void ProjectInstanceStoredInCache()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Message Text='text'/>
</Target>
</Project>
");
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
ProjectInstance instance2 = _buildManager.GetProjectInstanceForBuild(project);
Assert.Equal(instance, instance2); // "Instances don't match"
}
/// <summary>
/// Retrieving a ProjectInstance from the BuildManager after a build.
/// </summary>
[Fact]
public void ProjectInstanceRetrievedAfterBuildMatchesSourceProject()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<PropertyGroup>
<Foo>bar</Foo>
</PropertyGroup>
<Message Text='[success]'/>
</Target>
</Project>
");
IBuildComponentHost host = _buildManager;
host.GetComponent(BuildComponentType.ConfigCache);
BuildRequestData data = GetBuildRequestData(contents);
_buildManager.Build(_parameters, data);
Project project = _projectCollection.LoadProject(data.ProjectFullPath);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
Assert.Equal("bar", instance.GetPropertyValue("Foo"));
}
/// <summary>
/// Retrieving a ProjectInstance after resetting the cache clears the instances.
/// </summary>
[Fact]
public void ResetCacheClearsInstances()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<PropertyGroup>
<Foo>bar</Foo>
</PropertyGroup>
<Message Text='[success]'/>
</Target>
</Project>
");
IBuildComponentHost host = _buildManager;
host.GetComponent(BuildComponentType.ConfigCache);
BuildRequestData data = GetBuildRequestData(contents);
_buildManager.Build(_parameters, data);
Project project = _projectCollection.LoadProject(data.ProjectFullPath);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
Assert.Equal("bar", instance.GetPropertyValue("Foo"));
_buildManager.BeginBuild(_parameters);
_buildManager.EndBuild();
instance = _buildManager.GetProjectInstanceForBuild(project);
Assert.Null(instance.GetProperty("Foo"));
}
/// <summary>
/// Retrieving a ProjectInstance after another build without resetting the cache keeps the existing instance
/// </summary>
[Fact]
public void DisablingCacheResetKeepsInstance()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<PropertyGroup>
<Foo>bar</Foo>
</PropertyGroup>
<Message Text='[success]'/>
</Target>
</Project>
");
IBuildComponentHost host = _buildManager;
host.GetComponent(BuildComponentType.ConfigCache);
BuildRequestData data = GetBuildRequestData(contents);
_buildManager.Build(_parameters, data);
Project project = _projectCollection.LoadProject(data.ProjectFullPath);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
Assert.Equal("bar", instance.GetPropertyValue("Foo"));
_logger.ClearLog();
_parameters.ResetCaches = false;
_buildManager.BeginBuild(_parameters);
_buildManager.BuildRequest(data);
_buildManager.EndBuild();
// We should have built the same instance, with the same results, so the target will be skipped.
string skippedMessage = ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteSuccess", "test");
Assert.Contains(skippedMessage, _logger.FullLog);
ProjectInstance instance2 = _buildManager.GetProjectInstanceForBuild(project);
Assert.Equal(instance, instance2); // "Instances are not the same"
}
/// <summary>
/// Retrieving a ProjectInstance after another build without resetting the cache keeps the existing instance
/// </summary>
[Fact]
public void GhostProjectRootElementCache()
{
string p2pProject = _env.CreateFile(".Project2.proj").Path;
string contents1 = CleanupFileContents($@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='test'>
<Msbuild Projects='{p2pProject}'>
<Output TaskParameter='TargetOutputs' ItemName='P2pOutput'/>
</Msbuild>
<Message Text='Value:@(P2pOutput)' Importance='high'/>
</Target>
</Project>
");
string contents2 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<Bar Condition=""'$(Bar)' == ''"">Baz</Bar>
</PropertyGroup>
<Target Name='test' Returns='$(Bar)'/>
</Project>
");
IBuildComponentHost host = _buildManager;
host.GetComponent(BuildComponentType.ConfigCache);
// Create Project 1
ProjectInstance projectInstance = CreateProjectInstance(contents1, null, _projectCollection, false);
var data = new BuildRequestData(projectInstance, Array.Empty<string>());
_logger.ClearLog();
// Write the second project to disk and load it into its own project collection
using var projectCollection2 = new ProjectCollection();
File.WriteAllText(p2pProject, contents2);
Project project2 = projectCollection2.LoadProject(p2pProject);
_parameters.ResetCaches = false;
// Build the first project to make sure we get the expected default values out for the p2p call.
_parameters.ProjectRootElementCache = _projectCollection.ProjectRootElementCache;
_buildManager.BeginBuild(_parameters);
_buildManager.BuildRequest(data);
_buildManager.EndBuild();
_logger.AssertLogContains("Value:Baz");
_logger.ClearLog();
// Modify the property in the second project and save it to disk.
project2.SetProperty("Bar", "FOO");
project2.Save();
// Create a new build.
ProjectInstance projectInstance2 = CreateProjectInstance(contents1, null, _projectCollection, false);
var data2 = new BuildRequestData(projectInstance2, Array.Empty<string>());
// Build again.
_parameters.ResetCaches = false;
_buildManager.BeginBuild(_parameters);
_buildManager.BuildRequest(data2);
_buildManager.EndBuild();
_logger.AssertLogContains("Value:FOO");
}
/// <summary>
/// Verifies that explicitly loaded projects' imports are all marked as also explicitly loaded.
/// </summary>
[Fact]
public void VerifyImportedProjectRootElementsInheritExplicitLoadFlag()
{
string contents1 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Import Project='{0}' />
<Target Name='test' />
</Project>
");
string contents2 = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<ImportedProperty>ImportedValue</ImportedProperty>
</PropertyGroup>
</Project>
");
string importedProjectPath = _env.CreateFile(".proj").Path;
string rootProjectPath = _env.CreateFile(".proj").Path;
File.WriteAllText(importedProjectPath, contents2);
File.WriteAllText(rootProjectPath, String.Format(CultureInfo.InvariantCulture, contents1, importedProjectPath));
using var projectCollection = new ProjectCollection();
// Run a simple build just to prove that nothing is left in the cache.
var data = new BuildRequestData(rootProjectPath, ReadOnlyEmptyDictionary<string, string>.Instance, null, new[] { "test" }, null);
_parameters.ResetCaches = true;
_parameters.ProjectRootElementCache = projectCollection.ProjectRootElementCache;
_buildManager.BeginBuild(_parameters);
_buildManager.BuildRequest(data);
_buildManager.EndBuild();
_buildManager.ResetCaches();
// The semantic of TryOpen is to only retrieve the PRE if it is already in the weak cache.
Assert.Null(ProjectRootElement.TryOpen(rootProjectPath, projectCollection)); // "The built project shouldn't be in the cache anymore."
Assert.Null(ProjectRootElement.TryOpen(importedProjectPath, projectCollection)); // "The built project's import shouldn't be in the cache anymore."
Project project = projectCollection.LoadProject(rootProjectPath);
ProjectRootElement preRoot, preImported;
Assert.NotNull(preRoot = ProjectRootElement.TryOpen(rootProjectPath, projectCollection)); // "The root project file should be in the weak cache."
Assert.NotNull(preImported = ProjectRootElement.TryOpen(importedProjectPath, projectCollection)); // "The imported project file should be in the weak cache."
Assert.True(preRoot.IsExplicitlyLoaded);
Assert.True(preImported.IsExplicitlyLoaded);
// Run a simple build just to prove that it doesn't impact what is in the cache.
data = new BuildRequestData(rootProjectPath, ReadOnlyEmptyDictionary<string, string>.Instance, null, new[] { "test" }, null);
_parameters.ResetCaches = true;
_parameters.ProjectRootElementCache = projectCollection.ProjectRootElementCache;
_buildManager.BeginBuild(_parameters);
_buildManager.BuildRequest(data);
_buildManager.EndBuild();
_buildManager.ResetCaches();
// Now make sure they are still in the weak cache. Since they were loaded explicitly before the build, the build shouldn't have unloaded them from the cache.
Assert.Same(preRoot, ProjectRootElement.TryOpen(rootProjectPath, projectCollection)); // "The root project file should be in the weak cache after a build."
Assert.Same(preImported, ProjectRootElement.TryOpen(importedProjectPath, projectCollection)); // "The imported project file should be in the weak cache after a build."
Assert.True(preRoot.IsExplicitlyLoaded);
Assert.True(preImported.IsExplicitlyLoaded);
projectCollection.UnloadProject(project);
projectCollection.UnloadAllProjects();
Assert.Null(ProjectRootElement.TryOpen(rootProjectPath, projectCollection)); // "The unloaded project shouldn't be in the cache anymore."
Assert.Null(ProjectRootElement.TryOpen(importedProjectPath, projectCollection)); // "The unloaded project's import shouldn't be in the cache anymore."
}
/// <summary>
/// Verify that using a second BuildManager doesn't cause the system to crash.
/// </summary>
[Fact]
public void Regress251333()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
// First a normal build
BuildRequestData data = GetBuildRequestData(contents);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
// Now a build using a different build manager.
using (var newBuildManager = new BuildManager())
{
GetBuildRequestData(contents);
BuildResult result2 = newBuildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result2.OverallResult);
}
}
/// <summary>
/// Verify that disabling the in-proc node doesn't cause projects which don't require it to fail.
/// </summary>
[Fact]
public void Regress239661()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
string fileName = _env.CreateFile(".proj").Path;
File.WriteAllText(fileName, contents);
var data = new BuildRequestData(fileName, _projectCollection.GlobalProperties, MSBuildDefaultToolsVersion, Array.Empty<string>(), null);
_parameters.DisableInProcNode = true;
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
}
/// <summary>
/// Verify that disabling the in-proc node when a project requires it will cause the project to build on the out of proc node.
/// </summary>
[Fact]
public void ExplicitInprocAffinityGetsOverruledByDisableInprocNode()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<InitialProperty1>InitialProperty1</InitialProperty1>
<InitialProperty2>InitialProperty2</InitialProperty2>
<InitialProperty3>InitialProperty3</InitialProperty3>
</PropertyGroup>
<Target Name='test'>
<Message Text='[success]'/>
</Target>
</Project>
");
BuildRequestData data = GetBuildRequestData(contents);
_env.CreateFile(data.ProjectFullPath, data.ProjectInstance.ToProjectRootElement().RawXml);
_parameters.DisableInProcNode = true;
// Require that this project build on the in-proc node, which will not be available.
data.HostServices.SetNodeAffinity(data.ProjectFullPath, NodeAffinity.InProc);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_logger.AssertLogContains("[success]");
_logger.AssertLogDoesntContain("MSB4223");
}
/// <summary>
/// Ensures that properties and items are transferred to the out-of-proc node when an instance is used to start the build.
/// </summary>
[Fact]
public void ProjectInstanceTransfersToOOPNode()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<DeleteMe>deleteme</DeleteMe>
<Unmodified>unmodified</Unmodified>
<VirtualProp>original</VirtualProp>
</PropertyGroup>
<ItemGroup>
<Foo Include='foo'/>
<Foo2 Include='foo2'/>
</ItemGroup>
<Target Name='test'>
<Message Text='[$(DeleteMe)]'/>
<Message Text='[$(Unmodified)]'/>
<Message Text='[$(VirtualProp)]'/>
<Message Text='[$(NewProp)]'/>
<Message Text='[@(Foo)]'/>
<Message Text='[@(Foo2)]'/>
<Message Text='[@(Baz)]'/>
</Target>
</Project>
");
string fileName = _env.CreateFile(".proj").Path;
File.WriteAllText(fileName, contents);
var project = new Project(fileName);
ProjectInstance instance = project.CreateProjectInstance();
instance.RemoveProperty("DeleteMe");
instance.SetProperty("VirtualProp", "overridden");
instance.SetProperty("NewProp", "new");
instance.AddItem("Baz", "baz");
instance.AddItem("Foo2", "foo21");
foreach (var item in instance.Items)
{
if (item.EvaluatedInclude == "foo")
{
instance.RemoveItem(item);
break;
}
}
var data = new BuildRequestData(instance, Array.Empty<string>());
// Force this to build out-of-proc
_parameters.DisableInProcNode = true;
_buildManager.Build(_parameters, data);
_logger.AssertLogDoesntContain("[deleteme]");
_logger.AssertLogContains("[overridden]");
_logger.AssertLogContains("[unmodified]");
_logger.AssertLogContains("[new]");
_logger.AssertLogDoesntContain("[foo]");
_logger.AssertLogContains("[foo2;foo21]");
_logger.AssertLogContains("[baz]");
}
/// <summary>
/// Ensures that a limited set of properties are transferred from a project instance to an OOP node.
/// </summary>
[Fact]
public void ProjectInstanceLimitedTransferToOOPNode()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<Unmodified>unmodified</Unmodified>
<VirtualProp>original</VirtualProp>
</PropertyGroup>
<Target Name='test'>
<Message Text='[$(Unmodified)]'/>
<Message Text='[$(VirtualProp)]'/>
</Target>
</Project>
");
string fileName = _env.CreateFile(".proj").Path;
File.WriteAllText(fileName, contents);
Project project = new Project(fileName);
ProjectInstance instance = project.CreateProjectInstance();
instance.SetProperty("VirtualProp", "overridden");
instance.SetProperty("Unmodified", "changed");
var data = new BuildRequestData(instance, Array.Empty<string>(), null, BuildRequestDataFlags.None, new string[] { "VirtualProp" });
// Force this to build out-of-proc
_parameters.DisableInProcNode = true;
_buildManager.Build(_parameters, data);
_logger.AssertLogContains("[overridden]");
_logger.AssertLogContains("[unmodified]");
_logger.AssertLogDoesntContain("[changed]");
}
/// <summary>
/// Tests that cache files are created as expected and their lifetime is controlled appropriately.
/// </summary>
[Fact]
[Trait("Category", "netcore-osx-failing")]
[Trait("Category", "netcore-linux-failing")]
public void CacheLifetime()
{
FileUtilities.ClearCacheDirectory();
_env.SetEnvironmentVariable("MSBUILDDEBUGFORCECACHING", "1");
string outerBuildCacheDirectory;
// Do a build with one build manager.
using (var outerBuildManager = new BuildManager())
{
outerBuildCacheDirectory = BuildAndCheckCache(outerBuildManager, Array.Empty<string>());
// Do another build with a second build manager while the first still exists. Since both BuildManagers
// share a process-wide cache directory, we want to verify that they don't stomp on each other, either
// by accidentally sharing results, or by clearing them away.
string innerBuildCacheDirectory;
using (var innerBuildManager = new BuildManager())
{
innerBuildCacheDirectory = BuildAndCheckCache(innerBuildManager, new[] { outerBuildCacheDirectory });
// Force the cache for this build manager (and only this build manager) to be cleared. It should leave
// behind the results from the other one.
innerBuildManager.ResetCaches();
}
Assert.False(Directory.Exists(innerBuildCacheDirectory)); // "Inner build cache directory still exists after inner build manager was disposed."
Assert.True(Directory.Exists(outerBuildCacheDirectory)); // "Outer build cache directory doesn't exist after inner build manager was disposed."
// Force the cache for this build manager to be cleared.
outerBuildManager.ResetCaches();
}
Assert.False(Directory.Exists(outerBuildCacheDirectory)); // "Outer build cache directory still exists after outer build manager was disposed."
}
/// <summary>
/// If there's a P2P that otherwise succeeds, but has an AfterTarget that errors out, the
/// overall build result -- and thus the return value of the MSBuild task -- should reflect
/// that failure.
/// </summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FailedAfterTargetInP2PShouldCauseOverallBuildFailure(bool disableInProcNode)
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`" + projB + @"` />
<Warning Text=`We shouldn't reach here.` />
</Target>
</Project>
";
const string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`Build` />
</Target>
<Target Name=`Error` AfterTargets=`Build`>
<Error Text=`Error!` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
_parameters.DisableInProcNode = disableInProcNode;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null, new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertNoWarnings();
_buildManager.EndBuild();
}
/// <summary>
/// If there's a P2P that otherwise succeeds, but has an AfterTarget that errors out, the
/// overall build result -- and thus the return value of the MSBuild task -- should reflect
/// that failure. Specifically tests where there are multiple entrypoint targets with
/// AfterTargets, only one of which fails.
/// </summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FailedAfterTargetInP2PShouldCauseOverallBuildFailure_MultipleEntrypoints(bool disableInProcNode)
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`" + projB + @"` Targets=`Build;Build2` />
<Warning Text=`We shouldn't reach here.` />
</Target>
</Project>
";
const string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`[Build]` />
</Target>
<Target Name=`Build2`>
<Message Text=`[Build2]` />
</Target>
<Target Name=`AT1` AfterTargets=`Build`>
<Message Text=`[AT1]` />
</Target>
<Target Name=`AT2` AfterTargets=`Build2`>
<Message Text=`[AT2]` />
</Target>
<Target Name=`Error` AfterTargets=`Build2`>
<Error Text=`Error!` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
_parameters.DisableInProcNode = disableInProcNode;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null, new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertNoWarnings();
_logger.AssertLogContains("[Build]");
_logger.AssertLogContains("[Build2]");
_logger.AssertLogContains("[AT1]");
_logger.AssertLogContains("[AT2]");
_buildManager.EndBuild();
}
/// <summary>
/// If there's a P2P that otherwise succeeds, but has an AfterTarget that errors out, the
/// overall build result -- and thus the return value of the MSBuild task -- should reflect
/// that failure. This should also be true if the AfterTarget is an AfterTarget of the
/// entrypoint target.
/// </summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FailedNestedAfterTargetInP2PShouldCauseOverallBuildFailure(bool disableInProcNode)
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`" + projB + @"` />
<Warning Text=`We shouldn't reach here.` />
</Target>
</Project>
";
const string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`Build` />
</Target>
<Target Name=`Target1` AfterTargets=`Build`>
<Message Text=`Target1` />
</Target>
<Target Name=`Error` AfterTargets=`Target1`>
<Error Text=`Error!` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
_parameters.DisableInProcNode = disableInProcNode;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null, new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertNoWarnings();
_buildManager.EndBuild();
}
/// <summary>
/// If a project is called into twice, with two different entrypoint targets that
/// depend on non-overlapping sets of targets, and the first fails, the second
/// should not inherit that failure if all the targets it calls succeed.
/// </summary>
[Fact]
public void NonOverlappingEnusingTrypointTargetsShouldNotInfluenceEachOthersResults()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`The next MSBuild call should FAIL, but the build will continue.` />
<MSBuild Projects=`" + projB + @"` Targets=`Build` ContinueOnError=`true` />
<Message Text=`The next MSBuild call should SUCCEED without error.` />
<MSBuild Projects=`" + projB + @"` Targets=`GetTargetPath` />
</Target>
</Project>
";
const string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Error Text=`Forced error in Build` />
</Target>
<Target Name=`GetTargetPath`>
<Message Text=`Success` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null, new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Success, result.OverallResult);
Assert.Equal(1, _logger.ErrorCount);
_buildManager.EndBuild();
}
/// <summary>
/// In a situation where we have two requests calling into the same project, with different entry point
/// targets, one of which depends on "A;B", the other of which depends on "B", which has a dependency of
/// its own on "A", that we still properly build.
/// </summary>
#if RUNTIME_TYPE_NETCORE
[Fact(Skip = "https://github.com/dotnet/msbuild/issues/933")]
#else
[Fact]
#endif
public void Regress473114()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
var projC = _env.CreateFile(".proj").Path;
var projD = _env.CreateFile(".proj").Path;
string contentsA = @"<?xml version='1.0' encoding='utf-8'?>
<Project ToolsVersion='4.0' DefaultTargets='Build'>
<ItemGroup>
<ProjectReference Include='" + projD + @"' />
<ProjectReference Include='" + projC + @"' />
<ProjectReference Include='" + projB + @"' />
</ItemGroup>
<Target Name='Build'>
<MSBuild Projects='@(ProjectReference)' BuildInParallel='true' />
</Target>
</Project>
";
string contentsB = @"<?xml version='1.0' encoding='utf-8'?>
<Project ToolsVersion='4.0' DefaultTargets='CallsGenerateImpLib'>
<Target Name='CallsGenerateImpLib'>
<MSBuild Projects='" + projC + @"' Targets='GenerateImpLib' BuildInParallel='true' />
</Target>
</Project>
";
string contentsC = @"<?xml version='1.0' encoding='utf-8'?>
<Project ToolsVersion='4.0' DefaultTargets='Default'>
<Target Name='Default' DependsOnTargets='ResolveReferences;BuildCompile'>
<Message Text='Executed Default' />
</Target>
<Target Name='GenerateImpLib' DependsOnTargets='BuildCompile'>
<Message Text='Executed GenerateImpLib' />
</Target>
<Target Name='ResolveReferences'>
<MSBuild Projects='" + projD + @"' Targets='BuildSlower' BuildInParallel='true' />
</Target>
<Target Name='BuildCompile' DependsOnTargets='ResolveReferences'>
<Message Text='Executed BuildCompile' />
</Target>
</Project>
";
string contentsD = @"<?xml version='1.0' encoding='utf-8'?>
<Project ToolsVersion='4.0' DefaultTargets='Build'>
<Target Name='Build'>
<Message Text='In d.proj' />
</Target>
<Target Name='BuildSlower'>
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(500)) + @"' />
</Target>
</Project>
";
File.WriteAllText(projA, contentsA);
File.WriteAllText(projB, contentsB);
File.WriteAllText(projC, contentsC);
File.WriteAllText(projD, contentsD);
_parameters.MaxNodeCount = 3;
_parameters.EnableNodeReuse = false;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), "4.0", new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Success, result.OverallResult);
_buildManager.EndBuild();
}
/// <summary>
/// If two requests are made for the same project, and they call in with
/// just the right timing such that:
/// - request 1 builds for a while, reaches a P2P, and blocks
/// - request 2 starts building, skips for a while, reaches the above P2P, and
/// blocks waiting for request 1's results
/// - request 1 resumes building, errors, and exits
/// - request 2 resumes building
///
/// Then request 2 should end up exiting in the same fashion.
///
/// This simple test verifies that if there are two error targets in a row, the
/// second request will bail out where the first request did, as though it had
/// executed the target, rather than skipping and continuing.
/// </summary>
[Fact]
public void VerifyMultipleRequestForSameProjectWithErrors_Simple()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
var projC = _env.CreateFile(".proj").Path;
string contentsA = $@"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`{projB};{projC};{projB}` BuildInParallel=`true` />
</Target>
</Project>";
string contentsB = $@"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build` DependsOnTargets=`CallMSBuild;Error1;Error2` />
<Target Name=`CallMSBuild`>
<MSBuild Projects=`{projC}` Targets=`Sleep` BuildInParallel=`true` />
</Target>
<Target Name=`Error1`>
<Error Text=`Error 1` />
</Target>
<Target Name=`Error2`>
<Error Text=`Error 2` />
</Target>
</Project>";
string contentsC = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`foo` />
</Target>
<Target Name=`Sleep`>
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(500)) + @"` />
</Target>
</Project>";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
File.WriteAllText(projC, CleanupFileContents(contentsC));
_parameters.MaxNodeCount = 2;
_parameters.EnableNodeReuse = false;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null,
new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
// We should never get to Error2, because it's supposed to execute after Error1, which failed.
_logger.AssertLogDoesntContain("Error 2");
// We should, however, end up skipping Error1 on the second call to B.
string skippedMessage =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteFailure", "Error1");
_logger.AssertLogContains(skippedMessage);
_buildManager.EndBuild();
}
/// <summary>
/// If two requests are made for the same project, and they call in with
/// just the right timing such that:
/// - request 1 builds for a while, reaches a P2P, and blocks
/// - request 2 starts building, skips for a while, reaches the above P2P, and
/// blocks waiting for request 1's results
/// - request 1 resumes building, errors, and exits
/// - request 2 resumes building
///
/// Then request 2 should end up exiting in the same fashion.
///
/// This simple test verifies that if there are two error targets in a row, and the
/// first has a chain of OnError targets, the OnError targets will all execute as
/// expected in the first request, but be skipped by the second (since if it's "skipping
/// unsuccessful", it can assume that all other OnError targets have also already been run)
/// </summary>
[Fact]
public void VerifyMultipleRequestForSameProjectWithErrors_OnErrorChain()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
var projC = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`" + String.Join(";", projB, projC, projB) + @"` BuildInParallel=`true` />
</Target>
</Project>
";
string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build` DependsOnTargets=`CallMSBuild;Error1;Error2` />
<Target Name=`CallMSBuild`>
<MSBuild Projects=`" + projC + @"` Targets=`Sleep` BuildInParallel=`true` />
</Target>
<Target Name=`Error1`>
<Error Text=`Error 1` />
<OnError ExecuteTargets=`Target2;Target3` />
</Target>
<Target Name=`Error2`>
<Error Text=`Error 2` />
</Target>
<Target Name=`Target2`>
<Error Text=`Error in Target2` />
<OnError ExecuteTargets=`Target4` />
</Target>
<Target Name=`Target3`>
<Message Text=`Target 3` />
</Target>
<Target Name=`Target4`>
<Message Text=`Target 4` />
</Target>
</Project>
";
string contentsC = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`foo` />
</Target>
<Target Name=`Sleep`>
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(500)) + @"` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
File.WriteAllText(projC, CleanupFileContents(contentsC));
_parameters.MaxNodeCount = 2;
_parameters.EnableNodeReuse = false;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null,
new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
// We should never get to Error2, because it's supposed to execute after Error1, which failed.
_logger.AssertLogDoesntContain("Error 2");
// We should, however, get to Target2, Target3, and Target4, since they're part of the OnError
// chain for Error1
_logger.AssertLogContains("Error in Target2");
_logger.AssertLogContains("Target 3");
_logger.AssertLogContains("Target 4");
// We should end up skipping Error1 on the second call to B.
string skippedMessage1 =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteFailure", "Error1");
_logger.AssertLogContains(skippedMessage1);
// We shouldn't, however, see skip messages for the OnError targets
string skippedMessage2 =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteFailure", "Target2");
_logger.AssertLogDoesntContain(skippedMessage2);
string skippedMessage3 =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteSuccess", "Target3");
_logger.AssertLogDoesntContain(skippedMessage3);
string skippedMessage4 =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteSuccess", "Target4");
_logger.AssertLogDoesntContain(skippedMessage4);
_buildManager.EndBuild();
}
/// <summary>
/// If two requests are made for the same project, and they call in with
/// just the right timing such that:
/// - request 1 builds for a while, reaches a P2P, and blocks
/// - request 2 starts building, skips for a while, reaches the above P2P, and
/// blocks waiting for request 1's results
/// - request 1 resumes building, errors, and exits
/// - request 2 resumes building
///
/// Then request 2 should end up exiting in the same fashion.
///
/// This simple test verifies that if there are two error targets in a row, AND
/// they're marked as ContinueOnError=ErrorAndContinue, then we won't bail, but
/// will continue executing (on the first request) or skipping (on the second)
/// </summary>
[Fact]
public void VerifyMultipleRequestForSameProjectWithErrors_ErrorAndContinue()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
var projC = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`" + String.Join(";", projB, projC, projB) + @"` BuildInParallel=`true` />
</Target>
</Project>
";
string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build` DependsOnTargets=`CallMSBuild;Error1;Error2` />
<Target Name=`CallMSBuild`>
<MSBuild Projects=`" + projC + @"` Targets=`Sleep` BuildInParallel=`true` />
</Target>
<Target Name=`Error1`>
<Error Text=`Error 1` ContinueOnError=`ErrorAndContinue` />
</Target>
<Target Name=`Error2`>
<Error Text=`Error 2` />
</Target>
</Project>
";
string contentsC = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`foo` />
</Target>
<Target Name=`Sleep`>
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(500)) + @"` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
File.WriteAllText(projC, CleanupFileContents(contentsC));
_parameters.MaxNodeCount = 2;
_parameters.EnableNodeReuse = false;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null,
new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
// We should see both Error1 and Error2
_logger.AssertLogContains("Error 1");
_logger.AssertLogContains("Error 2");
// We should also end up skipping them both.
string skippedMessage1 =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteFailure", "Error1");
_logger.AssertLogContains(skippedMessage1);
string skippedMessage2 =
ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TargetAlreadyCompleteFailure", "Error2");
_logger.AssertLogContains(skippedMessage2);
_buildManager.EndBuild();
}
/// <summary>
/// If two requests are made for the same project, and they call in with
/// just the right timing such that:
/// - request 1 builds for a while, reaches a P2P, and blocks
/// - request 2 starts building, skips for a while, reaches the above P2P, and
/// blocks waiting for request 1's results
/// - request 1 resumes building, errors, and exits
/// - request 2 resumes building
///
/// Then request 2 should end up exiting in the same fashion.
///
/// This test verifies that if the errors are in AfterTargets, we still
/// exit as though the target that those targets run after has already run.
/// </summary>
[Fact]
public void VerifyMultipleRequestForSameProjectWithErrors_AfterTargets()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
var projC = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<MSBuild Projects=`" + String.Join(";", projB, projC, projB) + @"` BuildInParallel=`true` />
</Target>
</Project>
";
string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build` DependsOnTargets=`CallMSBuild` />
<Target Name=`CallMSBuild`>
<MSBuild Projects=`" + projC + @"` Targets=`Sleep` BuildInParallel=`true` />
</Target>
<Target Name=`Error1` AfterTargets=`CallMSBuild`>
<Error Text=`Error 1` />
</Target>
<Target Name=`Error2` AfterTargets=`CallMSBuild`>
<Error Text=`Error 2` />
</Target>
</Project>
";
string contentsC = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<Message Text=`foo` />
</Target>
<Target Name=`Sleep`>
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(500)) + @"` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
File.WriteAllText(projC, CleanupFileContents(contentsC));
_parameters.MaxNodeCount = 2;
_parameters.EnableNodeReuse = false;
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null,
new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
// We should never get to Error2, because we should never run its AfterTarget, after
// the AfterTarget with Error1 failed
_logger.AssertLogDoesntContain("Error 2");
_buildManager.EndBuild();
}
/// <summary>
/// Related to the two tests above, if two requests are made for the same project, but
/// for different entry targets, and a target fails in the first request, if the second
/// request also runs that target, its skip-unsuccessful should behave in the same
/// way as if the target had actually errored.
/// </summary>
[Fact]
public void VerifyMultipleRequestForSameProjectWithErrors_DifferentEntrypoints()
{
var projA = _env.CreateFile(".proj").Path;
var projB = _env.CreateFile(".proj").Path;
string contentsA = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<ItemGroup>
<PR Include=`" + projB + @"`>
<Targets>Build</Targets>
</PR>
<PR Include=`" + projB + @"`>
<Targets>Build2</Targets>
</PR>
</ItemGroup>
<Target Name=`Build`>
<MSBuild Projects=`@(PR)` Targets=`%(PR.Targets)` ContinueOnError=`true` />
</Target>
</Project>
";
const string contentsB = @"
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build` DependsOnTargets=`Target1;Error1`>
<Message Text=`[Build]` />
</Target>
<Target Name=`Build2` DependsOnTargets=`Target2;Error1;Error2`>
<Message Text=`[Build2]` />
</Target>
<Target Name=`Target1`>
<Message Text=`[Target1]` />
</Target>
<Target Name=`Target2`>
<Message Text=`[Target2]` />
</Target>
<Target Name=`Error1`>
<Error Text=`[Error1]` />
</Target>
<Target Name=`Error2`>
<Error Text=`[Error2]` />
</Target>
</Project>
";
File.WriteAllText(projA, CleanupFileContents(contentsA));
File.WriteAllText(projB, CleanupFileContents(contentsB));
_buildManager.BeginBuild(_parameters);
var data = new BuildRequestData(projA, new Dictionary<string, string>(), null,
new[] { "Build" }, new HostServices());
BuildResult result = _buildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Success, result.OverallResult);
// We should never get to Error2, because it's only ever executed in the second
// request after Error1, which should skip-unsuccessful and exit
_logger.AssertLogDoesntContain("[Error2]");
_buildManager.EndBuild();
}
/// <summary>
/// Verify that we can submit multiple simultaneous submissions with
/// legacy threading mode active and successfully build.
/// </summary>
[Fact]
public void TestSimultaneousSubmissionsWithLegacyThreadingData()
{
string projectContent = @"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`Build`>
<!-- Wait 200 ms -->
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(200)) + @"` />
</Target>
</Project>
";
var projectPath1 = _env.CreateFile(".proj").Path;
File.WriteAllText(projectPath1, CleanupFileContents(projectContent));
var project1 = new Project(projectPath1);
var projectPath2 = _env.CreateFile(".proj").Path;
File.WriteAllText(projectPath2, CleanupFileContents(projectContent));
var project2 = new Project(projectPath2);
ConsoleLogger cl = new ConsoleLogger();
var buildParameters =
new BuildParameters(ProjectCollection.GlobalProjectCollection)
{
Loggers = new ILogger[] { cl },
LegacyThreadingSemantics = true
};
BuildManager.DefaultBuildManager.BeginBuild(buildParameters);
using var project1DoneEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project1);
BuildRequestData requestData = new BuildRequestData(pi, new[] { "Build" });
BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
submission.Execute();
project1DoneEvent.Set();
});
using var project2DoneEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project2);
BuildRequestData requestData = new BuildRequestData(pi, new[] { "Build" });
BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
submission.Execute();
project2DoneEvent.Set();
});
project1DoneEvent.WaitOne();
project2DoneEvent.WaitOne();
BuildManager.DefaultBuildManager.EndBuild();
}
/// <summary>
/// Verify that we can submit multiple simultaneous submissions with
/// legacy threading mode active and successfully build, and that one of those
/// submissions can P2P to the other.
/// </summary>
[Fact]
public void TestSimultaneousSubmissionsWithLegacyThreadingData_P2P()
{
string projectContent1 = @"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`CopyRunEnvironmentFiles`>
<!-- Wait 100 ms -->
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(100)) + @"` />
</Target>
<Target Name=`MyConsoleTarget`>
<!-- Wait 100 ms -->
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(100)) + @"` />
</Target>
</Project>
";
var projectPath1 = _env.CreateFile(".proj").Path;
File.WriteAllText(projectPath1, CleanupFileContents(projectContent1));
var project1 = new Project(projectPath1);
string projectContent2 = @"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`MSDeployPublish` />
<Target Name=`DoStuff` AfterTargets=`MSDeployPublish`>
<MSBuild Projects=`" + projectPath1 + @"` Targets=`MyConsoleTarget` />
</Target>
</Project>
";
var projectPath2 = _env.CreateFile(".proj").Path;
File.WriteAllText(projectPath2, CleanupFileContents(projectContent2));
var project2 = new Project(projectPath2);
var cl = new ConsoleLogger();
var buildParameters =
new BuildParameters(ProjectCollection.GlobalProjectCollection)
{
Loggers = new ILogger[] { cl },
LegacyThreadingSemantics = true
};
BuildManager.DefaultBuildManager.BeginBuild(buildParameters);
using var project1DoneEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
// need to kick off project 2 first so that it project 1 can get submitted before the P2P happens
ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project2);
BuildRequestData requestData = new BuildRequestData(pi, new[] { "MSDeployPublish" });
BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
BuildResult br = submission.Execute();
Assert.Equal(BuildResultCode.Success, br.OverallResult);
project1DoneEvent.Set();
});
using var project2DoneEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project1);
BuildRequestData requestData = new BuildRequestData(pi, new string[] { "CopyRunEnvironmentFiles" });
BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
BuildResult br = submission.Execute();
Assert.Equal(BuildResultCode.Success, br.OverallResult);
project2DoneEvent.Set();
});
project1DoneEvent.WaitOne();
project2DoneEvent.WaitOne();
BuildManager.DefaultBuildManager.EndBuild();
}
/// <summary>
/// Verify that we can submit multiple simultaneous submissions with
/// legacy threading mode active and successfully build, and that one of those
/// submissions can P2P to the other.
///
/// A variation of the above test, where multiple nodes are available, so the
/// submissions aren't restricted to running strictly serially by the single in-proc
/// node.
/// </summary>
[Fact]
public void TestSimultaneousSubmissionsWithLegacyThreadingData_P2P_MP()
{
string projectContent1 = @"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`CopyRunEnvironmentFiles`>
<!-- Wait 100 ms -->
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(100)) + @"` />
</Target>
<Target Name=`MyConsoleTarget`>
<!-- Wait 100 ms -->
<Exec Command=`" + Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(100)) + @"` />
</Target>
</Project>
";
string projectPath1 = _env.CreateFile(".proj").Path;
File.WriteAllText(projectPath1, CleanupFileContents(projectContent1));
var project1 = new Project(projectPath1);
string projectContent2 = @"<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<Target Name=`MSDeployPublish` />
<Target Name=`DoStuff` AfterTargets=`MSDeployPublish`>
<MSBuild Projects=`" + projectPath1 + @"` Targets=`MyConsoleTarget` />
</Target>
</Project>
";
var projectPath2 = _env.CreateFile(".proj").Path;
File.WriteAllText(projectPath2, CleanupFileContents(projectContent2));
var project2 = new Project(projectPath2);
var cl = new ConsoleLogger();
var buildParameters =
new BuildParameters(ProjectCollection.GlobalProjectCollection)
{
Loggers = new ILogger[] { cl },
LegacyThreadingSemantics = true,
MaxNodeCount = 2,
EnableNodeReuse = false
};
BuildManager.DefaultBuildManager.BeginBuild(buildParameters);
using var project1DoneEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
// need to kick off project 2 first so that it project 1 can get submitted before the P2P happens
ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project2);
BuildRequestData requestData = new BuildRequestData(pi, new string[] { "MSDeployPublish" });
BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
BuildResult br = submission.Execute();
Assert.Equal(BuildResultCode.Success, br.OverallResult);
project1DoneEvent.Set();
});
using var project2DoneEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
ProjectInstance pi = BuildManager.DefaultBuildManager.GetProjectInstanceForBuild(project1);
BuildRequestData requestData = new BuildRequestData(pi, new string[] { "CopyRunEnvironmentFiles" });
BuildSubmission submission = BuildManager.DefaultBuildManager.PendBuildRequest(requestData);
BuildResult br = submission.Execute();
Assert.Equal(BuildResultCode.Success, br.OverallResult);
project2DoneEvent.Set();
});
project1DoneEvent.WaitOne();
project2DoneEvent.WaitOne();
BuildManager.DefaultBuildManager.EndBuild();
}
/// <summary>
/// Ensures that properties and items are transferred from an out-of-proc project to an in-proc project.
/// </summary>
/// <remarks>
/// This differs from transferring a project instance to an out-of-proc node because in this case the project
/// was loaded by MSBuild, not supplied directly by the user.
/// </remarks>
[Fact]
public void Regress265010()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<PropertyGroup>
<Prop>BaseValue</Prop>
</PropertyGroup>
<ItemGroup>
<Item Include='BaseItem'/>
</ItemGroup>
<Target Name='BaseTest'>
<Message Text='[$(Prop)]'/>
<Message Text='[@(Item)]'/>
<PropertyGroup>
<Prop>NewValue</Prop>
</PropertyGroup>
<ItemGroup>
<Item Include='NewItem'/>
</ItemGroup>
</Target>
<Target Name='MovedTest'>
<Message Text='[$(Prop)]'/>
<Message Text='[@(Item)]'/>
</Target>
</Project>
");
string fileName = _env.CreateFile(".proj").Path;
File.WriteAllText(fileName, contents);
_buildManager.BeginBuild(_parameters);
var services = new HostServices();
services.SetNodeAffinity(fileName, NodeAffinity.OutOfProc);
var data = new BuildRequestData(fileName, new Dictionary<string, string>(), MSBuildDefaultToolsVersion, new[] { "BaseTest" }, services);
_buildManager.PendBuildRequest(data).Execute();
_logger.AssertLogContains("[BaseValue]");
_logger.AssertLogContains("[BaseItem]");
_logger.ClearLog();
_parameters.ResetCaches = false;
services.SetNodeAffinity(fileName, NodeAffinity.InProc);
data = new BuildRequestData(fileName, new Dictionary<string, string>(), MSBuildDefaultToolsVersion, new[] { "MovedTest" }, services);
_buildManager.PendBuildRequest(data).Execute();
_logger.AssertLogContains("[NewValue]");
_logger.AssertLogContains("[BaseItem;NewItem]");
_logger.AssertLogDoesntContain("[BaseValue]");
_buildManager.EndBuild();
}
/// <summary>
/// Verifies that all warnings are treated as errors and that the overall build result is a failure.
/// </summary>
[Fact]
public void WarningsAreTreatedAsErrorsAll()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='target1'>
<Warning Text='This warning should be treated as an error' Code='ABC123'/>
<Warning Text='This warning should NOT be treated as an error' />
</Target>
</Project>
");
_parameters.WarningsAsErrors = new HashSet<string>();
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildResult result1 = _buildManager.BuildRequest(new BuildRequestData(instance, new[] { "target1" }));
_buildManager.EndBuild();
Assert.Equal(0, _logger.WarningCount);
Assert.Equal(2, _logger.ErrorCount);
Assert.Equal(BuildResultCode.Failure, result1.OverallResult);
Assert.True(result1.HasResultsForTarget("target1"));
}
/// <summary>
/// Verifies that only the specified warnings are treated as errors and that the overall build result is a failure.
/// </summary>
[Fact]
public void WarningsAreTreatedAsErrorsSpecific()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='target1'>
<Warning Text='This warning should be treated as an error' Code='ABC123'/>
<Warning Text='This warning should NOT be treated as an error' Code='NA123' />
<Warning Text='This warning should NOT be treated as an error' />
</Target>
</Project>
");
_parameters.WarningsAsErrors = new HashSet<string> { "ABC123" };
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildResult result1 = _buildManager.BuildRequest(new BuildRequestData(instance, new[] { "target1" }));
_buildManager.EndBuild();
Assert.Equal(2, _logger.WarningCount);
Assert.Equal(1, _logger.ErrorCount);
Assert.Equal(BuildResultCode.Failure, result1.OverallResult);
Assert.True(result1.HasResultsForTarget("target1"));
}
/// <summary>
/// Verifies that when building targets which emit warnings, they still show as succeeding but the overall build result is a failure.
/// </summary>
[Fact]
public void WarningsAreTreatedAsErrorsButTargetsStillSucceed()
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='target1'>
<Message Text='text'/>
</Target>
<Target Name='target2'>
<Warning Text='This warning should be treated as an error' Code='ABC123'/>
</Target>
</Project>
");
_parameters.WarningsAsErrors = new HashSet<string> { "ABC123" };
Project project = CreateProject(contents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildResult buildResult = _buildManager.BuildRequest(new BuildRequestData(instance, new string[] { "target1", "target2" }));
_buildManager.EndBuild();
Assert.Equal(0, _logger.WarningCount);
Assert.Equal(1, _logger.ErrorCount);
Assert.Equal(BuildResultCode.Failure, buildResult.OverallResult);
Assert.True(buildResult.HasResultsForTarget("target1"));
Assert.True(buildResult.HasResultsForTarget("target2"));
// The two targets should still show as success because they don't know their warning was changed to an error
// Logging a warning as an error does not change execution, only the final result of the build
Assert.Equal(TargetResultCode.Success, buildResult.ResultsByTarget["target1"].ResultCode);
Assert.Equal(TargetResultCode.Success, buildResult.ResultsByTarget["target2"].ResultCode);
}
/// <summary>
/// Helper for cache tests. Builds a project and verifies the right cache files are created.
/// </summary>
private static string BuildAndCheckCache(BuildManager localBuildManager, IEnumerable<string> exceptCacheDirectories)
{
string contents = CleanupFileContents(@"
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
<Target Name='One' Outputs='one.txt'>
</Target>
<Target Name='Two' Outputs='two.txt'>
</Target>
<Target Name='Three' Outputs='three.txt'>
</Target>
</Project>
");
string fileName = Path.GetTempFileName();
File.WriteAllText(fileName, contents);
string cacheDirectory = FileUtilities.GetCacheDirectory();
var parameters = new BuildParameters();
localBuildManager.BeginBuild(parameters);
try
{
var services = new HostServices();
BuildRequestData data = new BuildRequestData(fileName, new Dictionary<string, string>(), MSBuildDefaultToolsVersion, new[] { "One", "Two", "Three" }, services);
var result = localBuildManager.PendBuildRequest(data).Execute();
Assert.Equal(BuildResultCode.Success, result.OverallResult); // "Test project failed to build correctly."
}
finally
{
localBuildManager.EndBuild();
}
// Ensure that we got the cache files we expected. There should be one set of results in there, once we exclude
// any of the specified directories from previous builds in the same test.
string directory = Directory.EnumerateDirectories(cacheDirectory).Except(exceptCacheDirectories).First();
// Within this directory should be a set of target results files, one for each of the targets we invoked.
var resultsFiles = Directory.EnumerateFiles(directory).Select(Path.GetFileName);
Assert.Equal(3, resultsFiles.Count());
Assert.Contains("One.cache", resultsFiles);
Assert.Contains("Two.cache", resultsFiles);
Assert.Contains("Three.cache", resultsFiles);
// Return the cache directory created for this build.
return directory;
}
/// <summary>
/// Extract a string dictionary from the property enumeration on a project started event.
/// </summary>
private static Dictionary<string, string> ExtractProjectStartedPropertyList(IEnumerable properties)
{
Dictionary<string, string> propertiesLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Internal.Utilities.EnumerateProperties(properties, propertiesLookup,
static (dict, kvp) => dict.Add(kvp.Key, kvp.Value));
return propertiesLookup;
}
/// <summary>
/// Retrieves a BuildRequestData using the specified contents, default targets and an empty project collection.
/// </summary>
private BuildRequestData GetBuildRequestData(string projectContents) => GetBuildRequestData(projectContents, Array.Empty<string>());
/// <summary>
/// Retrieves a GraphBuildRequestData using the specified contents, default targets and an empty project collection.
/// </summary>
private GraphBuildRequestData GetGraphBuildRequestData(string projectContents) => GetGraphBuildRequestData(projectContents, Array.Empty<string>());
/// <summary>
/// Retrieves a BuildRequestData using the specified contents, targets and project collection.
/// </summary>
private BuildRequestData GetBuildRequestData(string projectContents, string[] targets, string toolsVersion = null)
=> new BuildRequestData(CreateProjectInstance(projectContents, toolsVersion, _projectCollection, true), targets, _projectCollection.HostServices);
/// <summary>
/// Retrieves a GraphBuildRequestData using the specified contents, targets and project collection.
/// </summary>
private GraphBuildRequestData GetGraphBuildRequestData(string projectContents, string[] targets)
=> new GraphBuildRequestData(CreateProjectGraph(projectContents, _projectCollection), targets, _projectCollection.HostServices);
/// <summary>
/// Retrieve a ProjectInstance evaluated with the specified contents using the specified projectCollection
/// </summary>
private ProjectInstance CreateProjectInstance(string contents, string toolsVersion, ProjectCollection projectCollection, bool deleteTempProject)
{
Project project = CreateProject(contents, toolsVersion, projectCollection, deleteTempProject);
return project.CreateProjectInstance();
}
/// <summary>
/// Retrieve a CreateProjectGraph evaluated with the specified contents using the specified projectCollection
/// </summary>
private ProjectGraph CreateProjectGraph(string contents, ProjectCollection projectCollection)
{
var projectFilePath = _env.CreateFile().Path;
File.WriteAllText(projectFilePath, contents);
return new ProjectGraph(projectFilePath, projectCollection);
}
/// <summary>
/// Retrieve a Project with the specified contents using the specified projectCollection
/// </summary>
private Project CreateProject(string contents, string toolsVersion, ProjectCollection projectCollection, bool deleteTempProject)
{
using ProjectFromString projectFromString = new(contents, null, toolsVersion, projectCollection);
Project project = projectFromString.Project;
project.FullPath = _env.CreateFile().Path;
if (!deleteTempProject)
{
project.Save();
}
if (deleteTempProject)
{
File.Delete(project.FullPath);
}
return project;
}
/// <summary>
/// Generate dummy projects
/// </summary>
private static ProjectInstance GenerateDummyProjects(string shutdownProjectDirectory, int parallelProjectCount, ProjectCollection projectCollection)
{
Directory.CreateDirectory(shutdownProjectDirectory);
// Generate the project. It will have the following format. Setting the AdditionalProperties
// causes the projects to be built to be separate configs, which allows us to build the same project
// a bunch of times in parallel.
//
// <Project/>
// <ItemGroup>
// <ProjectReference Include="RootProjectName.proj">
// <AdditionalProperties>p={incremented value}</AdditionalProperties>
// </ProjectReference>
// ...
// </ItemGroup>
//
// <Target Name="Build">
// <MSBuild Projects="@(ProjectReference) Targets="ChildBuild" BuildInParallel="true" />
// </Target>
//
// <Target Name="ChildBuild" />
// </Project>
string rootProjectPath = Path.Combine(shutdownProjectDirectory, String.Format(CultureInfo.InvariantCulture, "RootProj_{0}.proj", Guid.NewGuid().ToString("N")));
var rootProject = ProjectRootElement.Create(rootProjectPath, projectCollection);
ProjectTargetElement buildTarget = rootProject.AddTarget("Build");
ProjectTaskElement buildTask = buildTarget.AddTask("MSBuild");
buildTask.SetParameter("Projects", "@(ProjectReference)");
buildTask.SetParameter("BuildInParallel", "true");
buildTask.SetParameter("Targets", "ChildBuild");
rootProject.AddTarget("ChildBuild");
IDictionary<string, string> metadata = new Dictionary<string, string>(1);
for (int i = 0; i < parallelProjectCount; i++)
{
// Add the ProjectReference item for this actual config.
metadata["AdditionalProperties"] = String.Format(CultureInfo.InvariantCulture, "p={0}", i);
rootProject.AddItem("ProjectReference", rootProjectPath, metadata);
}
rootProject.Save();
return new ProjectInstance(rootProject);
}
[Fact]
public void ShouldBuildMutatedProjectInstanceWhoseProjectWasPreviouslyBuiltAsAP2PDependency()
{
const string mainProjectContents = @"<Project>
<Target Name=""BuildOther"" Outputs=""@(ReturnValue)"">
<MSBuild Projects=""{0}"" Targets=""Foo"">
<Output TaskParameter=""TargetOutputs"" ItemName=""ReturnValue"" />
</MSBuild>
</Target>
</Project>";
var p2pProjectContents =
@"<Project>
<PropertyGroup>
<P>InitialValue</P>
</PropertyGroup>
<Target Name=""Foo"" Outputs=""$(P)""/>
</Project>";
using (var env = TestEnvironment.Create())
using (var collection = new ProjectCollection())
using (var manager = new BuildManager())
{
try
{
var testFiles = env.CreateTestProjectWithFiles(string.Empty, new[] { "p2p", "main" });
var p2pProjectPath = testFiles.CreatedFiles[0];
File.WriteAllText(p2pProjectPath, p2pProjectContents);
using ProjectRootElementFromString projectRootElementFromString = new(string.Format(mainProjectContents, p2pProjectPath), collection);
ProjectRootElement mainRootElement = projectRootElementFromString.Project;
mainRootElement.FullPath = testFiles.CreatedFiles[1];
mainRootElement.Save();
// build p2p project as a real p2p dependency of some other project. This loads the p2p into msbuild's caches
var mainProject = new Project(mainRootElement, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, collection);
var mainInstance = mainProject.CreateProjectInstance(ProjectInstanceSettings.Immutable).DeepCopy(isImmutable: false);
Assert.Empty(mainInstance.GlobalProperties);
var request = new BuildRequestData(mainInstance, new[] { "BuildOther" });
var parameters = new BuildParameters
{
DisableInProcNode = true,
EnableNodeReuse = false,
};
manager.BeginBuild(parameters);
var submission = manager.PendBuildRequest(request);
var results = submission.Execute();
Assert.Equal(BuildResultCode.Success, results.OverallResult);
Assert.Equal("InitialValue", results.ResultsByTarget["BuildOther"].Items.First().ItemSpec);
// build p2p directly via mutated ProjectInstances based of the same Project.
// This should rebuild and the result should reflect the in-memory changes and not reuse stale cache info
var p2pProject = new Project(p2pProjectPath, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, collection);
for (var i = 0; i < 2; i++)
{
var p2pInstance = p2pProject.CreateProjectInstance(ProjectInstanceSettings.Immutable).DeepCopy(isImmutable: false);
var newPropertyValue = $"NewValue_{i}";
p2pInstance.SetProperty("P", newPropertyValue);
request = new BuildRequestData(p2pInstance, new[] { "Foo" });
submission = manager.PendBuildRequest(request);
results = submission.Execute();
Assert.Empty(p2pInstance.GlobalProperties);
Assert.Equal(BuildResultCode.Success, results.OverallResult);
Assert.Equal(newPropertyValue, results.ResultsByTarget["Foo"].Items.First().ItemSpec);
}
}
finally
{
manager.EndBuild();
}
}
}
[Fact]
public void OutOfProcFileBasedP2PBuildSucceeds()
{
const string mainProject = @"<Project>
<Target Name=`MainTarget` Returns=`foo;@(P2PReturnValue)`>
<MSBuild Projects=`{0}` Targets=`P2PTarget`>
<Output TaskParameter=`TargetOutputs` ItemName=`P2PReturnValue` />
</MSBuild>
</Target>
</Project>";
const string p2pProject = @"<Project>
<Target Name=`P2PTarget` Returns=`bar`>
<Message Text=`Bar` Importance=`High` />
</Target>
</Project>";
var testFiles = _env.CreateTestProjectWithFiles(string.Empty, new[] { "main", "p2p" }, string.Empty);
var buildParameters = new BuildParameters(_projectCollection)
{
DisableInProcNode = true,
EnableNodeReuse = false,
Loggers = new ILogger[] { _logger }
};
_buildManager.BeginBuild(buildParameters);
try
{
var p2pProjectPath = testFiles.CreatedFiles[1];
var cleanedUpP2pContents = CleanupFileContents(p2pProject);
File.WriteAllText(p2pProjectPath, cleanedUpP2pContents);
var mainProjectPath = testFiles.CreatedFiles[0];
var cleanedUpMainContents = CleanupFileContents(string.Format(mainProject, p2pProjectPath));
File.WriteAllText(mainProjectPath, cleanedUpMainContents);
var buildRequestData = new BuildRequestData(
mainProjectPath,
new Dictionary<string, string>(),
MSBuildConstants.CurrentToolsVersion,
new[] { "MainTarget" },
null);
var submission = _buildManager.PendBuildRequest(buildRequestData);
var result = submission.Execute();
Assert.Equal(BuildResultCode.Success, result.OverallResult);
Assert.Equal("foo;bar",
string.Join(";", result.ResultsByTarget["MainTarget"].Items.Select(i => i.ItemSpec)));
}
finally
{
_buildManager.EndBuild();
}
}
/// <summary>
/// When a <see cref="ProjectInstance"/> based <see cref="BuildRequestData"/> is built out of proc, the node should
/// not reload it from disk but instead fully utilize the entire translate project instance state
/// to do the build.
/// </summary>
/// <param name="shouldSerializeEntireState"><see langword="true"/> to serialize the entire project instance state; otherwise, <see langword="false"/>.</param>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void OutOfProcProjectInstanceBasedBuildDoesNotReloadFromDisk(bool shouldSerializeEntireState)
{
const string mainProject = @"<Project>
<PropertyGroup>
<ImportIt>true</ImportIt>
</PropertyGroup>
<Import Project=""{0}"" Condition=""'$(ImportIt)' == 'true'""/>
</Project>";
const string importProject = @"<Project>
<Target Name=""Foo"">
<Message Text=""Bar"" Importance=""High"" />
</Target>
</Project>";
var testFiles = _env.CreateTestProjectWithFiles(string.Empty, new[] { "main", "import" }, string.Empty);
var importPath = testFiles.CreatedFiles[1];
File.WriteAllText(importPath, CleanupFileContents(importProject));
using ProjectRootElementFromString projectRootElementFromString = new(string.Format(mainProject, importPath), _projectCollection);
ProjectRootElement root = projectRootElementFromString.Project;
root.FullPath = Path.GetTempFileName();
root.Save();
// build a project which runs a target from an imported file
var project = new Project(root, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion,
_projectCollection);
ProjectInstance instance = project.CreateProjectInstance(ProjectInstanceSettings.Immutable).DeepCopy(false);
instance.TranslateEntireState = shouldSerializeEntireState;
var request = new BuildRequestData(instance, new[] { "Foo" });
var parameters = new BuildParameters(_projectCollection)
{
DisableInProcNode = true,
EnableNodeReuse = false,
Loggers = new ILogger[] { _logger }
};
_buildManager.BeginBuild(parameters);
var submission = _buildManager.PendBuildRequest(request);
var results = submission.Execute();
Assert.True(results.OverallResult == BuildResultCode.Success);
// reset caches to ensure nothing is reused
_buildManager.EndBuild();
_buildManager.ResetCaches();
// mutate the file on disk such that the import (containing the target to get executed)
// is no longer imported
project.SetProperty("ImportIt", "false");
project.Save();
// Build the initial project instance again.
// The project instance is not in sync with the file anymore, making it an in-memory build:
// the file does not contain the target Foo, but the project instance does
// Building the stale project instance should still succeed when the entire state is translated: MSBuild should use the
// in-memory state to build and not reload from disk.
_buildManager.BeginBuild(parameters);
request = new BuildRequestData(instance, new[] { "Foo" }, null,
BuildRequestDataFlags.ReplaceExistingProjectInstance);
submission = _buildManager.PendBuildRequest(request);
results = submission.Execute();
_buildManager.EndBuild();
if (shouldSerializeEntireState)
{
Assert.Equal(BuildResultCode.Success, results.OverallResult);
}
else
{
Assert.Equal(BuildResultCode.Failure, results.OverallResult);
Assert.Contains("The target \"Foo\" does not exist in the project", _logger.FullLog,
StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void OutOfProcEvaluationIdsUnique()
{
const string mainProject = @"<Project>
<Target Name=`MainTarget`>
<MSBuild Projects=`{0};{1}` Targets=`DummyTarget` />
</Target>
</Project>";
const string childProject = @"<Project>
<Target Name=`DummyTarget`>
<Message Text=`Bar` Importance=`High` />
</Target>
</Project>";
var testFiles = _env.CreateTestProjectWithFiles(string.Empty, new[] { "main", "child1", "child2" }, string.Empty);
var buildParameters = new BuildParameters(_projectCollection)
{
DisableInProcNode = true,
EnableNodeReuse = false,
Loggers = new ILogger[] { _logger }
};
_buildManager.BeginBuild(buildParameters);
try
{
var child1ProjectPath = testFiles.CreatedFiles[1];
var child2ProjectPath = testFiles.CreatedFiles[2];
var cleanedUpChildContents = CleanupFileContents(childProject);
File.WriteAllText(child1ProjectPath, cleanedUpChildContents);
File.WriteAllText(child2ProjectPath, cleanedUpChildContents);
var mainProjectPath = testFiles.CreatedFiles[0];
var cleanedUpMainContents = CleanupFileContents(string.Format(mainProject, child1ProjectPath, child2ProjectPath));
File.WriteAllText(mainProjectPath, cleanedUpMainContents);
var buildRequestData = new BuildRequestData(
mainProjectPath,
new Dictionary<string, string>(),
MSBuildConstants.CurrentToolsVersion,
new[] { "MainTarget" },
null);
var submission = _buildManager.PendBuildRequest(buildRequestData);
var result = submission.Execute();
Assert.Equal(BuildResultCode.Success, result.OverallResult);
Assert.True(_logger.AllBuildEvents.OfType<ProjectEvaluationStartedEventArgs>().GroupBy(args => args.BuildEventContext.EvaluationId).All(g => g.Count() == 1));
}
finally
{
_buildManager.EndBuild();
}
}
/// <summary>
/// Regression test for https://github.com/dotnet/msbuild/issues/3047
/// </summary>
[Fact]
public void MultiProcReentrantProjectWithCallTargetDoesNotFail()
{
var a =
@"<Project>
<Target Name=`EntryTarget`>
<MSBuild Projects=`b;c` BuildInParallel=`true` />
</Target>
</Project>".Cleanup();
var b =
@"<Project>
<Target Name=`BTarget`>
<MSBuild Projects=`reentrant` Targets=`BuildGenerateSources` BuildInParallel=`true` />
</Target>
</Project>".Cleanup();
var c =
$@"<Project>
<Target Name=`CTarget`>
<Exec Command=`{Helpers.GetSleepCommand(TimeSpan.FromSeconds(1))}` />
<MSBuild Projects=`reentrant` Targets=`BuildGenerated` BuildInParallel=`true` />
</Target>
</Project>".Cleanup();
var delay =
$@"<Project>
<Target Name=`Delay`>
<Exec Command=`{Helpers.GetSleepCommand(TimeSpan.FromSeconds(2))}` />
</Target>
</Project>".Cleanup();
var reentrant =
$@"<Project DefaultTargets=`Build`>
<Target Name=`BuildGenerateSources` DependsOnTargets=`_Get;Build`></Target>
<Target Name=`BuildGenerated`>
<CallTarget Targets=`Build` />
</Target>
<Target Name=`Build`>
<CallTarget Targets=`_Get` />
</Target>
<Target Name=`_Get`>
<MSBuild Projects=`delay` BuildInParallel=`true` />
<Exec Command=`{Helpers.GetSleepCommand(TimeSpan.FromSeconds(5))}` YieldDuringToolExecution=`true` StandardOutputImportance=`low` />
</Target>
</Project>".Cleanup();
using (var env = TestEnvironment.Create(_output))
{
var entryFile = env.CreateFile(nameof(a), a).Path;
env.CreateFile(nameof(b), b);
env.CreateFile(nameof(c), c);
env.CreateFile(nameof(delay), delay);
env.CreateFile(nameof(reentrant), reentrant);
var mockLogger = new MockLogger(_output);
var buildParameters = new BuildParameters()
{
DisableInProcNode = true,
MaxNodeCount = NativeMethodsShared.GetLogicalCoreCount(),
EnableNodeReuse = false,
Loggers = new List<ILogger>()
{
mockLogger
}
};
var buildRequestData = new BuildRequestData(entryFile, new Dictionary<string, string>(), MSBuildDefaultToolsVersion, new[] { "EntryTarget" }, null);
var result = _buildManager.Build(buildParameters, buildRequestData);
result.OverallResult.ShouldBe(BuildResultCode.Success);
}
}
[Fact]
public void IdenticalSubmissionsShouldCompleteAndNotHangTheBuildOnMissingTargetExceptions()
{
var projectContents =
$@"<Project InitialTargets=`Sleep`>
<Target Name=`Sleep`>
<!-- Add a sleep to give the Scheduler time to register both submissions -->
<Exec Command='{Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(200))}' />
</Target>
<!-- The nonexistent target will force the TargetBuilder to not include a result for the Build target,
thus causing a BuildResult cache miss in the Scheduler, which causes the Scheduler to
handoff logging completion to the BuildManager.
-->
<Target Name='Build' DependsOnTargets='NonExistent'>
</Target>
</Project>";
Exception exception = null;
using var manager = new BuildManager();
using (var env = TestEnvironment.Create())
{
try
{
var testFiles = env.CreateTestProjectWithFiles(projectContents);
var parameters = new BuildParameters
{
MaxNodeCount = 1,
DisableInProcNode = false,
ShutdownInProcNodeOnBuildFinish = false,
UseSynchronousLogging = false,
};
/*
* When the scheduler completes a request, it looks ahead at the queue of unscheduled requests and
* preemptively completes identical ones (same global properties, path, toolsversion, AND requested targets).
* When the BuildResult has a result for the unscheduled identical request (most of the times), then the scheduler
* simulates the ProjectStarted / ProjectEnded events for the unscheduled request and completes it.
*
* However, when the initial build fails in such a way that there is no result for the requested targets,
* the initial BuildResult cannot satisfy the unscheduled submission. In this scenario the Scheduler still completes
* the request but does not simulate the ProjectStarted / ProjectEnded events. It also leaves logging completion to the
* BuildManager.
*/
var request1 = new BuildRequestData(testFiles.ProjectFile, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, new[] { "Build" }, null);
var request2 = new BuildRequestData(testFiles.ProjectFile, new Dictionary<string, string>(), MSBuildConstants.CurrentToolsVersion, new[] { "Build" }, null);
/* During builds, msbuild changes the current directory.
* When this test fails, the build never finishes so the current directory never gets restored.
* Force it to restore via the TestEnvironment, otherwise the true test failure gets masked by the current directory not getting restored.
*/
env.SetCurrentDirectory(Environment.CurrentDirectory);
manager.BeginBuild(parameters);
/* Executing async will make the Scheduler aware that there are two identical submissions under consideration.
* Otherwise it builds both sequentually and does not perform the lookahead optimization.
*/
var submission1 = manager.PendBuildRequest(request1);
var submission2 = manager.PendBuildRequest(request2);
submission1.ExecuteAsync(null, null);
submission2.ExecuteAsync(null, null);
submission1.WaitHandle.WaitOne(TimeSpan.FromSeconds(20));
submission2.WaitHandle.WaitOne(TimeSpan.FromSeconds(20));
submission1.IsCompleted.ShouldBeTrue();
submission2.IsCompleted.ShouldBeTrue();
submission1.BuildResult.Exception.ShouldBeOfType<InvalidProjectFileException>();
submission2.BuildResult.Exception.ShouldBeOfType<InvalidProjectFileException>();
}
catch (Exception e)
{
exception = e;
}
finally
{
// Only cleanup the manager when the test does not fail with Shouldly assertions (actual assert failures).
// If the assert exceptions hit then EndBuild hangs, waiting for submissions which will never complete.
if (exception is ShouldAssertException)
{
throw exception;
}
manager.EndBuild();
manager.Dispose();
}
}
}
[Fact]
public void BuildWithZeroConnectionTimeout()
{
string contents = CleanupFileContents(@"
<Project>
<Target Name='test'>
<Message Text='Text'/>
</Target>
</Project>
");
// Do not use MSBUILDINPROCENVCHECK because this test case is expected to leave a defunct in-proc node behind.
_inProcEnvCheckTransientEnvironmentVariable.Revert();
_env.SetEnvironmentVariable("MSBUILDNODECONNECTIONTIMEOUT", "0");
BuildRequestData data = GetBuildRequestData(contents);
try
{
BuildResult result = _buildManager.Build(_parameters, data);
// The build should either finish successfully (very unlikely).
result.OverallResult.ShouldBe(BuildResultCode.Success);
}
catch (Exception e)
{
// Or it should throw InternalErrorException because the node didn't get connected within 0ms.
e.ShouldBeOfType<InternalErrorException>();
}
}
[Fact]
[ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")]
public void GraphBuildValid()
{
string project1 = _env.CreateFile(".proj").Path;
string project2 = _env.CreateFile(".proj").Path;
File.WriteAllText(project1, CleanupFileContents($@"
<Project>
<ItemGroup>
<ProjectReferenceTargets Include='Build' Targets='Build' />
<ProjectReference Include='{project2}' />
</ItemGroup>
<Target Name='Build'>
<MsBuild Projects='{project2}' Targets='Build' />
</Target>
</Project>
"));
File.WriteAllText(project2, CleanupFileContents(@"
<Project>
<Target Name='Build' />
</Project>
"));
var graph = new ProjectGraph(project1);
graph.ProjectNodes.Count.ShouldBe(2);
var data = new GraphBuildRequestData(graph, Array.Empty<string>());
GraphBuildResult result = _buildManager.Build(_parameters, data);
result.OverallResult.ShouldBe(BuildResultCode.Success);
var node1 = graph.ProjectNodes.First(node => node.ProjectInstance.FullPath.Equals(project1, StringComparison.OrdinalIgnoreCase));
result.ResultsByNode.ContainsKey(node1).ShouldBeTrue();
result.ResultsByNode[node1].OverallResult.ShouldBe(BuildResultCode.Success);
var node2 = graph.ProjectNodes.First(node => node.ProjectInstance.FullPath.Equals(project2, StringComparison.OrdinalIgnoreCase));
result.ResultsByNode.ContainsKey(node2).ShouldBeTrue();
result.ResultsByNode[node2].OverallResult.ShouldBe(BuildResultCode.Success);
}
[Fact]
[ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")]
public void GraphBuildInvalid()
{
string project1 = _env.CreateFile(".proj").Path;
string project2 = _env.CreateFile(".proj").Path;
string project3 = _env.CreateFile(".proj").Path;
File.WriteAllText(project1, CleanupFileContents($@"
<Project>
<ItemGroup>
<ProjectReferenceTargets Include='Build' Targets='Build' />
<ProjectReference Include='{project2}' />
<ProjectReference Include='{project3}' />
</ItemGroup>
<Target Name='Build'>
<MsBuild Projects='@(ProjectReference)' Targets='Build' />
</Target>
</Project>
"));
File.WriteAllText(project2, CleanupFileContents(@"
<Project>
<WellThisIsntValid>
</Project>
"));
File.WriteAllText(project3, CleanupFileContents(@"
<Project>
<WellThisIsntValid>
</Project>
"));
var data = new GraphBuildRequestData(new ProjectGraphEntryPoint(project1), Array.Empty<string>());
GraphBuildResult result = _buildManager.Build(_parameters, data);
result.ShouldHaveFailed();
AggregateException aggException = result.Exception.ShouldBeOfType<AggregateException>();
aggException.InnerExceptions.Count.ShouldBe(2);
aggException.InnerExceptions[0].ShouldBeOfType<InvalidProjectFileException>().ProjectFile.ShouldBeOneOf(project2, project3);
aggException.InnerExceptions[1].ShouldBeOfType<InvalidProjectFileException>().ProjectFile.ShouldBeOneOf(project2, project3);
}
[Fact]
[ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")]
public void GraphBuildFail()
{
string project1 = _env.CreateFile(".proj").Path;
string project2 = _env.CreateFile(".proj").Path;
File.WriteAllText(project1, CleanupFileContents($@"
<Project>
<ItemGroup>
<ProjectReferenceTargets Include='Build' Targets='Build' />
<ProjectReference Include='{project2}' />
</ItemGroup>
<Target Name='Build'>
<MsBuild Projects='{project2}' Targets='Build' />
</Target>
</Project>
"));
File.WriteAllText(project2, CleanupFileContents(@"
<Project>
<Target Name='Build'>
<Error Text='Fail!'/>
</Target>
</Project>
"));
var graph = new ProjectGraph(project1);
graph.ProjectNodes.Count.ShouldBe(2);
var data = new GraphBuildRequestData(graph, Array.Empty<string>());
GraphBuildResult result = _buildManager.Build(_parameters, data);
result.OverallResult.ShouldBe(BuildResultCode.Failure);
var node1 = graph.ProjectNodes.First(node => node.ProjectInstance.FullPath.Equals(project1, StringComparison.OrdinalIgnoreCase));
result.ResultsByNode.ContainsKey(node1).ShouldBeTrue();
result.ResultsByNode[node1].OverallResult.ShouldBe(BuildResultCode.Failure);
var node2 = graph.ProjectNodes.First(node => node.ProjectInstance.FullPath.Equals(project2, StringComparison.OrdinalIgnoreCase));
result.ResultsByNode.ContainsKey(node2).ShouldBeTrue();
result.ResultsByNode[node2].OverallResult.ShouldBe(BuildResultCode.Failure);
}
[Fact]
[ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")]
public void GraphBuildCircular()
{
string project1 = _env.CreateFile(".proj").Path;
string project2 = _env.CreateFile(".proj").Path;
File.WriteAllText(project1, CleanupFileContents($@"
<Project>
<ItemGroup>
<ProjectReferenceTargets Include='Build' Targets='Build' />
<ProjectReference Include='{project2}' />
</ItemGroup>
<Target Name='Build'>
<MsBuild Projects='{project2}' Targets='Build' />
</Target>
</Project>
"));
File.WriteAllText(project2, CleanupFileContents($@"
<Project>
<ItemGroup>
<ProjectReferenceTargets Include='Build' Targets='Build' />
<ProjectReference Include='{project1}' />
</ItemGroup>
<Target Name='Build'>
<MsBuild Projects='{project1}' Targets='Build' />
</Target>
</Project>
"));
var data = new GraphBuildRequestData(new ProjectGraphEntryPoint(project1), Array.Empty<string>());
GraphBuildResult result = _buildManager.Build(_parameters, data);
result.OverallResult.ShouldBe(BuildResultCode.Failure);
result.CircularDependency.ShouldBeTrue();
}
[Fact]
public void GraphBuildShouldBeAbleToConstructGraphButSkipBuild()
{
var graph = Helpers.CreateProjectGraph(env: _env, dependencyEdges: new Dictionary<int, int[]> { { 1, new[] { 2, 3 } } });
MockLogger logger = null;
using (var buildSession = new Helpers.BuildManagerSession(_env))
{
var requestData = new GraphBuildRequestData(
projectGraphEntryPoints: new[] { new ProjectGraphEntryPoint(
graph.GraphRoots.First().ProjectInstance.FullPath,
new Dictionary<string, string>() { {"property1", "value1" } }) },
targetsToBuild: Array.Empty<string>(),
hostServices: null,
flags: BuildRequestDataFlags.None,
graphBuildOptions: new GraphBuildOptions { Build = false });
var graphResult = buildSession.BuildGraphSubmission(requestData);
graphResult.OverallResult.ShouldBe(BuildResultCode.Success);
logger = buildSession.Logger;
}
logger.EvaluationStartedEvents.Count.ShouldBe(3);
logger.ProjectStartedEvents.ShouldBeEmpty();
logger.TargetStartedEvents.ShouldBeEmpty();
logger.BuildStartedEvents.ShouldHaveSingleItem();
logger.BuildFinishedEvents.ShouldHaveSingleItem();
logger.FullLog.ShouldContain("Static graph loaded in");
logger.FullLog.ShouldContain("3 nodes, 2 edges");
}
/// <summary>
/// Helper task used by <see cref="TaskInputLoggingIsExposedToTasks"/> to verify <see cref="TaskLoggingHelper.IsTaskInputLoggingEnabled"/>.
/// </summary>
public class LogTaskInputsCheckingTask : Task
{
public bool ExpectedTaskInputLoggingEnabled { get; set; }
public override bool Execute()
{
return Log.IsTaskInputLoggingEnabled == ExpectedTaskInputLoggingEnabled;
}
}
[Theory]
[InlineData("", false)] // regular task host, input logging disabled
[InlineData("", true)] // regular task host, input logging enabled
[InlineData("TaskHostFactory", false)] // OOP task host, input logging disabled
[InlineData("TaskHostFactory", true)] // OOP task host, input logging enabled
public void TaskInputLoggingIsExposedToTasks(string taskFactory, bool taskInputLoggingEnabled)
{
string projectContents = ObjectModelHelpers.CleanupFileContents(@"<Project>
<UsingTask
TaskName=""" + typeof(LogTaskInputsCheckingTask).FullName + @"""
AssemblyFile=""" + Assembly.GetExecutingAssembly().Location + @"""
TaskFactory=""" + taskFactory + @"""
/>
<Target Name=""target1"">
<LogTaskInputsCheckingTask ExpectedTaskInputLoggingEnabled=""" + taskInputLoggingEnabled + @""" />
</Target>
</Project>");
_parameters.LogTaskInputs = taskInputLoggingEnabled;
Project project = CreateProject(projectContents, MSBuildDefaultToolsVersion, _projectCollection, true);
ProjectInstance instance = _buildManager.GetProjectInstanceForBuild(project);
_buildManager.BeginBuild(_parameters);
BuildResult result = _buildManager.BuildRequest(new BuildRequestData(instance, new[] { "target1" }));
_buildManager.EndBuild();
Assert.Equal(BuildResultCode.Success, result.OverallResult);
}
[Fact]
public void ProjectWithNoTargets()
{
string contents = @"<Project />";
BuildRequestData data = GetBuildRequestData(contents);
BuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertLogContains("MSB4040");
}
[Fact]
public void ProjectWithNoTargetsGraph()
{
string contents = @"<Project />";
GraphBuildRequestData data = GetGraphBuildRequestData(contents);
GraphBuildResult result = _buildManager.Build(_parameters, data);
Assert.Equal(BuildResultCode.Failure, result.OverallResult);
_logger.AssertLogContains("MSB4040");
}
}
}
|