File: Globbing\MSBuildGlob_Tests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.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.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Globbing;
using Microsoft.Build.Shared;
using Xunit;
 
#nullable disable
 
namespace Microsoft.Build.Engine.UnitTests.Globbing
{
    /// <summary>
    ///     Actual parsing tests are covered by FileMatcher_Tests (e.g. FileMatcher_Tests.SplitFileSpec)/>
    /// </summary>
    public class MSBuildGlob_Tests
    {
        [Theory]
        [InlineData("")]
        [InlineData("a")]
#if TEST_ISWINDOWS
        [InlineData(@"c:\a")]
#else
        [InlineData("/a")]
#endif
        public void GlobRootEndsWithTrailingSlash(string globRoot)
        {
            var glob = MSBuildGlob.Parse(globRoot, "*");
 
            Assert.Equal(glob.TestOnlyGlobRoot.LastOrDefault(), Path.DirectorySeparatorChar);
        }
 
        [Fact]
        public void GlobFromAbsoluteRootDoesNotChangeTheRoot()
        {
            var globRoot = NativeMethodsShared.IsWindows ? @"c:\a" : "/a";
            var glob = MSBuildGlob.Parse(globRoot, "*");
 
            var expectedRoot = Path.Combine(Directory.GetCurrentDirectory(), globRoot).WithTrailingSlash();
            Assert.Equal(expectedRoot, glob.TestOnlyGlobRoot);
        }
 
        [Fact]
        public void GlobFromEmptyRootUsesCurrentDirectory()
        {
            var glob = MSBuildGlob.Parse(string.Empty, "*");
 
            Assert.Equal(Directory.GetCurrentDirectory().WithTrailingSlash(), glob.TestOnlyGlobRoot);
        }
 
        [Fact]
        public void GlobFromNullRootThrows()
        {
            Assert.Throws<ArgumentNullException>(() => MSBuildGlob.Parse(null, "*"));
        }
 
        [Fact]
        public void GlobFromRelativeGlobRootNormalizesRootAgainstCurrentDirectory()
        {
            var globRoot = "a/b/c";
            var glob = MSBuildGlob.Parse(globRoot, "*");
 
            var expectedRoot = NormalizeRelativePathForGlobRepresentation(globRoot);
            Assert.Equal(expectedRoot, glob.TestOnlyGlobRoot);
        }
 
        [Fact]
        public void GlobFromRootWithInvalidPathThrows()
        {
            foreach (var invalidPathChar in FileUtilities.InvalidPathChars)
            {
                Assert.Throws<ArgumentException>(() => MSBuildGlob.Parse(invalidPathChar.ToString(), "*"));
            }
        }
 
        [Theory]
        [InlineData(
            "a/b/c",
            "**",
            "a/b/c")]
        [InlineData(
            "a/b/c",
            "../../**",
            "a")]
        [InlineData(
            "a/b/c",
            "../d/e/**",
            "a/b/d/e")]
        public void GlobWithRelativeFixedDirectoryPartShouldMismatchTheGlobRoot(string globRoot, string filespec, string expectedFixedDirectoryPart)
        {
            var glob = MSBuildGlob.Parse(globRoot, filespec);
 
            var expectedFixedDirectory = NormalizeRelativePathForGlobRepresentation(expectedFixedDirectoryPart);
 
            Assert.Equal(expectedFixedDirectory, glob.FixedDirectoryPart);
        }
 
        [Fact]
        public void GlobFromNullFileSpecThrows()
        {
            Assert.Throws<ArgumentNullException>(() => MSBuildGlob.Parse(null));
        }
 
        // Smoke test. Comprehensive parsing tests in FileMatcher
 
        [Fact]
        public void GlobParsingShouldInitializeState()
        {
            var globRoot = NativeMethodsShared.IsWindows ? @"c:\a" : "/a";
            var fileSpec = $"b/**/*.cs";
            var glob = MSBuildGlob.Parse(globRoot, fileSpec);
 
            var expectedFixedDirectory = Path.Combine(globRoot, "b").WithTrailingSlash();
 
            Assert.True(glob.IsLegal);
            Assert.True(glob.TestOnlyNeedsRecursion);
            Assert.Equal(fileSpec, glob.TestOnlyFileSpec);
 
            Assert.Equal(globRoot.WithTrailingSlash(), glob.TestOnlyGlobRoot);
            Assert.StartsWith(glob.TestOnlyGlobRoot, glob.FixedDirectoryPart);
 
            Assert.Equal(expectedFixedDirectory, glob.FixedDirectoryPart);
            Assert.Equal("**/", glob.WildcardDirectoryPart);
            Assert.Equal("*.cs", glob.FilenamePart);
        }
 
