Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions MSBuild.sln
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.BuildCheck.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Templates", "template_feed\Microsoft.Build.Templates.csproj", "{A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Build.CommandLine.EndToEnd.Tests", "src\MSBuild.EndToEnd.Tests\Microsoft.Build.CommandLine.EndToEnd.Tests.csproj", "{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -862,6 +864,30 @@ Global
{A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}.Release|x64.Build.0 = Release|Any CPU
{A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}.Release|x86.ActiveCfg = Release|Any CPU
{A86EE74A-AEF0-42ED-A5A7-7A54BC0773D8}.Release|x86.Build.0 = Release|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|ARM64.ActiveCfg = Debug|arm64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|ARM64.Build.0 = Debug|arm64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x64.ActiveCfg = Debug|x64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x64.Build.0 = Debug|x64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x86.ActiveCfg = Debug|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Debug|x86.Build.0 = Debug|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|ARM64.ActiveCfg = MachineIndependent|arm64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|ARM64.Build.0 = MachineIndependent|arm64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x64.Build.0 = MachineIndependent|x64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|Any CPU.Build.0 = Release|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|ARM64.ActiveCfg = Release|arm64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|ARM64.Build.0 = Release|arm64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x64.ActiveCfg = Release|x64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x64.Build.0 = Release|x64
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x86.ActiveCfg = Release|Any CPU
{29F9B7E2-F4BE-1C61-B8B5-427BA6463E8F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
21 changes: 3 additions & 18 deletions src/BuildCheck.UnitTests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ private EmbedResourceTestOutput RunEmbeddedResourceTest(string resourceXmlToAdd,
const string templateToReplace = "###EmbeddedResourceToAdd";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);

CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
ReplaceStringInFile(Path.Combine(workFolder.Path, referencedProjectName, $"{referencedProjectName}.csproj"),
templateToReplace, resourceXmlToAdd);
File.Copy(
Expand Down Expand Up @@ -198,21 +198,6 @@ void ReplaceStringInFile(string filePath, string original, string replacement)
}
}

private static void CopyFilesRecursively(string sourcePath, string targetPath)
{
// First Create all directories
foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath));
}

// Then copy all the files & Replaces any files with the same name
foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories))
{
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
}
}

private static int GetWarningsCount(string output)
{
Regex regex = new Regex(@"(\d+) Warning\(s\)");
Expand Down Expand Up @@ -272,7 +257,7 @@ public void CopyToOutputTest(bool skipUnchangedDuringCopy)
const string entryProjectName = "EntryProject";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);

CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);

_env.SetCurrentDirectory(Path.Combine(workFolder.Path, entryProjectName));

Expand Down Expand Up @@ -377,7 +362,7 @@ public void TFMConfusionCheckTest(string tfmString, string cliSuffix, bool shoul
const string templateToReplace = "###TFM";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);

CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
FileSystemUtilities.CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
ReplaceStringInFile(Path.Combine(workFolder.Path, $"{projectName}.csproj"),
templateToReplace, tfmString);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(RuntimeOutputTargetFrameworks)</TargetFrameworks>
<PlatformTarget>$(RuntimeOutputPlatformTarget)</PlatformTarget>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />

<PackageReference Include="Shouldly" />
<PackageReference Include="Verify.Xunit" />
<PackageReference Include="Microsoft.IO.Redist" Condition="'$(FeatureMSIORedist)' == 'true'" />
</ItemGroup>

<ItemGroup>
<Reference Include="System.IO.Compression" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MSBuild\MSBuild.csproj" />
<ProjectReference Include="..\UnitTests.Shared\Microsoft.Build.UnitTests.Shared.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Remove="TestAssets\**\*.cs" />
<None Include="TestAssets\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
149 changes: 149 additions & 0 deletions src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;
using Xunit.Abstractions;

#nullable disable

