Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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,185 @@
// 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 Microsoft.Build.Utilities;
using Xunit;

namespace Microsoft.NET.Build.Tasks.UnitTests
{
public class GivenAPrepareForReadyToRunCompilationMultiThreading
{
[Fact]
public void NullInputs_DoNotCrash()
{
var engine = new MockBuildEngine();
var task = new PrepareForReadyToRunCompilation
{
BuildEngine = engine,
MainAssembly = new TaskItem("nonexistent.dll"),
OutputPath = "",
IncludeSymbolsInSingleFile = false,
Assemblies = null,
ReadyToRunUseCrossgen2 = false,
};

task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

// With null Assemblies and no crossgen tool, Execute should complete without NRE
var result = task.Execute();

// Task may log errors about missing crossgen tool, but must not throw NRE
result.Should().BeTrue("null Assemblies causes early return with no errors");
}

[Fact]
public void DiaSymReaderPath_IsResolvedRelativeToProjectDirectory()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"r2r-prepare-mt-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
try
{
// Create a fake DiaSymReader file at a relative path
var toolsDir = Path.Combine(projectDir, "tools");
Directory.CreateDirectory(toolsDir);
File.WriteAllText(Path.Combine(toolsDir, "diasymreader.dll"), "fake");

var crossgenTool = new TaskItem("tools\\crossgen.exe");
crossgenTool.SetMetadata("DiaSymReader", "tools\\diasymreader.dll");
crossgenTool.SetMetadata("JitPath", "tools\\clrjit.dll");

var task = new PrepareForReadyToRunCompilation
{
BuildEngine = new MockBuildEngine(),
MainAssembly = new TaskItem("test.dll"),
OutputPath = "output",
IncludeSymbolsInSingleFile = false,
Assemblies = null,
ReadyToRunUseCrossgen2 = false,
CrossgenTool = crossgenTool,
EmitSymbols = true,
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};

var result = task.Execute();

// With null Assemblies, ProcessInputFileList returns early.
// The key point is that DiaSymReader at "tools\diasymreader.dll"
// is resolved via TaskEnvironment relative to projectDir and found.
result.Should().BeTrue("null Assemblies produces no errors");
}
finally
{
Directory.Delete(projectDir, true);
}
}

[Fact]
public void DualModeParity_SameResultRegardlessOfCwd()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"r2r-prepare-dual-{Guid.NewGuid():N}"));
var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"r2r-prepare-other-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
Directory.CreateDirectory(otherDir);
try
{
// Create a fake DiaSymReader file under projectDir
var toolsDir = Path.Combine(projectDir, "tools");
Directory.CreateDirectory(toolsDir);
File.WriteAllText(Path.Combine(toolsDir, "diasymreader.dll"), "fake");

var taskEnv = TaskEnvironmentHelper.CreateForTest(projectDir);

// Run with CWD = projectDir
var savedCwd = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(projectDir);

var engine1 = new MockBuildEngine();
var crossgen1 = new TaskItem("tools\\crossgen.exe");
crossgen1.SetMetadata("DiaSymReader", "tools\\diasymreader.dll");
crossgen1.SetMetadata("JitPath", "tools\\clrjit.dll");

var task1 = new PrepareForReadyToRunCompilation
{
BuildEngine = engine1,
MainAssembly = new TaskItem("test.dll"),
OutputPath = "output",
IncludeSymbolsInSingleFile = false,
Assemblies = null,
ReadyToRunUseCrossgen2 = false,
CrossgenTool = crossgen1,
EmitSymbols = true,
TaskEnvironment = taskEnv,
};
var result1 = task1.Execute();

// Run with CWD = otherDir (different from projectDir)
Directory.SetCurrentDirectory(otherDir);

var engine2 = new MockBuildEngine();
var crossgen2 = new TaskItem("tools\\crossgen.exe");
crossgen2.SetMetadata("DiaSymReader", "tools\\diasymreader.dll");
crossgen2.SetMetadata("JitPath", "tools\\clrjit.dll");

var task2 = new PrepareForReadyToRunCompilation
{
BuildEngine = engine2,
MainAssembly = new TaskItem("test.dll"),
OutputPath = "output",
IncludeSymbolsInSingleFile = false,
Assemblies = null,
ReadyToRunUseCrossgen2 = false,
CrossgenTool = crossgen2,
EmitSymbols = true,
TaskEnvironment = taskEnv,
};
var result2 = task2.Execute();

Directory.SetCurrentDirectory(savedCwd);

// Both runs should produce the same result regardless of CWD
result1.Should().Be(result2, "TaskEnvironment resolves paths independent of CWD");
engine1.Errors.Count.Should().Be(engine2.Errors.Count,
"same errors regardless of CWD");
}
finally
{
Directory.Delete(projectDir, true);
Directory.Delete(otherDir, true);
}
}

