File: AbsolutePath_Tests.cs
Web Access
Project: ..\..\..\src\Framework.UnitTests\Microsoft.Build.Framework.UnitTests.csproj (Microsoft.Build.Framework.UnitTests)
// 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.Framework;
using Shouldly;
using Xunit;
using Xunit.NetCore.Extensions;
 
namespace Microsoft.Build.UnitTests
{
    public class AbsolutePath_Tests
    {
        private static AbsolutePath GetTestBasePath()
        {
            string baseDirectory = Path.Combine(Path.GetTempPath(), "abspath_test_base");
            return new AbsolutePath(baseDirectory, ignoreRootedCheck: false);
        }
 
        private static void ValidatePathAcceptance(string path, bool shouldBeAccepted)
        {
            if (shouldBeAccepted)
            {
                // Should not throw - these are truly absolute paths
                var absolutePath = new AbsolutePath(path);
                absolutePath.Value.ShouldBe(path);
            }
            else
            {
                // Should throw ArgumentException for any non-absolute path
                Should.Throw<ArgumentException>(() => new AbsolutePath(path, ignoreRootedCheck: false),
                    $"Path '{path}' should be rejected as it's not a true absolute path");
            }
        }
 
        [Fact]
        public void AbsolutePath_FromAbsolutePath_ShouldPreservePath()
        {
            string absolutePathString = Path.GetTempPath();
            var absolutePath = new AbsolutePath(absolutePathString);
 
            absolutePath.Value.ShouldBe(absolutePathString);
            Path.IsPathRooted(absolutePath.Value).ShouldBeTrue();
        }
 
        [Fact]
        public void AbsolutePath_NullOrEmpty_ShouldThrowOnNull()
        {
            string? path = null;
 
            Should.Throw<ArgumentNullException>(() => new AbsolutePath(path!));
        }
 
        [Fact]
        [UseInvariantCulture]
        public void AbsolutePath_NullOrEmpty_ShouldThrowOnEmpty()
        {
            string path = "";
 
            var exception = Should.Throw<ArgumentException>(() => new AbsolutePath(path));
            exception.Message.ShouldStartWith("The value cannot be an empty string.");
        }
 
        [Fact]
        public void AbsolutePath_NullOrEmptyWithBasePath_ShouldThrowOnNull()
        {
            string? path = null;
            var basePath = GetTestBasePath();
 
            Should.Throw<ArgumentNullException>(() => new AbsolutePath(path!, basePath));
        }
 
        [Fact]
        [UseInvariantCulture]
        public void AbsolutePath_NullOrEmptyWithBasePath_ShouldThrowOnEmpty()
        {
            string path = "";
            var basePath = GetTestBasePath();
 
            var exception = Should.Throw<ArgumentException>(() => new AbsolutePath(path, basePath));
            exception.Message.ShouldStartWith("The value cannot be an empty string.");
        }
 
        [Theory]
        [InlineData("subfolder")]
        [InlineData("deep/nested/path")]
        [InlineData(".")]
        [InlineData("..")]
        public void AbsolutePath_FromRelativePath_ShouldResolveAgainstBase(string relativePath)
        {
            string baseDirectory = Path.Combine(Path.GetTempPath(), "testfolder");
            var basePath = new AbsolutePath(baseDirectory);
            var absolutePath = new AbsolutePath(relativePath, basePath);
 
            Path.IsPathRooted(absolutePath.Value).ShouldBeTrue();
 
            string expectedPath = Path.Combine(baseDirectory, relativePath);
            absolutePath.Value.ShouldBe(expectedPath);
        }
 
        [Fact]
        public void AbsolutePath_Equality_ShouldWorkCorrectly()
        {
            string testPath = Path.GetTempPath();
            var path1 = new AbsolutePath(testPath);
            var path2 = new AbsolutePath(testPath);
            var differentPath = new AbsolutePath(Path.Combine(testPath, "different"));
 
            path1.ShouldBe(path2);
            (path1 == path2).ShouldBeTrue();
            path1.ShouldNotBe(differentPath);
            (path1 == differentPath).ShouldBeFalse();
        }
 
