Skip to content
Open
292 changes: 292 additions & 0 deletions src/Build.UnitTests/BackEnd/RepoLocalSdkResolver_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
// 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.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;

using RepoLocalSdkResolver = Microsoft.Build.BackEnd.SdkResolution.RepoLocalSdkResolver;
using DefaultSdkResolver = Microsoft.Build.BackEnd.SdkResolution.DefaultSdkResolver;
using SdkResultFactory = Microsoft.Build.BackEnd.SdkResolution.SdkResultFactory;

#nullable disable

namespace Microsoft.Build.Engine.UnitTests.BackEnd
{
public class RepoLocalSdkResolver_Tests : IDisposable
{
private readonly string _testRoot;

public RepoLocalSdkResolver_Tests()
{
_testRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_testRoot);
}

public void Dispose()
{
if (Directory.Exists(_testRoot))
{
FileUtilities.DeleteDirectoryNoThrow(_testRoot, recursive: true);
}
}

[Theory]
[InlineData(".git")]
[InlineData(".hg")]
[InlineData(".svn")]
public void ResolveSdkFromRepoLocalDirectory(string repoMarker)
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, repoMarker));

string sdkName = "My.Test.Sdk";
string sdkPath = Path.Combine(repoRoot, ".msbuild", "Sdk", sdkName, "Sdk");
Directory.CreateDirectory(sdkPath);

// Create Sdk.props and Sdk.targets to make it a valid SDK
File.WriteAllText(Path.Combine(sdkPath, "Sdk.props"), "<Project />");
File.WriteAllText(Path.Combine(sdkPath, "Sdk.targets"), "<Project />");

string projectPath = Path.Combine(repoRoot, "src", "Project.csproj");
Directory.CreateDirectory(Path.GetDirectoryName(projectPath));
File.WriteAllText(projectPath, "<Project />");

var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeTrue();
result.Path.ShouldBe(sdkPath);
}

[Fact]
public void DoesNotResolveSdkWhenNoRepoMarkerFound()
{
// Arrange
string projectDir = Path.Combine(_testRoot, "project");
Directory.CreateDirectory(projectDir);

string projectPath = Path.Combine(projectDir, "Project.csproj");
File.WriteAllText(projectPath, "<Project />");

string sdkName = "My.Test.Sdk";
var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeFalse();
}

[Fact]
public void DoesNotResolveSdkWhenSdkDirectoryDoesNotExist()
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, ".git"));

string projectPath = Path.Combine(repoRoot, "src", "Project.csproj");
Directory.CreateDirectory(Path.GetDirectoryName(projectPath));
File.WriteAllText(projectPath, "<Project />");

string sdkName = "My.Test.Sdk";
var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeFalse();
}

[Fact]
public void FailsWhenSdkDirectoryExistsButNoPropsOrTargets()
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, ".git"));

string sdkName = "My.Test.Sdk";
string sdkPath = Path.Combine(repoRoot, ".msbuild", "Sdk", sdkName, "Sdk");
Directory.CreateDirectory(sdkPath);
// Don't create Sdk.props or Sdk.targets

string projectPath = Path.Combine(repoRoot, "src", "Project.csproj");
Directory.CreateDirectory(Path.GetDirectoryName(projectPath));
File.WriteAllText(projectPath, "<Project />");

var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeFalse();
}

[Fact]
public void SucceedsWithOnlyPropsFile()
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, ".git"));

string sdkName = "My.Test.Sdk";
string sdkPath = Path.Combine(repoRoot, ".msbuild", "Sdk", sdkName, "Sdk");
Directory.CreateDirectory(sdkPath);

// Create only Sdk.props
File.WriteAllText(Path.Combine(sdkPath, "Sdk.props"), "<Project />");

string projectPath = Path.Combine(repoRoot, "src", "Project.csproj");
Directory.CreateDirectory(Path.GetDirectoryName(projectPath));
File.WriteAllText(projectPath, "<Project />");

var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeTrue();
result.Path.ShouldBe(sdkPath);
}