namespace Microsoft.Build.EndToEndTests
{
/// <summary>
/// Tests for multithreaded MSBuild execution scenarios using test assets.
/// </summary>
public class MultithreadedExecution_Tests : IClassFixture<TestSolutionAssetsFixture>, IDisposable
{
private readonly ITestOutputHelper _output;
private readonly TestEnvironment _env;
private readonly string _testAssetDir;

private readonly int _timeoutInMilliseconds = 60_000;

// Common parameters for all multithreaded tests:
// /nodereuse:false - Prevents MSBuild server processes from persisting between tests,
// ensuring proper test isolation and avoiding potential timeouts
// /v:minimal - Reduces log verbosity for cleaner test output and better performance
private const string CommonMSBuildArgs = "/nodereuse:false /v:minimal";

public MultithreadedExecution_Tests(ITestOutputHelper output, TestSolutionAssetsFixture testAssetFixture)
{
_output = output;
_env = TestEnvironment.Create(output);
_testAssetDir = testAssetFixture.TestAssetDir;
}

public void Dispose()
{
_env.Dispose();
}

/// <summary>
/// Prepares an isolated copy of test assets in a temporary directory for each test run.
/// This ensures fresh builds and proper test isolation.
/// </summary>
/// <param name="testAsset">Test asset</param>
/// <returns>TestSolutionAsset for the copied asset in a temporary folder.</returns>
private TestSolutionAsset PrepareIsolatedTestAssets(TestSolutionAsset testAsset)
{
string sourceAssetDir = Path.Combine(_testAssetDir, testAsset.SolutionFolder);

// Ensure source test asset exists
Directory.Exists(sourceAssetDir).ShouldBeTrue($"Test asset not found: {sourceAssetDir}.");

// Create isolated copy of entire test asset directory structure
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);

FileSystemUtilities.CopyFilesRecursively(sourceAssetDir, workFolder.Path);

// Return TestSolutionAsset with temp folder and project file
return new TestSolutionAsset(workFolder.Path, testAsset.ProjectRelativePath);
}

/// <summary>
/// Helper method to resolve TestSolutionAsset instances by name.
/// This is the easiest way to work around the limitation that [InlineData] cannot pass complex objects like TestSolutionAsset directly.
/// </summary>
private static TestSolutionAsset GetTestAssetByName(string testAssetName)
{
return testAssetName switch
{
nameof(TestSolutionAssetsFixture.SingleProject) => TestSolutionAssetsFixture.SingleProject,
nameof(TestSolutionAssetsFixture.ProjectWithDependencies) => TestSolutionAssetsFixture.ProjectWithDependencies,
_ => throw new ArgumentException($"Unknown test asset name: {testAssetName}", nameof(testAssetName))
};
}

/// <summary>
/// Tests building projects with various multithreading flags.
/// </summary>
[Theory]
[InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:1 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:1 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:2 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")]
public void MultithreadedBuild_Success(string testAssetName, string multithreadingArgs)
{
// Resolve TestSolutionAsset from name
TestSolutionAsset testAsset = GetTestAssetByName(testAssetName);

// Prepare isolated copy of test assets to ensure fresh builds
TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset);

string output = RunnerUtilities.ExecBootstrapedMSBuild(
$"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} {CommonMSBuildArgs}",
out bool success,
timeoutMilliseconds: _timeoutInMilliseconds);

success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}");

_output.WriteLine($"Built {testAsset.SolutionFolder} with arguments {multithreadingArgs}.");
}

/// <summary>
/// Tests binary logging with multithreaded builds and verifies replay functionality.
/// </summary>
[Theory]
[InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")]
public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs)
{
// Resolve TestSolutionAsset from name
TestSolutionAsset testAsset = GetTestAssetByName(testAssetName);

// Prepare isolated copy of test assets to ensure fresh builds
TestSolutionAsset isolatedAsset = PrepareIsolatedTestAssets(testAsset);

string binlogPath = Path.Combine(isolatedAsset.SolutionFolder, "build.binlog");

// Build with binary logging
string output = RunnerUtilities.ExecBootstrapedMSBuild(
$"\"{isolatedAsset.ProjectPath}\" {multithreadingArgs} /bl:\"{binlogPath}\" {CommonMSBuildArgs}",
out bool success,
timeoutMilliseconds: _timeoutInMilliseconds);

success.ShouldBeTrue($"Build failed with args '{multithreadingArgs}' for {testAsset.SolutionFolder}. Output:\\n{output}.");

// Verify binary log was created and has content
File.Exists(binlogPath).ShouldBeTrue("Binary log file was not created.");

// Test binlog replay
string replayOutput = RunnerUtilities.ExecBootstrapedMSBuild(
$"\"{binlogPath}\" {CommonMSBuildArgs}",
out bool replaySuccess,
timeoutMilliseconds: _timeoutInMilliseconds);

replaySuccess.ShouldBeTrue($"Binlog replay failed. Output:\\n{replayOutput}");

_output.WriteLine($"Built and replayed {testAsset.SolutionFolder} with arguments {multithreadingArgs}.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Library1\Library1.csproj" />
<ProjectReference Include="..\Library2\Library2.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Library1
{
public class Class1
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Library3\Library3.csproj" />
<ProjectReference Include="..\Library4\Library4.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Library2
{
public class Class2
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Library3\Library3.csproj" />
<ProjectReference Include="..\Library4\Library4.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Library3
{
public class Class3
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Library3
{
public class Class3
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
12 changes: 12 additions & 0 deletions src/MSBuild.EndToEnd.Tests/TestAssets/SingleProject/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Loading
Loading