        [Fact]
        public void AbsolutePath_Inequality_ShouldWorkCorrectly()
        {
            string testPath = Path.GetTempPath();
            var path1 = new AbsolutePath(testPath);
            var differentPath = new AbsolutePath(Path.Combine(testPath, "different"));
 
            (path1 != differentPath).ShouldBeTrue();
#pragma warning disable CS1718 // Comparison made to same variable
            (path1 != path1).ShouldBeFalse();
#pragma warning restore CS1718 // Comparison made to same variable
        }
 
        [Fact]
        public void AbsolutePath_GetHashCode_ShouldBeConsistentWithEquals()
        {
            string testPath = Path.GetTempPath();
            var path1 = new AbsolutePath(testPath);
            var path2 = new AbsolutePath(testPath);
 
            // Equal objects must have equal hash codes
            path1.Equals(path2).ShouldBeTrue();
            path1.GetHashCode().ShouldBe(path2.GetHashCode());
        }
 
        [Fact]
        public void AbsolutePath_Equals_WithObject_ShouldWorkCorrectly()
        {
            string testPath = Path.GetTempPath();
            var path1 = new AbsolutePath(testPath);
            object path2 = new AbsolutePath(testPath);
            object notAPath = "not a path";
 
            path1.Equals(path2).ShouldBeTrue();
            path1.Equals(notAPath).ShouldBeFalse();
            path1.Equals(null).ShouldBeFalse();
        }
 
        [WindowsOnlyFact]
        public void AbsolutePath_CaseInsensitive_OnWindows()
        {
            // On Windows, paths are case-insensitive
            var lowerPath = new AbsolutePath("C:\\foo\\bar", ignoreRootedCheck: true);
            var upperPath = new AbsolutePath("C:\\FOO\\BAR", ignoreRootedCheck: true);
 
            lowerPath.Equals(upperPath).ShouldBeTrue();
            (lowerPath == upperPath).ShouldBeTrue();
            lowerPath.GetHashCode().ShouldBe(upperPath.GetHashCode());
        }
 
        [LinuxOnlyFact]
        public void AbsolutePath_CaseSensitive_OnLinux()
        {
            // On Linux, paths are case-sensitive
            var lowerPath = new AbsolutePath("/foo/bar");
            var upperPath = new AbsolutePath("/FOO/BAR");
 
            lowerPath.Equals(upperPath).ShouldBeFalse();
            (lowerPath == upperPath).ShouldBeFalse();
        }
 
        [Theory]
        [InlineData("not/rooted/path", false, true)]
        [InlineData("not/rooted/path", true, false)]
        public void AbsolutePath_RootedValidation_ShouldBehaveProperly(string path, bool ignoreRootedCheck, bool shouldThrow)
        {
            if (shouldThrow)
            {
                Should.Throw<ArgumentException>(() => new AbsolutePath(path, ignoreRootedCheck: ignoreRootedCheck));
            }
            else
            {
                var absolutePath = new AbsolutePath(path, ignoreRootedCheck: ignoreRootedCheck);
                absolutePath.Value.ShouldBe(path);
            }
        }
 
        [WindowsOnlyTheory]
        // True Windows absolute paths - should be accepted
        [InlineData("C:\\foo", true)]                    // Standard Windows absolute path
        [InlineData("C:\\foo\\bar", true)]                // Another Windows absolute path
        [InlineData("D:\\foo\\bar", true)]                // Different drive Windows path
        [InlineData("C:\\foo\\bar\\.", true)]              // Windows absolute path with current directory
        [InlineData("C:\\foo\\bar\\..", true)]             // Windows absolute path with parent directory
        // Windows rooted but NOT absolute paths - should be rejected
        [InlineData("\\foo", false)]                     // Root-relative (missing drive)
        [InlineData("\\foo\\bar", false)]                 // Root-relative (missing drive)
        [InlineData("C:foo", false)]                    // Drive-relative (no backslash after colon)
        [InlineData("C:1\\foo", false)]                  // Drive-relative with unexpected character
        // Relative paths - should be rejected
        [InlineData("foo", false)]                       // Simple relative path
        [InlineData("foo/bar", false)]                   // Forward slash relative path
        [InlineData("foo\\bar", false)]                  // Backslash relative path
        [InlineData(".", false)]                         // Current directory
        [InlineData("..", false)]                        // Parent directory
        [InlineData("../parent", false)]                 // Parent relative path
        [InlineData("subfolder/file.txt", false)]        // Nested relative path
        public void AbsolutePath_WindowsPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(string path, bool shouldBeAccepted)
        {
            ValidatePathAcceptance(path, shouldBeAccepted);
        }
 
