Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Build.Framework;
using Xunit;

namespace Microsoft.NET.Build.Tasks.UnitTests
{
public class GivenACreateComHostMultiThreading
{
[Fact]
public void Paths_AreResolvedRelativeToProjectDirectory()
{
// Create a unique temp directory to act as the project directory
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"comhost-mt-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
try
{
// Create relative path structure under the project dir
var sourceDir = Path.Combine(projectDir, "source");
var destDir = Path.Combine(projectDir, "output");
Directory.CreateDirectory(sourceDir);
Directory.CreateDirectory(destDir);

// Create a fake comhost source file and clsid map
File.WriteAllText(Path.Combine(sourceDir, "comhost.dll"), "fake-source");
File.WriteAllText(Path.Combine(sourceDir, "clsidmap.bin"), "fake-clsid");

var task = new CreateComHost
{
BuildEngine = new MockBuildEngine(),
ComHostSourcePath = "source\\comhost.dll",
ComHostDestinationPath = "output\\comhost.dll",
ClsidMapPath = "source\\clsidmap.bin",
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test hard-codes Windows path separators (e.g., source\\comhost.dll). On non-Windows agents this will be treated as a literal backslash in the filename, so the created files won’t be found and the test can fail/flap. Prefer Path.Combine(...) (or Path.Join) for ItemSpec strings in tests to keep them cross-platform.

Suggested change
ComHostSourcePath = "source\\comhost.dll",
ComHostDestinationPath = "output\\comhost.dll",
ClsidMapPath = "source\\clsidmap.bin",
ComHostSourcePath = Path.Combine("source", "comhost.dll"),
ComHostDestinationPath = Path.Combine("output", "comhost.dll"),
ClsidMapPath = Path.Combine("source", "clsidmap.bin"),

Copilot uses AI. Check for mistakes.
};

// Set TaskEnvironment for path resolution
task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir);

// ComHost.Create will throw because our fake files aren't valid PE binaries.
// The key assertion is that the exception comes from PE processing (ResourceUpdater),
// NOT from "file not found" — proving paths were resolved via TaskEnvironment.
try
{
task.Execute();
}
catch (Exception)
{
// Expected — ComHost.Create fails on fake binaries
}

// Verify that any errors logged are NOT about missing files
var engine = (MockBuildEngine)task.BuildEngine;
var errors = engine.Errors.Select(e => e.Message).ToList();
errors.Should().NotContain(e => e.Contains("not found", StringComparison.OrdinalIgnoreCase)
&& e.Contains("comhost", StringComparison.OrdinalIgnoreCase),
"paths should be resolved via TaskEnvironment, not CWD");
}
finally
{
Directory.Delete(projectDir, true);
}
}

[Fact]
public void ItProducesSameErrorsInMultiProcessAndMultiThreadedEnvironments()
{
var projectDir = Path.Combine(Path.GetTempPath(), "comhost-test-" + Guid.NewGuid().ToString("N"));
var otherDir = Path.Combine(Path.GetTempPath(), "comhost-decoy-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(projectDir);
Directory.CreateDirectory(otherDir);
var savedCwd = Directory.GetCurrentDirectory();
try
{
// Create fake comhost and clsidmap under projectDir
var sourceDir = Path.Combine(projectDir, "source");
var outputDir = Path.Combine(projectDir, "output");
Directory.CreateDirectory(sourceDir);
Directory.CreateDirectory(outputDir);
File.WriteAllText(Path.Combine(sourceDir, "comhost.dll"), "fake-comhost-pe");
File.WriteAllText(Path.Combine(sourceDir, "clsidmap.bin"), "fake-clsid");

var comHostSource = Path.Combine("source", "comhost.dll");
var comHostDest = Path.Combine("output", "comhost.dll");
var clsidMap = Path.Combine("source", "clsidmap.bin");

// --- Multiprocess mode: CWD == projectDir ---
Directory.SetCurrentDirectory(projectDir);
var (result1, engine1, ex1) = RunTask(comHostSource, comHostDest, clsidMap, projectDir);

// --- Multithreaded mode: CWD == otherDir ---
Directory.SetCurrentDirectory(otherDir);
var (result2, engine2, ex2) = RunTask(comHostSource, comHostDest, clsidMap, projectDir);

// Both should produce the same result
result1.Should().Be(result2,
"task should return the same success/failure in both environments");
(ex1 == null).Should().Be(ex2 == null,
"both environments should agree on whether an exception is thrown");
if (ex1 != null && ex2 != null)
{
ex1.GetType().Should().Be(ex2.GetType(),
"exception type should be identical in both environments");
}
engine1.Errors.Count.Should().Be(engine2.Errors.Count,
"error count should be the same in both environments");
for (int i = 0; i < engine1.Errors.Count; i++)
{
engine1.Errors[i].Message.Should().Be(
engine2.Errors[i].Message,
$"error message [{i}] should be identical in both environments");
}
engine1.Warnings.Count.Should().Be(engine2.Warnings.Count,
"warning count should be the same in both environments");
}
finally
{
Directory.SetCurrentDirectory(savedCwd);
if (Directory.Exists(projectDir))
Directory.Delete(projectDir, true);
if (Directory.Exists(otherDir))
Directory.Delete(otherDir, true);
}
}

[Theory]
[InlineData(4)]
[InlineData(16)]
public void CreateComHost_ConcurrentExecution(int parallelism)
{
// These tasks work with PE binaries. With fake inputs they will throw/fail,
// but the concurrent execution should not produce different failure modes
// (no shared-state corruption, no deadlocks, no data races).
var results = new ConcurrentBag<(bool success, string exType)>();
var barrier = new Barrier(parallelism);

Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
var projectDir = Path.Combine(Path.GetTempPath(), $"comhost-conc-{i}-{Guid.NewGuid():N}");
Directory.CreateDirectory(projectDir);
try
{
var sourceDir = Path.Combine(projectDir, "source");
var outputDir = Path.Combine(projectDir, "output");
Directory.CreateDirectory(sourceDir);
Directory.CreateDirectory(outputDir);
File.WriteAllText(Path.Combine(sourceDir, "comhost.dll"), "fake-comhost-pe");
File.WriteAllText(Path.Combine(sourceDir, "clsidmap.bin"), "fake-clsid");

var task = new CreateComHost
{
BuildEngine = new MockBuildEngine(),
ComHostSourcePath = Path.Combine("source", "comhost.dll"),
ComHostDestinationPath = Path.Combine("output", "comhost.dll"),
ClsidMapPath = Path.Combine("source", "clsidmap.bin"),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};

barrier.SignalAndWait();
var result = task.Execute();
results.Add((result, "none"));
}
catch (Exception ex)
{
results.Add((false, ex.GetType().Name));
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Barrier.SignalAndWait() is used without a timeout; if any worker throws before reaching the barrier (e.g., due to I/O issues), the remaining workers can deadlock and hang the test run. Add a timeout (and fail when it expires) or switch to a non-blocking coordination primitive that can’t deadlock on early failure.

Copilot uses AI. Check for mistakes.
finally
{
if (Directory.Exists(projectDir))
Directory.Delete(projectDir, true);
}
});

// All threads should get the same outcome (all succeed or all fail the same way)
results.Select(r => r.exType).Distinct().Should().HaveCount(1,
"all threads should experience the same failure mode");
}

private static (bool result, MockBuildEngine engine, Exception? exception) RunTask(
string comHostSource, string comHostDest, string clsidMap, string projectDir)
{
var engine = new MockBuildEngine();
var task = new CreateComHost
{
BuildEngine = engine,
ComHostSourcePath = comHostSource,
ComHostDestinationPath = comHostDest,
ClsidMapPath = clsidMap,
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};

try
{
var result = task.Execute();
return (result, engine, null);
}
catch (Exception ex)
{
return (false, engine, ex);
}
}
}
}
Loading
Loading