File: Utils\PathLookupHelperTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Aspire.Cli.Tests.Utils;
 
public class PathLookupHelperTests
{
    [Fact]
    public void FindFullPathFromPath_WhenCommandExistsOnPath_ReturnsFullPath()
    {
        // Arrange
        var existingFiles = new HashSet<string>
        {
            Path.Combine("/usr/bin", "mycommand")
        };
 
        // Act
        var result = PathLookupHelper.FindFullPathFromPath("mycommand", "/usr/bin:/usr/local/bin", ':', existingFiles.Contains, null);
 
        // Assert
        Assert.Equal(Path.Combine("/usr/bin", "mycommand"), result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WhenCommandNotOnPath_ReturnsNull()
    {
        // Arrange
        static bool AlwaysFalse(string _) => false;
 
        // Act
        var result = PathLookupHelper.FindFullPathFromPath("mycommand", "/usr/bin:/usr/local/bin", ':', AlwaysFalse, null);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WhenPathVariableIsEmpty_ReturnsNull()
    {
        // Arrange - when path is empty, no files should exist on that (non-existent) path
        static bool AlwaysFalse(string _) => false;
 
        // Act
        var result = PathLookupHelper.FindFullPathFromPath("mycommand", "", ':', AlwaysFalse, null);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WhenPathVariableIsNull_ReturnsNull()
    {
        // Arrange
        static bool AlwaysFalse(string _) => false;
 
        // Act
        var result = PathLookupHelper.FindFullPathFromPath("mycommand", null, ':', AlwaysFalse, null);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_ReturnsFirstMatchFromPath()
    {
        // Arrange
        var existingFiles = new HashSet<string>
        {
            Path.Combine("/first/path", "mycommand"),
            Path.Combine("/second/path", "mycommand")
        };
 
        // Act
        var result = PathLookupHelper.FindFullPathFromPath("mycommand", "/first/path:/second/path", ':', existingFiles.Contains, null);
 
        // Assert
        Assert.Equal(Path.Combine("/first/path", "mycommand"), result);
    }
 
    [Fact]
    public void FindFullPathFromPath_UsesCorrectPathSeparator()
    {
        // Arrange - use platform-agnostic paths for testing
        var dir = Path.Combine("testdir", "bin");
        var expectedPath = Path.Combine(dir, "mycommand");
        var existingFiles = new HashSet<string>
        {
            expectedPath
        };
 
        // Act
        var result = PathLookupHelper.FindFullPathFromPath("mycommand", $"{dir};otherdir", ';', existingFiles.Contains, null);
 
        // Assert
        Assert.Equal(expectedPath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_FindsCommandWithExtension()
    {
        // Arrange - simulate Windows behavior where "code" is actually "code.CMD"
        var dir = Path.Combine("testdir", "bin");
        var expectedPath = Path.Combine(dir, "code.CMD");
        var existingFiles = new HashSet<string>
        {
            expectedPath
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - searching for "code" should find "code.CMD"
        var result = PathLookupHelper.FindFullPathFromPath("code", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Equal(expectedPath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_FindsFirstMatchingExtension()
    {
        // Arrange - when multiple extensions match, returns the first one in PATHEXT order
        var dir = Path.Combine("testdir", "bin");
        var exePath = Path.Combine(dir, "code.EXE");
        var cmdPath = Path.Combine(dir, "code.CMD");
        var existingFiles = new HashSet<string>
        {
            exePath,
            cmdPath
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - should find .EXE before .CMD because .EXE comes first in PATHEXT
        var result = PathLookupHelper.FindFullPathFromPath("code", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Equal(exePath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_PrefersExtensionOverExactMatch()
    {
        // Arrange - when both exist, the extension-based file is preferred on Windows
        // This is important because Windows cannot execute extension-less scripts directly.
        // For example, "code" in VS Code's bin folder is a shell script that Windows can't run,
        // but "code.cmd" is the proper executable wrapper.
        var dir = Path.Combine("testdir", "bin");
        var exactPath = Path.Combine(dir, "code");
        var cmdPath = Path.Combine(dir, "code.CMD");
        var existingFiles = new HashSet<string>
        {
            exactPath,
            cmdPath
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - should find "code.CMD", not "code" (extension-based files preferred on Windows)
        var result = PathLookupHelper.FindFullPathFromPath("code", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Equal(cmdPath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_FallsBackToExactMatchIfNoExtensionFound()
    {
        // Arrange - when no extension-based file exists, fall back to exact match
        var dir = Path.Combine("testdir", "bin");
        var exactPath = Path.Combine(dir, "mytool");
        var existingFiles = new HashSet<string>
        {
            exactPath
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - no extension files exist, so should fall back to exact match
        var result = PathLookupHelper.FindFullPathFromPath("mytool", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Equal(exactPath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_DoesNotDuplicateExtension()
    {
        // Arrange - when command already has extension, don't duplicate it
        var dir = Path.Combine("testdir", "bin");
        var expectedPath = Path.Combine(dir, "code.CMD");
        var existingFiles = new HashSet<string>
        {
            expectedPath
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - searching for "code.CMD" should find "code.CMD", not "code.CMD.CMD"
        var result = PathLookupHelper.FindFullPathFromPath("code.CMD", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Equal(expectedPath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_CommandWithExtensionNotFound_ReturnsNull()
    {
        // Arrange - command has known extension but file doesn't exist
        var dir = Path.Combine("testdir", "bin");
        var existingFiles = new HashSet<string>();
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - searching for "code.EXE" when it doesn't exist should return null
        var result = PathLookupHelper.FindFullPathFromPath("code.EXE", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_CommandWithExtension_DoesNotTryOtherExtensions()
    {
        // Arrange - when command has a known extension, don't try other extensions
        var dir = Path.Combine("testdir", "bin");
        var cmdPath = Path.Combine(dir, "code.CMD");
        var existingFiles = new HashSet<string>
        {
            cmdPath
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - searching for "code.EXE" should NOT find "code.CMD"
        // (we should not strip .EXE and try .CMD)
        var result = PathLookupHelper.FindFullPathFromPath("code.EXE", dir, ';', existingFiles.Contains, pathExtensions);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithNullPathExtensions_DoesNotTryExtensions()
    {
        // Arrange - simulate non-Windows behavior where PATHEXT is not used
        var dir = "/usr/bin";
        var existingFiles = new HashSet<string>
        {
            Path.Combine(dir, "code.cmd")
        };
 
        // Act - with null pathExtensions, should NOT find "code.cmd" when searching for "code"
        var result = PathLookupHelper.FindFullPathFromPath("code", dir, ':', existingFiles.Contains, null);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithEmptyPathExtensions_DoesNotTryExtensions()
    {
        // Arrange
        var dir = "/usr/bin";
        var existingFiles = new HashSet<string>
        {
            Path.Combine(dir, "code.cmd")
        };
 
        // Act - with empty pathExtensions array, should NOT find "code.cmd" when searching for "code"
        var result = PathLookupHelper.FindFullPathFromPath("code", dir, ':', existingFiles.Contains, []);
 
        // Assert
        Assert.Null(result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_DirectoryOrderTakesPrecedenceOverExtensionOrder()
    {
        // Arrange - This test validates Windows PATH lookup semantics:
        // Windows searches each directory completely (trying all PATHEXT extensions) before moving to the next.
        // So if dir1 has code.CMD and dir2 has code.EXE, dir1/code.CMD should be found first,
        // even though .EXE comes before .CMD in PATHEXT order.
        var dir1 = Path.Combine("first", "bin");
        var dir2 = Path.Combine("second", "bin");
        var dir1CmdPath = Path.Combine(dir1, "code.CMD");
        var dir2ExePath = Path.Combine(dir2, "code.EXE");
        var existingFiles = new HashSet<string>
        {
            dir1CmdPath,  // Lower priority extension, but in first directory
            dir2ExePath   // Higher priority extension, but in second directory
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - should find dir1/code.CMD because directory order takes precedence
        var result = PathLookupHelper.FindFullPathFromPath("code", $"{dir1};{dir2}", ';', existingFiles.Contains, pathExtensions);
 
        // Assert - directory order (dir1) takes precedence over extension order (.EXE before .CMD)
        Assert.Equal(dir1CmdPath, result);
    }
 
    [Fact]
    public void FindFullPathFromPath_WithPathExtensions_ExactMatchInFirstDirBeatsExtensionInSecondDir()
    {
        // Arrange - exact match in first directory should be found before extension match in second directory
        var dir1 = Path.Combine("first", "bin");
        var dir2 = Path.Combine("second", "bin");
        var dir1ExactPath = Path.Combine(dir1, "mytool");
        var dir2ExePath = Path.Combine(dir2, "mytool.EXE");
        var existingFiles = new HashSet<string>
        {
            dir1ExactPath,  // Exact match in first directory
            dir2ExePath     // Extension match in second directory
        };
        var pathExtensions = new[] { ".COM", ".EXE", ".BAT", ".CMD" };
 
        // Act - should find dir1/mytool because directory order takes precedence
        // Note: Within a directory, extension matches are tried first, then exact match.
        // But across directories, the first directory's results (including fallback) take precedence.
        var result = PathLookupHelper.FindFullPathFromPath("mytool", $"{dir1};{dir2}", ';', existingFiles.Contains, pathExtensions);
 
        // Assert - dir1 exact match is found because no extension match exists in dir1
        Assert.Equal(dir1ExactPath, result);
    }
}