Skip to content
Merged
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
3 changes: 3 additions & 0 deletions MSBuild.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
<Project Path="src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj">
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj">
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj">
<Platform Solution="*|x64" Project="x64" />
</Project>
Expand Down
22 changes: 4 additions & 18 deletions src/BuildCheck.UnitTests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Microsoft.Build.UnitTests.Shared;
Expand Down Expand Up @@ -160,7 +161,7 @@ private EmbedResourceTestOutput RunEmbeddedResourceTest(string resourceXmlToAdd,
const string templateToReplace = "###EmbeddedResourceToAdd";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);

CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
ReplaceStringInFile(Path.Combine(workFolder.Path, referencedProjectName, $"{referencedProjectName}.csproj"),
templateToReplace, resourceXmlToAdd);
File.Copy(
Expand Down Expand Up @@ -196,21 +197,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 @@ -270,7 +256,7 @@ public void CopyToOutputTest(bool skipUnchangedDuringCopy)
const string entryProjectName = "EntryProject";
TransientTestFolder workFolder = _env.CreateFolder(createFolder: true);

CopyFilesRecursively(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);

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

Expand Down Expand Up @@ -382,7 +368,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);
FileUtilities.CopyDirectory(Path.Combine(TestAssetsRootPath, testAssetsFolderName), workFolder.Path);
ReplaceStringInFile(Path.Combine(workFolder.Path, $"{projectName}.csproj"),
templateToReplace, tfmString);

Expand Down
1 change: 1 addition & 0 deletions src/Framework/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
[assembly: InternalsVisibleTo("Microsoft.Build.Engine.OM.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.EndToEnd.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.UnitTests.Shared, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]

Expand Down
31 changes: 31 additions & 0 deletions src/MSBuild.EndToEnd.Tests/Microsoft.Build.EndToEnd.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
Comment thread
OvesN marked this conversation as resolved.

<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="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" />
<ProjectReference Include="..\Xunit.NetCore.Extensions\Xunit.NetCore.Extensions.csproj" />
</ItemGroup>

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

</Project>
190 changes: 190 additions & 0 deletions src/MSBuild.EndToEnd.Tests/MultithreadedExecution_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// 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;
Comment thread
OvesN marked this conversation as resolved.
using Microsoft.Build.Framework;
using Microsoft.Build.UnitTests;
using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;

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 = 180_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);

FileUtilities.CopyDirectory(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,
nameof(TestSolutionAssetsFixture.NonSdkSingleProject) => TestSolutionAssetsFixture.NonSdkSingleProject,
nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies) => TestSolutionAssetsFixture.NonSdkProjectWithDependencies,
_ => throw new ArgumentException($"Unknown test asset name: {testAssetName}", nameof(testAssetName))
};
}

/// <summary>
/// Builds a test asset with the given MSBuild args and verifies success.
/// </summary>
private void BuildAndVerify(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}.");
}
Comment thread
OvesN marked this conversation as resolved.

/// <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.SingleProject), "/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)
{
BuildAndVerify(testAssetName, multithreadingArgs);
}

Comment thread
OvesN marked this conversation as resolved.
/// <summary>
/// Builds a test asset with binary logging, then replays the binlog and verifies both succeed.
/// </summary>
private void BuildWithBinlogAndVerifyReplay(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.");
Comment thread
OvesN marked this conversation as resolved.
new FileInfo(binlogPath).Length.ShouldBeGreaterThan(0, "Binary log file was created but is empty.");

// 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}.");
}

/// <summary>
/// Tests binary logging with multithreaded builds and verifies replay functionality.
/// </summary>
[Theory]
[InlineData(nameof(TestSolutionAssetsFixture.SingleProject), "/m:8 /mt")]
Comment thread
OvesN marked this conversation as resolved.
[InlineData(nameof(TestSolutionAssetsFixture.ProjectWithDependencies), "/m:8 /mt")]
public void MultithreadedBuild_BinaryLogging(string testAssetName, string multithreadingArgs)
{
BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs);
}

/// <summary>
/// Tests building non-SDK-style projects with multithreading flags.
/// </summary>
[WindowsOnlyTheory]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:1 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/mt")]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:1 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:2 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")]
public void MultithreadedBuild_NonSdkStyle_Success(string testAssetName, string multithreadingArgs)
{
BuildAndVerify(testAssetName, multithreadingArgs);
}

/// <summary>
/// Tests binary logging with non-SDK-style multithreaded builds and verifies replay functionality.
/// </summary>
[WindowsOnlyTheory]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkSingleProject), "/m:8 /mt")]
[InlineData(nameof(TestSolutionAssetsFixture.NonSdkProjectWithDependencies), "/m:8 /mt")]
public void MultithreadedBuild_NonSdkStyle_BinaryLogging(string testAssetName, string multithreadingArgs)
{
BuildWithBinlogAndVerifyReplay(testAssetName, multithreadingArgs);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Library1\Library1.csproj" />
<ProjectReference Include="..\Library2\Library2.csproj" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using Library1;
using Library2;

namespace NonSdkConsoleApp
{
class Program
{
static void Main(string[] args)
{
var c1 = new Class1();
var c2 = new Class2();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Library1
{
public class Class1
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Library2
{
public class Class2
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="Class2.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace NonSdkSingleProject
{
class Program
{
static void Main(string[] args)
{
}
}
}
Loading
Loading