File: RuntimeHostInfoTests.cs
Web Access
Project: src\src\Compilers\Core\MSBuildTaskTests\Microsoft.Build.Tasks.CodeAnalysis.UnitTests.csproj (Microsoft.Build.Tasks.CodeAnalysis.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.CommandLine;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests;
 
public sealed class RuntimeHostInfoTests(ITestOutputHelper output) : TestBase
{
    private readonly ITestOutputHelper _output = output;
 
    /// <summary>
    /// Normalizes a path by resolving any symbolic links or subst drives (on Windows).
    /// This ensures paths can be compared even when one is a subst drive (e.g., T:\ -> D:\a\_work\1\s\artifacts\tmp\Debug).
    /// </summary>
    private static string NormalizePath(string path)
    {
        // Our test infra uses `subst` which the .NET Core's implementation of `ResolveLinkTarget` wouldn't resolve,
        // hence we always use our Win32 polyfill on Windows to ensure paths are fully normalized and can be compared in tests.
        var resolvedPath = PlatformInformation.IsWindows
            ? NativeMethods.ResolveLinkTargetWin32(path, returnFinalTarget: true)
            : File.ResolveLinkTarget(path, returnFinalTarget: true);
        if (resolvedPath != null)
        {
            return resolvedPath.FullName;
        }
 
        return path;
    }
 
    [Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")]
    public void DotNetInPath()
    {
        using var tempRoot = new TempRoot();
        var testDir = tempRoot.CreateDirectory();
        var globalDotNetDir = testDir.CreateDirectory("global-dotnet");
        var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}");
 
        var result = ApplyEnvironmentVariables(
        [
            new("PATH", globalDotNetDir.Path),
            new(RuntimeHostInfo.DotNetHostPathEnvironmentName, ""),
            new(RuntimeHostInfo.DotNetExperimentalHostPathEnvironmentName, ""),
        ],
        () => RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
 
        Assert.NotNull(result);
        AssertEx.Equal(NormalizePath(globalDotNetDir.Path), NormalizePath(result));
    }
 
    [Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")]
    public void DotNetInPath_None()
    {
        var result = ApplyEnvironmentVariables(
        [
            new("PATH", ""),
            new(RuntimeHostInfo.DotNetHostPathEnvironmentName, ""),
            new(RuntimeHostInfo.DotNetExperimentalHostPathEnvironmentName, ""),
        ],
        () => RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
 
        Assert.Null(result);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")]
    public void DotNetInPath_Symlinked()
    {
        using var tempRoot = new TempRoot();
        var testDir = tempRoot.CreateDirectory();
        var globalDotNetDir = testDir.CreateDirectory("global-dotnet");
        var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}");
        var binDir = testDir.CreateDirectory("bin");
        var symlinkPath = Path.Combine(binDir.Path, $"dotnet{PlatformInformation.ExeExtension}");
 
        // Create symlink from binDir to the actual dotnet executable
        File.CreateSymbolicLink(path: symlinkPath, pathToTarget: globalDotNetExe.Path);
 
        var result = ApplyEnvironmentVariables(
        [
            new("PATH", binDir.Path),
            new(RuntimeHostInfo.DotNetHostPathEnvironmentName, ""),
            new(RuntimeHostInfo.DotNetExperimentalHostPathEnvironmentName, ""),
        ],
        () => RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine));
 
        Assert.NotNull(result);
        AssertEx.Equal(NormalizePath(globalDotNetDir.Path), NormalizePath(result));
    }
}
 
#if !NET
file static class TestNativeMethods
{
    extension(File)
    {
        /// <remarks>
        /// Only used by tests currently (might need some hardening if this is to be used by production code).
        /// </remarks>
        public static void CreateSymbolicLink(string path, string pathToTarget)
        {
            bool ok = CreateSymbolicLink(
                lpSymlinkFileName: path,
                lpTargetFileName: pathToTarget,
                dwFlags: SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE);
            if (!ok)
            {
                throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
            }
        }
    }
 
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool CreateSymbolicLink(
        string lpSymlinkFileName,
        string lpTargetFileName,
        uint dwFlags);
 
    private const uint SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2;
}
#endif