[Theory]
[InlineData(4)]
[InlineData(16)]
public void PrepareForReadyToRunCompilation_ConcurrentExecution(int parallelism)
{
var errors = new ConcurrentBag<string>();
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
{
var task = new PrepareForReadyToRunCompilation
{
BuildEngine = new MockBuildEngine(),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
MainAssembly = new TaskItem("nonexistent.dll"),
OutputPath = "",
IncludeSymbolsInSingleFile = false,
Assemblies = null,
ReadyToRunUseCrossgen2 = false,
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 Microsoft.Build.Utilities;
using Xunit;

namespace Microsoft.NET.Build.Tasks.UnitTests
{
public class GivenAResolveReadyToRunCompilersMultiThreading
{
[Fact]
public void EmptyRuntimePacks_LogsErrorGracefully()
{
var engine = new MockBuildEngine();
var task = new ResolveReadyToRunCompilers
{
BuildEngine = engine,
RuntimePacks = new ITaskItem[] { new TaskItem("SomePack") },
TargetingPacks = Array.Empty<ITaskItem>(),
RuntimeGraphPath = "nonexistent.json",
NETCoreSdkRuntimeIdentifier = "win-x64",
ReadyToRunUseCrossgen2 = false,
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};

// RuntimePacks has no matching NETCore.App pack, so it should log error gracefully
var result = task.Execute();

result.Should().BeFalse("no valid NETCore.App runtime pack exists");
engine.Errors.Should().NotBeEmpty("should log an error about missing runtime pack");
// Verify no NullReferenceException — error is about missing pack, not a crash
engine.Errors.Select(e => e.Message).Should().NotContain(
e => e.Contains("NullReference", StringComparison.OrdinalIgnoreCase),
"should not crash with NullReferenceException");
}

[Fact]
public void TaskEnvironmentProperty_IsWirable()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"resolve-r2r-mt-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
try
{
var task = new ResolveReadyToRunCompilers
{
BuildEngine = new MockBuildEngine(),
RuntimePacks = new ITaskItem[] { new TaskItem("SomePack") },
TargetingPacks = Array.Empty<ITaskItem>(),
RuntimeGraphPath = "runtime.json",
NETCoreSdkRuntimeIdentifier = "win-x64",
};

var teProp = task.GetType().GetProperty("TaskEnvironment");
teProp.Should().NotBeNull("task must have a TaskEnvironment property after migration");
teProp!.SetValue(task, TaskEnvironmentHelper.CreateForTest(projectDir));

// The task should have TaskEnvironment set
task.TaskEnvironment.Should().NotBeNull();
}
finally
{
Directory.Delete(projectDir, true);
}
}

[Theory]
[InlineData(4)]
[InlineData(16)]
public void ResolveReadyToRunCompilers_ConcurrentExecution(int parallelism)
{
var errors = new ConcurrentBag<string>();
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
{
var task = new ResolveReadyToRunCompilers
{
BuildEngine = new MockBuildEngine(),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
RuntimePacks = new ITaskItem[] { new TaskItem("SomePack") },
TargetingPacks = Array.Empty<ITaskItem>(),
RuntimeGraphPath = "nonexistent.json",
NETCoreSdkRuntimeIdentifier = "win-x64",
ReadyToRunUseCrossgen2 = false,
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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 Microsoft.Build.Utilities;
using Xunit;

namespace Microsoft.NET.Build.Tasks.UnitTests
{
public class GivenARunCsWinRTGeneratorMultiThreading
{
[Fact]
public void NullInputs_DoNotCrashWithNullReferenceException()
{
// RunCsWinRTGenerator extends ToolTask — full Execute() tries to launch
// an external process. We only verify that null/empty required properties
// don't cause NullReferenceException.
var engine = new MockBuildEngine();
var task = new RunCsWinRTGenerator
{
BuildEngine = engine,
ReferenceAssemblyPaths = null,
OutputAssemblyPath = null,
InteropAssemblyDirectory = null,
CsWinRTToolsDirectory = null,
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
};

// Execute will fail validation due to null required properties,
// but should not throw NullReferenceException
bool threw = false;
bool result = false;
try
{
result = task.Execute();
}
catch (NullReferenceException)
{
threw = true;
}

threw.Should().BeFalse("null properties should be handled gracefully, not throw NRE");
if (!result)
{
// Validation failed as expected — check warnings/errors are about missing inputs
var allMessages = engine.Warnings.Select(w => w.Message)
.Concat(engine.Errors.Select(e => e.Message))
.ToList();
allMessages.Should().NotBeEmpty("validation should produce diagnostic messages");
}
}

[Fact]
public void TaskEnvironmentProperty_IsWirable()
{
var task = new RunCsWinRTGenerator();

var teProp = task.GetType().GetProperty("TaskEnvironment");
teProp.Should().NotBeNull("task must have a TaskEnvironment property after migration");

var env = TaskEnvironmentHelper.CreateForTest();
teProp!.SetValue(task, env);
task.TaskEnvironment.Should().NotBeNull();
}

[Theory]
[InlineData(4)]
[InlineData(16)]
public void RunCsWinRTGenerator_ConcurrentExecution(int parallelism)
{
var errors = new ConcurrentBag<string>();
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
{
var task = new RunCsWinRTGenerator
{
BuildEngine = new MockBuildEngine(),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
ReferenceAssemblyPaths = null,
OutputAssemblyPath = null,
InteropAssemblyDirectory = null,
CsWinRTToolsDirectory = null,
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}
}
}
Loading
Loading