[Fact]
public void SucceedsWithOnlyTargetsFile()
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, ".git"));

string sdkName = "My.Test.Sdk";
string sdkPath = Path.Combine(repoRoot, ".msbuild", "Sdk", sdkName, "Sdk");
Directory.CreateDirectory(sdkPath);

// Create only Sdk.targets
File.WriteAllText(Path.Combine(sdkPath, "Sdk.targets"), "<Project />");

string projectPath = Path.Combine(repoRoot, "src", "Project.csproj");
Directory.CreateDirectory(Path.GetDirectoryName(projectPath));
File.WriteAllText(projectPath, "<Project />");

var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeTrue();
result.Path.ShouldBe(sdkPath);
}

[Fact]
public void FindsRepoRootMultipleLevelsUp()
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, ".git"));

string sdkName = "My.Test.Sdk";
string sdkPath = Path.Combine(repoRoot, ".msbuild", "Sdk", sdkName, "Sdk");
Directory.CreateDirectory(sdkPath);

File.WriteAllText(Path.Combine(sdkPath, "Sdk.props"), "<Project />");
File.WriteAllText(Path.Combine(sdkPath, "Sdk.targets"), "<Project />");

// Create project several levels deep
string projectPath = Path.Combine(repoRoot, "src", "subfolder", "nested", "Project.csproj");
Directory.CreateDirectory(Path.GetDirectoryName(projectPath));
File.WriteAllText(projectPath, "<Project />");

var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(projectPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeTrue();
result.Path.ShouldBe(sdkPath);
}

[Fact]
public void UsesSolutionPathWhenProjectPathIsNull()
{
// Arrange
string repoRoot = Path.Combine(_testRoot, "repo");
Directory.CreateDirectory(repoRoot);
Directory.CreateDirectory(Path.Combine(repoRoot, ".git"));

string sdkName = "My.Test.Sdk";
string sdkPath = Path.Combine(repoRoot, ".msbuild", "Sdk", sdkName, "Sdk");
Directory.CreateDirectory(sdkPath);

File.WriteAllText(Path.Combine(sdkPath, "Sdk.props"), "<Project />");
File.WriteAllText(Path.Combine(sdkPath, "Sdk.targets"), "<Project />");

string solutionPath = Path.Combine(repoRoot, "MySolution.sln");
File.WriteAllText(solutionPath, "");

var resolver = new RepoLocalSdkResolver();
var context = new MockSdkResolverContext(null, solutionPath);
var factory = new SdkResultFactory(new SdkReference(sdkName, null, null));

// Act
var result = resolver.Resolve(new SdkReference(sdkName, null, null), context, factory);

// Assert
result.Success.ShouldBeTrue();
result.Path.ShouldBe(sdkPath);
}

[Fact]
public void ResolverHasHigherPriorityThanDefault()
{
// This test verifies that RepoLocalSdkResolver has a higher priority (lower number)
// than DefaultSdkResolver so that repo-local SDKs take precedence
var repoLocalResolver = new RepoLocalSdkResolver();
var defaultResolver = new DefaultSdkResolver();

repoLocalResolver.Priority.ShouldBeLessThan(defaultResolver.Priority);
}

private sealed class MockSdkResolverContext : Microsoft.Build.Framework.SdkResolverContext
{
public MockSdkResolverContext(string projectFilePath, string solutionFilePath = null)
{
ProjectFilePath = projectFilePath;
SolutionFilePath = solutionFilePath;
}
}
}
}
2 changes: 1 addition & 1 deletion src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void AssertDefaultLoaderReturnsDefaultResolvers()

var resolvers = loader.LoadAllResolvers(new MockElementLocation("file"));

resolvers.Select(i => i.GetType().FullName).ShouldBe(new[] { typeof(DefaultSdkResolver).FullName });
resolvers.Select(i => i.GetType().FullName).ShouldBe(new[] { typeof(RepoLocalSdkResolver).FullName, typeof(DefaultSdkResolver).FullName });

_logger.ErrorCount.ShouldBe(0);
_logger.WarningCount.ShouldBe(0);
Expand Down
Loading