Skip to content
Closed
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
884 changes: 884 additions & 0 deletions src/Build.UnitTests/BackEnd/BuildCoordinator_Tests.cs

Large diffs are not rendered by default.

245 changes: 245 additions & 0 deletions src/Build.UnitTests/BackEnd/HashBasedPipeNaming_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// 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 Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;

#nullable disable

namespace Microsoft.Build.UnitTests
{
/// <summary>
/// Tests for hash-based pipe naming in NamedPipeUtil and Handshake.ComputeHash.
/// </summary>
public class HashBasedPipeNaming_Tests
{
#region ComputeHash Tests

[Fact]
public void ComputeHash_ReturnsDeterministicValue()
{
var handshake = new Handshake(HandshakeOptions.NodeReuse);
string hash1 = handshake.ComputeHash();
string hash2 = handshake.ComputeHash();

hash1.ShouldNotBeNullOrEmpty();
hash2.ShouldNotBeNullOrEmpty();
hash1.ShouldBe(hash2);
}

[Fact]
public void ComputeHash_SameOptionsSameHash()
{
var h1 = new Handshake(HandshakeOptions.NodeReuse);
var h2 = new Handshake(HandshakeOptions.NodeReuse);

h1.ComputeHash().ShouldBe(h2.ComputeHash());
}

[Fact]
public void ComputeHash_DifferentOptionsYieldDifferentHash()
{
var h1 = new Handshake(HandshakeOptions.NodeReuse);
var h2 = new Handshake(HandshakeOptions.None);

h1.ComputeHash().ShouldNotBe(h2.ComputeHash());
}

[Fact]
public void ComputeHash_NoPaddingOrSlashes()
{
var handshake = new Handshake(HandshakeOptions.NodeReuse);
string hash = handshake.ComputeHash();

// Hash should be URL/filename-safe: no / or = characters
hash.ShouldNotContain("/");
hash.ShouldNotContain("=");
}

[Fact]
public void ComputeHash_IsCached()
{
var handshake = new Handshake(HandshakeOptions.NodeReuse);
string hash1 = handshake.ComputeHash();
string hash2 = handshake.ComputeHash();

// Should be the exact same object reference (cached)
ReferenceEquals(hash1, hash2).ShouldBeTrue();
}

#endregion

#region GetHashBasedPipeName Tests

[Fact]
public void GetHashBasedPipeName_ContainsHashAndPid()
{
string hash = "abc123";
int pid = 42;
string pipeName = NamedPipeUtil.GetHashBasedPipeName(hash, pid);

pipeName.ShouldContain("MSBuild-abc123-42");
}

[Fact]
public void GetHashBasedPipeName_DefaultsToCurrentPid()
{
string hash = "testhash";
string pipeName = NamedPipeUtil.GetHashBasedPipeName(hash);

int currentPid = EnvironmentUtilities.CurrentProcessId;
pipeName.ShouldContain($"MSBuild-testhash-{currentPid}");
}

[Fact]
public void GetHashBasedPipeName_OnUnix_IsAbsolutePath()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}

string pipeName = NamedPipeUtil.GetHashBasedPipeName("hash", 123);
pipeName.ShouldStartWith("/tmp/");
}

#endregion

#region FindNodesByHandshakeHash Tests

[Fact]
public void FindNodesByHandshakeHash_ReturnsEmptyOnWindows()
{
if (NativeMethodsShared.IsUnixLike)
{
return; // Only test on Windows
}

var pids = NamedPipeUtil.FindNodesByHandshakeHash("nonexistent");
pids.ShouldBeEmpty();
}

[Fact]
public void FindNodesByHandshakeHash_FindsMatchingPipeFiles()
{
if (!NativeMethodsShared.IsUnixLike)
{
return; // Only works on Unix where pipes are files
}

string testHash = $"test-{Guid.NewGuid():N}";

// Create fake pipe files in /tmp
string pipe1 = $"/tmp/MSBuild-{testHash}-1001";
string pipe2 = $"/tmp/MSBuild-{testHash}-1002";
string pipeOther = $"/tmp/MSBuild-otherhash-9999";

try
{
File.WriteAllText(pipe1, "");
File.WriteAllText(pipe2, "");
File.WriteAllText(pipeOther, "");

var pids = NamedPipeUtil.FindNodesByHandshakeHash(testHash);

pids.ShouldContain(1001);
pids.ShouldContain(1002);
pids.ShouldNotContain(9999);
}
finally
{
File.Delete(pipe1);
File.Delete(pipe2);
File.Delete(pipeOther);
}
}

[Fact]
public void FindNodesByHandshakeHash_IgnoresMalformedFileNames()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}

string testHash = $"test-{Guid.NewGuid():N}";
string pipeGood = $"/tmp/MSBuild-{testHash}-5555";
string pipeBad = $"/tmp/MSBuild-{testHash}-notanumber";

try
{
File.WriteAllText(pipeGood, "");
File.WriteAllText(pipeBad, "");

var pids = NamedPipeUtil.FindNodesByHandshakeHash(testHash);

pids.ShouldContain(5555);
pids.Count.ShouldBe(1);
}
finally
{
File.Delete(pipeGood);
File.Delete(pipeBad);
}
}

[Fact]
public void FindNodesByHandshakeHash_ReturnsEmptyWhenNoMatches()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}

var pids = NamedPipeUtil.FindNodesByHandshakeHash($"nopipes-{Guid.NewGuid():N}");
pids.ShouldBeEmpty();
}

#endregion

#region SessionId Fix Tests

[Fact]
public void Handshake_OnUnix_SessionIdIsZero()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}

// Two handshakes created from different contexts should have the same
// session ID (0) on Unix, enabling cross-terminal node reuse.
var h1 = new Handshake(HandshakeOptions.NodeReuse);
var h2 = new Handshake(HandshakeOptions.NodeReuse);

// Same handshake key means same session ID was used
h1.GetKey().ShouldBe(h2.GetKey());
}

[Fact]
public void Handshake_SessionIdComponent_IsZeroOnUnix()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}

var handshake = new Handshake(HandshakeOptions.NodeReuse);
var components = handshake.RetrieveHandshakeComponents();

// SessionId should be 0 on Unix (may be transformed by AvoidEndOfHandshakeSignal)
// The key representation includes the raw session id
string key = handshake.GetKey();
// Key format: "options salt major minor build private sessionId"
// Last component should be 0
string[] keyParts = key.Split(' ');
keyParts[keyParts.Length - 1].ShouldBe("0");
}

#endregion
}
}
Loading
Loading