        [Fact]
        public void GlobParsingShouldInitializePartialStateOnIllegalFileSpec()
        {
            var globRoot = NativeMethodsShared.IsWindows ? @"c:\a" : "/a";
            var illegalFileSpec = $"b/.../**/*.cs";
            var glob = MSBuildGlob.Parse(globRoot, illegalFileSpec);
 
            Assert.False(glob.IsLegal);
            Assert.False(glob.TestOnlyNeedsRecursion);
            Assert.Equal(illegalFileSpec, glob.TestOnlyFileSpec);
 
            Assert.Equal(globRoot.WithTrailingSlash(), glob.TestOnlyGlobRoot);
 
            Assert.Equal(string.Empty, glob.FixedDirectoryPart);
            Assert.Equal(string.Empty, glob.WildcardDirectoryPart);
            Assert.Equal(string.Empty, glob.FilenamePart);
 
            Assert.False(glob.IsMatch($"b/.../c/d.cs"));
        }
 
        [Fact]
        public void GlobParsingShouldDeduplicateRegexes()
        {
            var globRoot = NativeMethodsShared.IsWindows ? @"c:\a" : "/a";
            var fileSpec = $"b/**/*.cs";
            var glob1 = MSBuildGlob.Parse(globRoot, fileSpec);
            var glob2 = MSBuildGlob.Parse(globRoot, fileSpec);
 
            Assert.Same(glob1.TestOnlyRegex, glob2.TestOnlyRegex);
        }
 
        [Fact]
        public void GlobIsNotUnescaped()
        {
            var glob = MSBuildGlob.Parse("%42/%42");
 
            Assert.True(glob.IsLegal);
            Assert.EndsWith("%42" + Path.DirectorySeparatorChar, glob.FixedDirectoryPart);
            Assert.Equal(string.Empty, glob.WildcardDirectoryPart);
            Assert.Equal("%42", glob.FilenamePart);
        }
 
        [Fact]
        public void GlobMatchingShouldThrowOnNullMatchArgument()
        {
            var glob = MSBuildGlob.Parse("*");
 
            Assert.Throws<ArgumentNullException>(() => glob.IsMatch(null));
        }
 
        [Fact]
        public void GlobMatchShouldReturnFalseIfArgumentContainsInvalidPathOrFileCharacters()
        {
            var glob = MSBuildGlob.Parse("*");
 
            foreach (var invalidPathChar in FileUtilities.InvalidPathChars)
            {
                Assert.False(glob.IsMatch(invalidPathChar.ToString()));
            }
 
            foreach (var invalidFileChar in FileUtilities.InvalidFileNameChars)
            {
                if (invalidFileChar == '\\' || invalidFileChar == '/')
                {
                    continue;
                }
 
                Assert.False(glob.IsMatch(invalidFileChar.ToString()));
            }
        }
 
        [Fact]
        public void GlobMatchingBehaviourWhenInputIsASingleSlash()
        {
            var glob = MSBuildGlob.Parse("*");
 
            if (NativeMethodsShared.IsUnixLike)
            {
                // \ is an acceptable file character on Unix, * should match it
                Assert.True(glob.IsMatch("\\"));
 
                // / means root on Unix
                Assert.False(glob.IsMatch("/"));
            }
            else
            {
                // \ means partition root on Windows
                Assert.False(glob.IsMatch("\\"));
 
                // / also means partition root on Windows
                Assert.False(glob.IsMatch("/"));
            }
        }
 
        [Fact]
        public void GlobMatchingShouldWorkWithLiteralStrings()
        {
            var glob = MSBuildGlob.Parse("abc");
 
            Assert.True(glob.IsMatch("abc"));
        }
 
        // Just a smoke test. Comprehensive tests in FileMatcher_Tests
 
        [Fact]
        public void GlobMatchingShouldWorkWithWildcards()
        {
            var glob = MSBuildGlob.Parse("ab?c*");
 
            Assert.False(glob.IsMatch("acd"));
        }
 
        [Fact]
        public void GlobMatchingShouldNotUnescapeInput()
        {
            var glob = MSBuildGlob.Parse("%42");
 
            Assert.False(glob.IsMatch("B"));
        }
 
        [Theory]
        [InlineData("")]
        [InlineData("a/b/c")]
        public void GlobShouldMatchEmptyArgWhenGlobIsEmpty(string globRoot)
        {
            var glob = MSBuildGlob.Parse(globRoot, string.Empty);
 
            Assert.True(glob.IsMatch(string.Empty));
        }
 