        [UnixOnlyTheory]
        // True Unix absolute paths - should be accepted
        [InlineData("/foo", true)]                       // Standard Unix absolute path
        [InlineData("/foo/bar", true)]                   // Nested Unix absolute path
        [InlineData("/", true)]                          // Root directory
        [InlineData("/foo/bar/.", true)]                 // Unix absolute path with current directory
        [InlineData("/foo/bar/..", true)]                // Unix absolute path with parent directory
        // Relative paths - should be rejected (same on all platforms)
        [InlineData("foo", false)]                       // Simple relative path
        [InlineData("foo/bar", false)]                   // Forward slash relative path
        [InlineData("foo\\bar", false)]                  // Backslash relative path (unusual on Unix but still relative)
        [InlineData(".", false)]                         // Current directory
        [InlineData("..", false)]                        // Parent directory
        [InlineData("../parent", false)]                 // Parent relative path
        [InlineData("subfolder/file.txt", false)]        // Nested relative path
        public void AbsolutePath_UnixPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(string path, bool shouldBeAccepted)
        {
            ValidatePathAcceptance(path, shouldBeAccepted);
        }
 
        [Fact]
        public void GetCanonicalForm_NullPath_ShouldReturnSameInstance()
        {
            var absolutePath = new AbsolutePath(null!, null!, ignoreRootedCheck: true);
            var result = absolutePath.GetCanonicalForm();
 
            // Should return the same struct values when no normalization is needed
            result.ShouldBe(absolutePath);
        }
 
 
        [WindowsOnlyTheory]
        [InlineData("C:\\foo\\.\\bar")]                    // Current directory reference
        [InlineData("C:\\foo\\..\\bar")]                   // Parent directory reference
        [InlineData("C:\\foo/bar")]                        // Forward slash to backslash
        [InlineData("C:\\foo\\bar")]                       // Simple Windows path (no normalization needed)
        public void GetCanonicalForm_WindowsPathNormalization_ShouldMatchPathGetFullPath(string inputPath)
        {
            ValidateGetCanonicalFormMatchesSystem(inputPath);
        }
 
        [UnixOnlyTheory]
        [InlineData("/foo/./bar")]                         // Current directory reference
        [InlineData("/foo/../bar")]                        // Parent directory reference     
        [InlineData("/foo/bar")]                           // Simple Unix path (no normalization needed)
        [InlineData("/foo/bar\\baz")]                      // Simple Unix path with backslash that is not a path separator (no normalization needed)
        public void GetCanonicalForm_UnixPathNormalization_ShouldMatchPathGetFullPath(string inputPath)
        {
            ValidateGetCanonicalFormMatchesSystem(inputPath);
        }
 
        private static void ValidateGetCanonicalFormMatchesSystem(string inputPath)
        {
            var absolutePath = new AbsolutePath(inputPath, ignoreRootedCheck: true);
            var result = absolutePath.GetCanonicalForm();
            var systemResult = Path.GetFullPath(inputPath);
 
            // Should match Path.GetFullPath behavior exactly
            result.Value.ShouldBe(systemResult);
 
            // Should preserve original value
            result.OriginalValue.ShouldBe(absolutePath.OriginalValue);
        }
 
        [WindowsOnlyFact]
        [UseInvariantCulture]
        public void AbsolutePath_NotRooted_ShouldThrowWithLocalizedMessage()
        {
            var exception = Should.Throw<ArgumentException>(() => new AbsolutePath("relative/path"));
            exception.Message.ShouldContain("Path must be rooted");
        }
    }
}