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
66 changes: 66 additions & 0 deletions src/Build.UnitTests/TerminalLogger_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,72 @@ public async Task ProjectFinishedReportsTargetFrameworkAndRuntimeIdentifier()
await Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform();
}

[Fact]
public void ReplayBinaryLogWithFewerNodesThanOriginalBuild()
Comment thread
YuliiaKovalova marked this conversation as resolved.
{
// This test validates that replaying a binary log with terminal logger
// using fewer nodes than the original build does not cause an IndexOutOfRangeException.
// See issue: https://github.com/dotnet/msbuild/issues/10596

using (TestEnvironment env = TestEnvironment.Create())
{
// Create multiple projects that will build in parallel
TransientTestFolder logFolder = env.CreateFolder(createFolder: true);

// Create three simple projects
TransientTestFile project1 = env.CreateFile(logFolder, "project1.proj", @"
<Project>
<Target Name='Build'>
<Message Text='Building project1' Importance='High' />
</Target>
</Project>");

TransientTestFile project2 = env.CreateFile(logFolder, "project2.proj", @"
<Project>
<Target Name='Build'>
<Message Text='Building project2' Importance='High' />
</Target>
</Project>");

TransientTestFile project3 = env.CreateFile(logFolder, "project3.proj", @"
<Project>
<Target Name='Build'>
<Message Text='Building project3' Importance='High' />
</Target>
</Project>");

// Create a solution file that builds all projects in parallel
string solutionContents = $@"
<Project>
<Target Name='Build'>
<MSBuild Projects='{project1.Path};{project2.Path};{project3.Path}' BuildInParallel='true' />
</Target>
</Project>";
TransientTestFile solutionFile = env.CreateFile(logFolder, "solution.proj", solutionContents);

string binlogPath = env.ExpectFile(".binlog").Path;

// Build with multiple nodes to create a binlog with higher node IDs
RunnerUtilities.ExecMSBuild($"{solutionFile.Path} /m:4 /bl:{binlogPath}", out bool success, outputHelper: _outputHelper);
success.ShouldBeTrue();

// Replay the binlog with TerminalLogger using only 1 node
// This should NOT throw an IndexOutOfRangeException
var replayEventSource = new BinaryLogReplayEventSource();
using var outputWriter = new StringWriter();
using var mockTerminal = new Terminal(outputWriter);
var terminalLogger = new TerminalLogger(mockTerminal);

// Initialize with only 1 node (fewer than the original build)
terminalLogger.Initialize(replayEventSource, nodeCount: 1);

// This should complete without throwing an exception
Should.NotThrow(() => replayEventSource.Replay(binlogPath));

terminalLogger.Shutdown();
}
Comment thread
YuliiaKovalova marked this conversation as resolved.
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
35 changes: 34 additions & 1 deletion src/Build/Logging/TerminalLogger/TerminalLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ public EvalContext(BuildEventContext context)
/// </summary>
private ProjectContext? _restoreContext;

/// <summary>
/// True if we're replaying a binary log. In this mode, we may encounter NodeIds higher than the initial node count.
/// </summary>
private bool _isReplayMode = false;

/// <summary>
/// The thread that performs periodic refresh of the console output.
/// </summary>
Expand Down Expand Up @@ -432,6 +437,9 @@ public void Initialize(IEventSource eventSource)
{
ParseParameters();

// Detect if we're in replay mode
_isReplayMode = eventSource is IBinaryLogReplaySource;

eventSource.BuildStarted += BuildStarted;
eventSource.BuildFinished += BuildFinished;
eventSource.ProjectStarted += ProjectStarted;
Expand Down Expand Up @@ -732,6 +740,7 @@ private void ProjectStarted(object sender, ProjectStartedEventArgs e)
{
_restoreContext = c;
int nodeIndex = NodeIndexForContext(e.BuildEventContext);
EnsureNodeCapacity(nodeIndex);
Comment thread
YuliiaKovalova marked this conversation as resolved.
_nodes[nodeIndex] = new TerminalNodeStatus(e.ProjectFile!, targetFramework, runtimeIdentifier, "Restore", _projects[c].Stopwatch);
}
}
Expand Down Expand Up @@ -1053,9 +1062,31 @@ private void TargetStarted(object sender, TargetStartedEventArgs e)
private void UpdateNodeStatus(BuildEventContext buildEventContext, TerminalNodeStatus? nodeStatus)
{
int nodeIndex = NodeIndexForContext(buildEventContext);
EnsureNodeCapacity(nodeIndex);
_nodes[nodeIndex] = nodeStatus;
}

/// <summary>
/// Ensures that the <see cref="_nodes"/> array has enough capacity to accommodate the given index.
/// This is necessary for binary log replay scenarios where the replay may use fewer nodes than the original build.
/// </summary>
private void EnsureNodeCapacity(int nodeIndex)
{
// Only resize in replay mode - during normal builds, the node count is fixed
if (_isReplayMode && nodeIndex >= _nodes.Length)
{
// Resize to accommodate the new index plus some extra capacity
lock (_lock)
{
if (nodeIndex >= _nodes.Length)
{
int newSize = Math.Max(nodeIndex + 1, _nodes.Length * 2);
Array.Resize(ref _nodes, newSize);
}
}
}
}

/// <summary>
/// The <see cref="IEventSource.TargetFinished"/> callback. Unused.
/// </summary>
Expand Down Expand Up @@ -1191,7 +1222,9 @@ private void MessageRaised(object sender, BuildMessageEventArgs e)

if (hasProject && project!.IsTestProject)
{
TerminalNodeStatus? node = _nodes[NodeIndexForContext(buildEventContext)];
int nodeIndex = NodeIndexForContext(buildEventContext);
EnsureNodeCapacity(nodeIndex);
TerminalNodeStatus? node = _nodes[nodeIndex];

// Consumes test update messages produced by VSTest and MSTest runner.
if (e is IExtendedBuildEventArgs extendedMessage)
Expand Down