        [Theory]
        [InlineData("")]
        [InlineData("a/b/c")]
        public void GlobShouldMatchEmptyArgWhenGlobAcceptsEmptyString(string globRoot)
        {
            var glob = MSBuildGlob.Parse(globRoot, "*");
 
            Assert.True(glob.IsMatch(string.Empty));
        }
 
        [Theory]
        [InlineData("")]
        [InlineData("a/b/c")]
        public void GlobShouldNotMatchEmptyArgWhenGlobDoesNotRepresentEmpty(string globRoot)
        {
            var glob = MSBuildGlob.Parse(globRoot, "*a*");
 
            Assert.False(glob.IsMatch(string.Empty));
        }
 
        [Fact(Skip = "TODO")]
        public void GlobMatchingShouldTreatIllegalFileSpecAsLiteral()
        {
            var illegalSpec = "|...*";
            var glob = MSBuildGlob.Parse(illegalSpec);
 
            Assert.False(glob.IsLegal);
            Assert.True(glob.IsMatch(illegalSpec));
        }
 
        public static IEnumerable<object[]> GlobMatchingShouldRespectTheRootOfTheGlobTestData => GlobbingTestData.GlobbingConesTestData;
 
        [Theory]
 
        [MemberData(nameof(GlobMatchingShouldRespectTheRootOfTheGlobTestData))]
 
        public void GlobMatchingShouldRespectTheRootOfTheGlob(string fileSpec, string stringToMatch, string globRoot, bool shouldMatch)
        {
            var glob = MSBuildGlob.Parse(globRoot, fileSpec);
 
            if (shouldMatch)
            {
                Assert.True(glob.IsMatch(stringToMatch));
            }
            else
            {
                Assert.False(glob.IsMatch(stringToMatch));
            }
        }
 
        private static string NormalizeRelativePathForGlobRepresentation(string expectedFixedDirectoryPart)
        {
            return Path.Combine(Directory.GetCurrentDirectory(), expectedFixedDirectoryPart).Replace("/", "\\").WithTrailingSlash();
        }
 
        [Fact]
        public void GlobMatchingShouldWorkWithComplexRelativeLiterals()
        {
            var glob = MSBuildGlob.Parse("u/x", "../../u/x/d11/d21/../d22/../../d12/a.cs");
 
            Assert.True(glob.IsMatch(@"../x/d13/../../x/d12/d23/../a.cs"));
            Assert.False(glob.IsMatch(@"../x/d13/../x/d12/d23/../a.cs"));
        }
 
        [Theory]
        [InlineData(
            @"a/b\c",
            @"d/e\f/**\a.cs",
            @"d\e/f\g/h\i/a.cs",
            @"d\e/f\", @"g/h\i/", @"a.cs")]
        [InlineData(
            @"a/b\c",
            @"d/e\f/*b*\*.cs",
            @"d\e/f\abc/a.cs",
            @"d\e/f\", @"abc/", @"a.cs")]
        [InlineData(
            @"a/b/\c",
            @"d/e\/*b*/\*.cs",
            @"d\e\\abc/\a.cs",
            @"d\e\\", @"abc\\", @"a.cs")]
        public void GlobMatchingIgnoresSlashOrientationAndRepetitions(string globRoot, string fileSpec, string stringToMatch,
            string fixedDirectoryPart, string wildcardDirectoryPart, string filenamePart)
        {
            var glob = MSBuildGlob.Parse(globRoot, fileSpec);
 
            Assert.True(glob.IsMatch(stringToMatch));
 
            MSBuildGlob.MatchInfoResult result = glob.MatchInfo(stringToMatch);
            Assert.True(result.IsMatch);
 
            string NormalizeSlashes(string path)
            {
                string normalizedPath = path.Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar);
                return NativeMethodsShared.IsWindows ? normalizedPath.Replace("\\\\", "\\") : normalizedPath;
            }
 
            var rootedFixedDirectoryPart = Path.Combine(FileUtilities.NormalizePath(globRoot), fixedDirectoryPart);
 
            Assert.Equal(FileUtilities.GetFullPathNoThrow(rootedFixedDirectoryPart), result.FixedDirectoryPartMatchGroup);
            Assert.Equal(NormalizeSlashes(wildcardDirectoryPart), result.WildcardDirectoryPartMatchGroup);
            Assert.Equal(NormalizeSlashes(filenamePart), result.FilenamePartMatchGroup);
        }
    }
}