diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs index b89fa6fc7..dcb0f15e3 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.IO; diff --git a/src/Microsoft.DotNet.InternalAbstractions/HashCodeCombiner.cs b/src/Microsoft.DotNet.InternalAbstractions/HashCodeCombiner.cs new file mode 100644 index 000000000..f8b057c75 --- /dev/null +++ b/src/Microsoft.DotNet.InternalAbstractions/HashCodeCombiner.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.DotNet.InternalAbstractions +{ + public struct HashCodeCombiner + { + private long _combinedHash64; + + public int CombinedHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { return _combinedHash64.GetHashCode(); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private HashCodeCombiner(long seed) + { + _combinedHash64 = seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(string s) + { + var hashCode = (s != null) ? s.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(object o) + { + var hashCode = (o != null) ? o.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TValue value, IEqualityComparer comparer) + { + var hashCode = value != null ? comparer.GetHashCode(value) : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(0x1505L); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryAsset.cs b/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryAsset.cs index b4565dd19..3294e24fe 100644 --- a/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryAsset.cs +++ b/src/Microsoft.DotNet.ProjectModel/Compilation/LibraryAsset.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.DotNet.InternalAbstractions; using Microsoft.DotNet.ProjectModel.Utilities; -using Microsoft.Extensions.Internal; namespace Microsoft.DotNet.ProjectModel.Compilation { diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/DirectoryInfoBase.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/DirectoryInfoBase.cs new file mode 100644 index 000000000..69d5fe357 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/DirectoryInfoBase.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions +{ + public abstract class DirectoryInfoBase : FileSystemInfoBase + { + public abstract IEnumerable EnumerateFileSystemInfos(); + + public abstract DirectoryInfoBase GetDirectory(string path); + + public abstract FileInfoBase GetFile(string path); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/DirectoryInfoWrapper.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/DirectoryInfoWrapper.cs new file mode 100644 index 000000000..3d3ec1236 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/DirectoryInfoWrapper.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions +{ + public class DirectoryInfoWrapper : DirectoryInfoBase + { + private readonly DirectoryInfo _directoryInfo; + private readonly bool _isParentPath; + + public DirectoryInfoWrapper(DirectoryInfo directoryInfo, bool isParentPath = false) + { + _directoryInfo = directoryInfo; + _isParentPath = isParentPath; + } + + public override IEnumerable EnumerateFileSystemInfos() + { + if (_directoryInfo.Exists) + { + foreach (var fileSystemInfo in _directoryInfo.EnumerateFileSystemInfos("*", SearchOption.TopDirectoryOnly)) + { + var directoryInfo = fileSystemInfo as DirectoryInfo; + if (directoryInfo != null) + { + yield return new DirectoryInfoWrapper(directoryInfo); + } + else + { + yield return new FileInfoWrapper((FileInfo)fileSystemInfo); + } + } + } + } + + public override DirectoryInfoBase GetDirectory(string name) + { + var isParentPath = string.Equals(name, "..", StringComparison.Ordinal); + + if (isParentPath) + { + return new DirectoryInfoWrapper(new DirectoryInfo(Path.Combine(_directoryInfo.FullName, name)), isParentPath); + } + else + { + var dirs = _directoryInfo.GetDirectories(name); + + if (dirs.Length == 1) + { + return new DirectoryInfoWrapper(dirs[0], isParentPath); + } + else if (dirs.Length == 0) + { + return null; + } + else + { + // This shouldn't happen. The parameter name isn't supposed to contain wild card. + throw new InvalidOperationException( + string.Format("More than one sub directories are found under {0} with name {1}.", _directoryInfo.FullName, name)); + } + } + } + + public override FileInfoBase GetFile(string name) + { + return new FileInfoWrapper(new FileInfo(Path.Combine(_directoryInfo.FullName, name))); + } + + public override string Name + { + get { return _isParentPath ? ".." : _directoryInfo.Name; } + } + + public override string FullName + { + get { return _directoryInfo.FullName; } + } + + public override DirectoryInfoBase ParentDirectory + { + get { return new DirectoryInfoWrapper(_directoryInfo.Parent); } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileInfoBase.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileInfoBase.cs new file mode 100644 index 000000000..5eee717b0 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileInfoBase.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions +{ + public abstract class FileInfoBase : FileSystemInfoBase + { + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileInfoWrapper.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileInfoWrapper.cs new file mode 100644 index 000000000..addafc60d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileInfoWrapper.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions +{ + public class FileInfoWrapper : FileInfoBase + { + private FileInfo _fileInfo; + + public FileInfoWrapper(FileInfo fileInfo) + { + _fileInfo = fileInfo; + } + + public override string Name + { + get { return _fileInfo.Name; } + } + + public override string FullName + { + get { return _fileInfo.FullName; } + } + + public override DirectoryInfoBase ParentDirectory + { + get { return new DirectoryInfoWrapper(_fileInfo.Directory); } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileSystemInfoBase.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileSystemInfoBase.cs new file mode 100644 index 000000000..e6826cfb2 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Abstractions/FileSystemInfoBase.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions +{ + public abstract class FileSystemInfoBase + { + public abstract string Name { get; } + + public abstract string FullName { get; } + + public abstract DirectoryInfoBase ParentDirectory { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/FilePatternMatch.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/FilePatternMatch.cs new file mode 100644 index 000000000..e37232103 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/FilePatternMatch.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing +{ + public struct FilePatternMatch : IEquatable + { + public string Path { get; } + public string Stem { get; } + + public FilePatternMatch(string path, string stem) + { + Path = path; + Stem = stem; + } + + public bool Equals(FilePatternMatch other) + { + return string.Equals(other.Path, Path, StringComparison.OrdinalIgnoreCase) && + string.Equals(other.Stem, Stem, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + return Equals((FilePatternMatch)obj); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Path, StringComparer.OrdinalIgnoreCase); + hashCodeCombiner.Add(Stem, StringComparer.OrdinalIgnoreCase); + + return hashCodeCombiner.CombinedHash; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/ILinearPattern.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/ILinearPattern.cs new file mode 100644 index 000000000..a5cd7419b --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/ILinearPattern.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public interface ILinearPattern : IPattern + { + IList Segments { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPathSegment.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPathSegment.cs new file mode 100644 index 000000000..9d15792cb --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPathSegment.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public interface IPathSegment + { + bool CanProduceStem { get; } + + bool Match(string value); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPattern.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPattern.cs new file mode 100644 index 000000000..c67d87abe --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPattern.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public interface IPattern + { + IPatternContext CreatePatternContextForInclude(); + + IPatternContext CreatePatternContextForExclude(); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPatternContext.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPatternContext.cs new file mode 100644 index 000000000..ac98cb297 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IPatternContext.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public interface IPatternContext + { + void Declare(Action onDeclare); + + bool Test(DirectoryInfoBase directory); + + PatternTestResult Test(FileInfoBase file); + + void PushDirectory(DirectoryInfoBase directory); + + void PopDirectory(); + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IRaggedPattern.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IRaggedPattern.cs new file mode 100644 index 000000000..66915b91c --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/IRaggedPattern.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public interface IRaggedPattern : IPattern + { + IList Segments { get; } + + IList StartsWith { get; } + + IList> Contains { get; } + + IList EndsWith { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/MatcherContext.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/MatcherContext.cs new file mode 100644 index 000000000..22565a06a --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/MatcherContext.cs @@ -0,0 +1,250 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Util; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public class MatcherContext + { + private readonly DirectoryInfoBase _root; + private readonly IList _includePatternContexts; + private readonly IList _excludePatternContexts; + private readonly IList _files; + + private readonly HashSet _declaredLiteralFolderSegmentInString; + private readonly HashSet _declaredLiteralFolderSegments = new HashSet(); + private readonly HashSet _declaredLiteralFileSegments = new HashSet(); + + private bool _declaredParentPathSegment; + private bool _declaredWildcardPathSegment; + + private readonly StringComparison _comparisonType; + + public MatcherContext(IEnumerable includePatterns, + IEnumerable excludePatterns, + DirectoryInfoBase directoryInfo, + StringComparison comparison) + { + _root = directoryInfo; + _files = new List(); + _comparisonType = comparison; + + _includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToList(); + _excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToList(); + + _declaredLiteralFolderSegmentInString = new HashSet(StringComparisonHelper.GetStringComparer(comparison)); + } + + public PatternMatchingResult Execute() + { + _files.Clear(); + + Match(_root, parentRelativePath: null); + + return new PatternMatchingResult(_files); + } + + private void Match(DirectoryInfoBase directory, string parentRelativePath) + { + // Request all the including and excluding patterns to push current directory onto their status stack. + PushDirectory(directory); + Declare(); + + var entities = new List(); + if (_declaredWildcardPathSegment || _declaredLiteralFileSegments.Any()) + { + entities.AddRange(directory.EnumerateFileSystemInfos()); + } + else + { + var candidates = directory.EnumerateFileSystemInfos().OfType(); + foreach (var candidate in candidates) + { + if (_declaredLiteralFolderSegmentInString.Contains(candidate.Name)) + { + entities.Add(candidate); + } + } + } + + if (_declaredParentPathSegment) + { + entities.Add(directory.GetDirectory("..")); + } + + // collect files and sub directories + var subDirectories = new List(); + foreach (var entity in entities) + { + var fileInfo = entity as FileInfoBase; + if (fileInfo != null) + { + var result = MatchPatternContexts(fileInfo, (pattern, file) => pattern.Test(file)); + if (result.IsSuccessful) + { + _files.Add(new FilePatternMatch( + path: CombinePath(parentRelativePath, fileInfo.Name), + stem: result.Stem)); + } + + continue; + } + + var directoryInfo = entity as DirectoryInfoBase; + if (directoryInfo != null) + { + if (MatchPatternContexts(directoryInfo, (pattern, dir) => pattern.Test(dir))) + { + subDirectories.Add(directoryInfo); + } + + continue; + } + } + + // Matches the sub directories recursively + foreach (var subDir in subDirectories) + { + var relativePath = CombinePath(parentRelativePath, subDir.Name); + + Match(subDir, relativePath); + } + + // Request all the including and excluding patterns to pop their status stack. + PopDirectory(); + } + + private void Declare() + { + _declaredLiteralFileSegments.Clear(); + _declaredLiteralFolderSegments.Clear(); + _declaredParentPathSegment = false; + _declaredWildcardPathSegment = false; + + foreach (var include in _includePatternContexts) + { + include.Declare(DeclareInclude); + } + } + + private void DeclareInclude(IPathSegment patternSegment, bool isLastSegment) + { + var literalSegment = patternSegment as LiteralPathSegment; + if (literalSegment != null) + { + if (isLastSegment) + { + _declaredLiteralFileSegments.Add(literalSegment); + } + else + { + _declaredLiteralFolderSegments.Add(literalSegment); + _declaredLiteralFolderSegmentInString.Add(literalSegment.Value); + } + } + else if (patternSegment is ParentPathSegment) + { + _declaredParentPathSegment = true; + } + else if (patternSegment is WildcardPathSegment) + { + _declaredWildcardPathSegment = true; + } + } + + internal static string CombinePath(string left, string right) + { + if (string.IsNullOrEmpty(left)) + { + return right; + } + else + { + return string.Format("{0}/{1}", left, right); + } + } + + // Used to adapt Test(DirectoryInfoBase) for the below overload + private bool MatchPatternContexts(TFileInfoBase fileinfo, Func test) + { + return MatchPatternContexts( + fileinfo, + (ctx, file) => + { + if (test(ctx, file)) + { + return PatternTestResult.Success(stem: string.Empty); + } + else + { + return PatternTestResult.Failed; + } + }).IsSuccessful; + } + + private PatternTestResult MatchPatternContexts(TFileInfoBase fileinfo, Func test) + { + var result = PatternTestResult.Failed; + + // If the given file/directory matches any including pattern, continues to next step. + foreach (var context in _includePatternContexts) + { + var localResult = test(context, fileinfo); + if (localResult.IsSuccessful) + { + result = localResult; + break; + } + } + + // If the given file/directory doesn't match any of the including pattern, returns false. + if (!result.IsSuccessful) + { + return PatternTestResult.Failed; + } + + // If the given file/directory matches any excluding pattern, returns false. + foreach (var context in _excludePatternContexts) + { + if (test(context, fileinfo).IsSuccessful) + { + return PatternTestResult.Failed; + } + } + + return result; + } + + private void PopDirectory() + { + foreach (var context in _excludePatternContexts) + { + context.PopDirectory(); + } + + foreach (var context in _includePatternContexts) + { + context.PopDirectory(); + } + } + + private void PushDirectory(DirectoryInfoBase directory) + { + foreach (var context in _includePatternContexts) + { + context.PushDirectory(directory); + } + + foreach (var context in _excludePatternContexts) + { + context.PushDirectory(directory); + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/CurrentPathSegment.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/CurrentPathSegment.cs new file mode 100644 index 000000000..1b50202ca --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/CurrentPathSegment.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments +{ + public class CurrentPathSegment : IPathSegment + { + public bool CanProduceStem { get { return false; } } + + public bool Match(string value) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/LiteralPathSegment.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/LiteralPathSegment.cs new file mode 100644 index 000000000..83b4dbe81 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/LiteralPathSegment.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Util; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments +{ + public class LiteralPathSegment : IPathSegment + { + private readonly StringComparison _comparisonType; + + public bool CanProduceStem { get { return false; } } + + public LiteralPathSegment(string value, StringComparison comparisonType) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Value = value; + + _comparisonType = comparisonType; + } + + public string Value { get; } + + public bool Match(string value) + { + return string.Equals(Value, value, _comparisonType); + } + + public override bool Equals(object obj) + { + var other = obj as LiteralPathSegment; + + return other != null && + _comparisonType == other._comparisonType && + string.Equals(other.Value, Value, _comparisonType); + } + + public override int GetHashCode() + { + return StringComparisonHelper.GetStringComparer(_comparisonType).GetHashCode(Value); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/ParentPathSegment.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/ParentPathSegment.cs new file mode 100644 index 000000000..02b336990 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/ParentPathSegment.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments +{ + public class ParentPathSegment : IPathSegment + { + private static readonly string LiteralParent = ".."; + + public bool CanProduceStem { get { return false; } } + + public bool Match(string value) + { + return string.Equals(LiteralParent, value, StringComparison.Ordinal); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/RecursiveWildcardSegment.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/RecursiveWildcardSegment.cs new file mode 100644 index 000000000..1e9c607df --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/RecursiveWildcardSegment.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments +{ + public class RecursiveWildcardSegment : IPathSegment + { + public bool CanProduceStem { get { return true; } } + + public bool Match(string value) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/WildcardPathSegment.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/WildcardPathSegment.cs new file mode 100644 index 000000000..d0eee3b0c --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PathSegments/WildcardPathSegment.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments +{ + public class WildcardPathSegment : IPathSegment + { + // It doesn't matter which StringComparison type is used in this MatchAll segment because + // all comparing are skipped since there is no content in the segment. + public static readonly WildcardPathSegment MatchAll = new WildcardPathSegment( + string.Empty, new List(), string.Empty, StringComparison.OrdinalIgnoreCase); + + private readonly StringComparison _comparisonType; + + public WildcardPathSegment(string beginsWith, List contains, string endsWith, StringComparison comparisonType) + { + BeginsWith = beginsWith; + Contains = contains; + EndsWith = endsWith; + _comparisonType = comparisonType; + } + + public bool CanProduceStem { get { return true; } } + + public string BeginsWith { get; } + + public List Contains { get; } + + public string EndsWith { get; } + + public bool Match(string value) + { + var wildcard = this; + + if (value.Length < wildcard.BeginsWith.Length + wildcard.EndsWith.Length) + { + return false; + } + + if (!value.StartsWith(wildcard.BeginsWith, _comparisonType)) + { + return false; + } + + if (!value.EndsWith(wildcard.EndsWith, _comparisonType)) + { + return false; + } + + var beginRemaining = wildcard.BeginsWith.Length; + var endRemaining = value.Length - wildcard.EndsWith.Length; + for (var containsIndex = 0; containsIndex != wildcard.Contains.Count; ++containsIndex) + { + var containsValue = wildcard.Contains[containsIndex]; + var indexOf = value.IndexOf( + value: containsValue, + startIndex: beginRemaining, + count: endRemaining - beginRemaining, + comparisonType: _comparisonType); + if (indexOf == -1) + { + return false; + } + + beginRemaining = indexOf + containsValue.Length; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContext.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContext.cs new file mode 100644 index 000000000..9cc35670e --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContext.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public abstract class PatternContext : IPatternContext + { + private Stack _stack = new Stack(); + protected TFrame Frame; + + public virtual void Declare(Action declare) { } + + public abstract PatternTestResult Test(FileInfoBase file); + + public abstract bool Test(DirectoryInfoBase directory); + + public abstract void PushDirectory(DirectoryInfoBase directory); + + public virtual void PopDirectory() + { + Frame = _stack.Pop(); + } + + protected void PushDataFrame(TFrame frame) + { + _stack.Push(Frame); + Frame = frame; + } + + protected bool IsStackEmpty() + { + return _stack.Count == 0; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinear.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinear.cs new file mode 100644 index 000000000..c2767590d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinear.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public abstract class PatternContextLinear + : PatternContext + { + public PatternContextLinear(ILinearPattern pattern) + { + Pattern = pattern; + } + + public override PatternTestResult Test(FileInfoBase file) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test file before entering a directory."); + } + + if(!Frame.IsNotApplicable && IsLastSegment() && TestMatchingSegment(file.Name)) + { + return PatternTestResult.Success(CalculateStem(file)); + } + + return PatternTestResult.Failed; + } + + public override void PushDirectory(DirectoryInfoBase directory) + { + // copy the current frame + var frame = Frame; + + if (IsStackEmpty() || Frame.IsNotApplicable) + { + // when the stack is being initialized + // or no change is required. + } + else if (!TestMatchingSegment(directory.Name)) + { + // nothing down this path is affected by this pattern + frame.IsNotApplicable = true; + } + else + { + // Determine this frame's contribution to the stem (if any) + var segment = Pattern.Segments[Frame.SegmentIndex]; + if (frame.InStem || segment.CanProduceStem) + { + frame.InStem = true; + frame.StemItems.Add(directory.Name); + } + + // directory matches segment, advance position in pattern + frame.SegmentIndex = frame.SegmentIndex + 1; + } + + PushDataFrame(frame); + } + + public struct FrameData + { + public bool IsNotApplicable; + public int SegmentIndex; + public bool InStem; + private IList _stemItems; + + public IList StemItems + { + get { return _stemItems ?? (_stemItems = new List()); } + } + + public string Stem + { + get { return _stemItems == null ? null : string.Join("/", _stemItems); } + } + } + + protected ILinearPattern Pattern { get; } + + protected bool IsLastSegment() + { + return Frame.SegmentIndex == Pattern.Segments.Count - 1; + } + + protected bool TestMatchingSegment(string value) + { + if (Frame.SegmentIndex >= Pattern.Segments.Count) + { + return false; + } + + return Pattern.Segments[Frame.SegmentIndex].Match(value); + } + + protected string CalculateStem(FileInfoBase matchedFile) + { + return MatcherContext.CombinePath(Frame.Stem, matchedFile.Name); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearExclude.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearExclude.cs new file mode 100644 index 000000000..17d0dffdb --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearExclude.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public class PatternContextLinearExclude : PatternContextLinear + { + public PatternContextLinearExclude(ILinearPattern pattern) + : base(pattern) + { + } + + public override bool Test(DirectoryInfoBase directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + return IsLastSegment() && TestMatchingSegment(directory.Name); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearInclude.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearInclude.cs new file mode 100644 index 000000000..5deb186aa --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearInclude.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public class PatternContextLinearInclude : PatternContextLinear + { + public PatternContextLinearInclude(ILinearPattern pattern) + : base(pattern) + { + } + + public override void Declare(Action onDeclare) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't declare path segment before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return; + } + + if (Frame.SegmentIndex < Pattern.Segments.Count) + { + onDeclare(Pattern.Segments[Frame.SegmentIndex], IsLastSegment()); + } + } + + public override bool Test(DirectoryInfoBase directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + return !IsLastSegment() && TestMatchingSegment(directory.Name); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRagged.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRagged.cs new file mode 100644 index 000000000..ea2dab475 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRagged.cs @@ -0,0 +1,186 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public abstract class PatternContextRagged : PatternContext + { + public PatternContextRagged(IRaggedPattern pattern) + { + Pattern = pattern; + } + + public override PatternTestResult Test(FileInfoBase file) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test file before entering a directory."); + } + + if(!Frame.IsNotApplicable && IsEndingGroup() && TestMatchingGroup(file)) + { + return PatternTestResult.Success(CalculateStem(file)); + } + return PatternTestResult.Failed; + } + + public sealed override void PushDirectory(DirectoryInfoBase directory) + { + // copy the current frame + var frame = Frame; + + if (IsStackEmpty()) + { + // initializing + frame.SegmentGroupIndex = -1; + frame.SegmentGroup = Pattern.StartsWith; + } + else if (Frame.IsNotApplicable) + { + // no change + } + else if (IsStartingGroup()) + { + if (!TestMatchingSegment(directory.Name)) + { + // nothing down this path is affected by this pattern + frame.IsNotApplicable = true; + } + else + { + // starting path incrementally satisfied + frame.SegmentIndex += 1; + } + } + else if (!IsStartingGroup() && directory.Name == "..") + { + // any parent path segment is not applicable in ** + frame.IsNotApplicable = true; + } + else if (!IsStartingGroup() && !IsEndingGroup() && TestMatchingGroup(directory)) + { + frame.SegmentIndex = Frame.SegmentGroup.Count; + frame.BacktrackAvailable = 0; + } + else + { + // increase directory backtrack length + frame.BacktrackAvailable += 1; + } + + if (frame.InStem) + { + frame.StemItems.Add(directory.Name); + } + + while ( + frame.SegmentIndex == frame.SegmentGroup.Count && + frame.SegmentGroupIndex != Pattern.Contains.Count) + { + frame.SegmentGroupIndex += 1; + frame.SegmentIndex = 0; + if (frame.SegmentGroupIndex < Pattern.Contains.Count) + { + frame.SegmentGroup = Pattern.Contains[frame.SegmentGroupIndex]; + } + else + { + frame.SegmentGroup = Pattern.EndsWith; + } + + // We now care about the stem + frame.InStem = true; + } + + PushDataFrame(frame); + } + + public override void PopDirectory() + { + base.PopDirectory(); + if (Frame.StemItems.Count > 0) + { + Frame.StemItems.RemoveAt(Frame.StemItems.Count - 1); + } + } + + public struct FrameData + { + public bool IsNotApplicable; + + public int SegmentGroupIndex; + + public IList SegmentGroup; + + public int BacktrackAvailable; + + public int SegmentIndex; + + public bool InStem; + + private IList _stemItems; + + public IList StemItems + { + get { return _stemItems ?? (_stemItems = new List()); } + } + + public string Stem + { + get { return _stemItems == null ? null : string.Join("/", _stemItems); } + } + } + + protected IRaggedPattern Pattern { get; } + + protected bool IsStartingGroup() + { + return Frame.SegmentGroupIndex == -1; + } + + protected bool IsEndingGroup() + { + return Frame.SegmentGroupIndex == Pattern.Contains.Count; + } + + protected bool TestMatchingSegment(string value) + { + if (Frame.SegmentIndex >= Frame.SegmentGroup.Count) + { + return false; + } + return Frame.SegmentGroup[Frame.SegmentIndex].Match(value); + } + + protected bool TestMatchingGroup(FileSystemInfoBase value) + { + var groupLength = Frame.SegmentGroup.Count; + var backtrackLength = Frame.BacktrackAvailable + 1; + if (backtrackLength < groupLength) + { + return false; + } + + var scan = value; + for (int index = 0; index != groupLength; ++index) + { + var segment = Frame.SegmentGroup[groupLength - index - 1]; + if (!segment.Match(scan.Name)) + { + return false; + } + scan = scan.ParentDirectory; + } + return true; + } + + protected string CalculateStem(FileInfoBase matchedFile) + { + return MatcherContext.CombinePath(Frame.Stem, matchedFile.Name); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedExclude.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedExclude.cs new file mode 100644 index 000000000..fa757828d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedExclude.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public class PatternContextRaggedExclude : PatternContextRagged + { + public PatternContextRaggedExclude(IRaggedPattern pattern) + : base(pattern) + { + } + + public override bool Test(DirectoryInfoBase directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + if (IsEndingGroup() && TestMatchingGroup(directory)) + { + // directory excluded with file-like pattern + return true; + } + + if (Pattern.EndsWith.Count == 0 && + Frame.SegmentGroupIndex == Pattern.Contains.Count - 1 && + TestMatchingGroup(directory)) + { + // directory excluded by matching up to final '/**' + return true; + } + + return false; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedInclude.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedInclude.cs new file mode 100644 index 000000000..33bef4954 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedInclude.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts +{ + public class PatternContextRaggedInclude : PatternContextRagged + { + public PatternContextRaggedInclude(IRaggedPattern pattern) + : base(pattern) + { + } + + public override void Declare(Action onDeclare) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't declare path segment before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return; + } + + if (IsStartingGroup() && Frame.SegmentIndex < Frame.SegmentGroup.Count) + { + onDeclare(Frame.SegmentGroup[Frame.SegmentIndex], false); + } + else + { + onDeclare(WildcardPathSegment.MatchAll, false); + } + } + + public override bool Test(DirectoryInfoBase directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + if (IsStartingGroup() && !TestMatchingSegment(directory.Name)) + { + // deterministic not-included + return false; + } + + return true; + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternTestResult.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternTestResult.cs new file mode 100644 index 000000000..408dd29be --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/PatternTestResult.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal +{ + public struct PatternTestResult + { + public static readonly PatternTestResult Failed = new PatternTestResult(isSuccessful: false, stem: null); + + public bool IsSuccessful { get; } + public string Stem { get; } + + private PatternTestResult(bool isSuccessful, string stem) + { + IsSuccessful = isSuccessful; + Stem = stem; + } + + public static PatternTestResult Success(string stem) + { + return new PatternTestResult(isSuccessful: true, stem: stem); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/Patterns/PatternBuilder.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/Patterns/PatternBuilder.cs new file mode 100644 index 000000000..c6995bce5 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Internal/Patterns/PatternBuilder.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.Patterns +{ + public class PatternBuilder + { + private static readonly char[] _slashes = new[] { '/', '\\' }; + private static readonly char[] _star = new[] { '*' }; + + public PatternBuilder() + { + ComparisonType = StringComparison.OrdinalIgnoreCase; + } + + public PatternBuilder(StringComparison comparisonType) + { + ComparisonType = comparisonType; + } + + public StringComparison ComparisonType { get; } + + public IPattern Build(string pattern) + { + if (pattern == null) + { + throw new ArgumentNullException("pattern"); + } + + pattern = pattern.TrimStart(_slashes); + + if (pattern.TrimEnd(_slashes).Length < pattern.Length) + { + // If the pattern end with a slash, it is considered as + // a directory. + pattern = pattern.TrimEnd(_slashes) + "/**"; + } + + var allSegments = new List(); + var isParentSegmentLegal = true; + + IList segmentsPatternStartsWith = null; + IList> segmentsPatternContains = null; + IList segmentsPatternEndsWith = null; + + var endPattern = pattern.Length; + for (int scanPattern = 0; scanPattern < endPattern;) + { + var beginSegment = scanPattern; + var endSegment = NextIndex(pattern, _slashes, scanPattern, endPattern); + + IPathSegment segment = null; + + if (segment == null && endSegment - beginSegment == 3) + { + if (pattern[beginSegment] == '*' && + pattern[beginSegment + 1] == '.' && + pattern[beginSegment + 2] == '*') + { + // turn *.* into * + beginSegment += 2; + } + } + + if (segment == null && endSegment - beginSegment == 2) + { + if (pattern[beginSegment] == '*' && + pattern[beginSegment + 1] == '*') + { + // recognized ** + segment = new RecursiveWildcardSegment(); + } + else if (pattern[beginSegment] == '.' && + pattern[beginSegment + 1] == '.') + { + // recognized .. + + if (!isParentSegmentLegal) + { + throw new ArgumentException("\"..\" can be only added at the beginning of the pattern."); + } + segment = new ParentPathSegment(); + } + } + + if (segment == null && endSegment - beginSegment == 1) + { + if (pattern[beginSegment] == '.') + { + // recognized . + segment = new CurrentPathSegment(); + } + } + + if (segment == null && endSegment - beginSegment > 2) + { + if (pattern[beginSegment] == '*' && + pattern[beginSegment + 1] == '*' && + pattern[beginSegment + 2] == '.') + { + // recognize **. + // swallow the first *, add the recursive path segment and + // the remaining part will be treat as wild card in next loop. + segment = new RecursiveWildcardSegment(); + endSegment = beginSegment; + } + } + + if (segment == null) + { + var beginsWith = string.Empty; + var contains = new List(); + var endsWith = string.Empty; + + for (int scanSegment = beginSegment; scanSegment < endSegment;) + { + var beginLiteral = scanSegment; + var endLiteral = NextIndex(pattern, _star, scanSegment, endSegment); + + if (beginLiteral == beginSegment) + { + if (endLiteral == endSegment) + { + // and the only bit + segment = new LiteralPathSegment(Portion(pattern, beginLiteral, endLiteral), ComparisonType); + } + else + { + // this is the first bit + beginsWith = Portion(pattern, beginLiteral, endLiteral); + } + } + else if (endLiteral == endSegment) + { + // this is the last bit + endsWith = Portion(pattern, beginLiteral, endLiteral); + } + else + { + if (beginLiteral != endLiteral) + { + // this is a middle bit + contains.Add(Portion(pattern, beginLiteral, endLiteral)); + } + else + { + // note: NOOP here, adjacent *'s are collapsed when they + // are mixed with literal text in a path segment + } + } + + scanSegment = endLiteral + 1; + } + + if (segment == null) + { + segment = new WildcardPathSegment(beginsWith, contains, endsWith, ComparisonType); + } + } + + if (!(segment is ParentPathSegment)) + { + isParentSegmentLegal = false; + } + + if (segment is CurrentPathSegment) + { + // ignore ".\" + } + else + { + if (segment is RecursiveWildcardSegment) + { + if (segmentsPatternStartsWith == null) + { + segmentsPatternStartsWith = new List(allSegments); + segmentsPatternEndsWith = new List(); + segmentsPatternContains = new List>(); + } + else if (segmentsPatternEndsWith.Count != 0) + { + segmentsPatternContains.Add(segmentsPatternEndsWith); + segmentsPatternEndsWith = new List(); + } + } + else if (segmentsPatternEndsWith != null) + { + segmentsPatternEndsWith.Add(segment); + } + + allSegments.Add(segment); + } + + scanPattern = endSegment + 1; + } + + if (segmentsPatternStartsWith == null) + { + return new LinearPattern(allSegments); + } + else + { + return new RaggedPattern(allSegments, segmentsPatternStartsWith, segmentsPatternEndsWith, segmentsPatternContains); + } + } + + private static int NextIndex(string pattern, char[] anyOf, int beginIndex, int endIndex) + { + var index = pattern.IndexOfAny(anyOf, beginIndex, endIndex - beginIndex); + return index == -1 ? endIndex : index; + } + + private static string Portion(string pattern, int beginIndex, int endIndex) + { + return pattern.Substring(beginIndex, endIndex - beginIndex); + } + + private class LinearPattern : ILinearPattern + { + public LinearPattern(List allSegments) + { + Segments = allSegments; + } + + public IList Segments { get; } + + public IPatternContext CreatePatternContextForInclude() + { + return new PatternContextLinearInclude(this); + } + + public IPatternContext CreatePatternContextForExclude() + { + return new PatternContextLinearExclude(this); + } + } + + private class RaggedPattern : IRaggedPattern + { + public RaggedPattern(List allSegments, IList segmentsPatternStartsWith, IList segmentsPatternEndsWith, IList> segmentsPatternContains) + { + Segments = allSegments; + StartsWith = segmentsPatternStartsWith; + Contains = segmentsPatternContains; + EndsWith = segmentsPatternEndsWith; + } + + public IList> Contains { get; } + + public IList EndsWith { get; } + + public IList Segments { get; } + + public IList StartsWith { get; } + + public IPatternContext CreatePatternContextForInclude() + { + return new PatternContextRaggedInclude(this); + } + + public IPatternContext CreatePatternContextForExclude() + { + return new PatternContextRaggedExclude(this); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Matcher.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Matcher.cs new file mode 100644 index 000000000..64dc98586 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Matcher.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.Patterns; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing +{ + public class Matcher + { + private IList _includePatterns = new List(); + private IList _excludePatterns = new List(); + private readonly PatternBuilder _builder; + private readonly StringComparison _comparison; + + public Matcher() + : this(StringComparison.OrdinalIgnoreCase) + { + } + + public Matcher(StringComparison comparisonType) + { + _comparison = comparisonType; + _builder = new PatternBuilder(comparisonType); + } + + public virtual Matcher AddInclude(string pattern) + { + _includePatterns.Add(_builder.Build(pattern)); + return this; + } + + public virtual Matcher AddExclude(string pattern) + { + _excludePatterns.Add(_builder.Build(pattern)); + return this; + } + + public virtual PatternMatchingResult Execute(DirectoryInfoBase directoryInfo) + { + var context = new MatcherContext(_includePatterns, _excludePatterns, directoryInfo, _comparison); + return context.Execute(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/MatcherExtensions.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/MatcherExtensions.cs new file mode 100644 index 000000000..f44e97ead --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/MatcherExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing +{ + public static class MatcherExtensions + { + public static void AddExcludePatterns(this Matcher matcher, params IEnumerable[] excludePatternsGroups) + { + foreach (var group in excludePatternsGroups) + { + foreach (var pattern in group) + { + matcher.AddExclude(pattern); + } + } + } + + public static void AddIncludePatterns(this Matcher matcher, params IEnumerable[] includePatternsGroups) + { + foreach (var group in includePatternsGroups) + { + foreach (var pattern in group) + { + matcher.AddInclude(pattern); + } + } + } + + public static IEnumerable GetResultsInFullPath(this Matcher matcher, string directoryPath) + { + var matches = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(directoryPath))).Files; + var result = matches.Select(match => Path.GetFullPath(Path.Combine(directoryPath, match.Path))).ToArray(); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/PatternMatchingResult.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/PatternMatchingResult.cs new file mode 100644 index 000000000..0a18b4b65 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/PatternMatchingResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing +{ + public class PatternMatchingResult + { + public PatternMatchingResult(IEnumerable files) + { + Files = files; + } + + public IEnumerable Files { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Util/StringComparisonHelper.cs b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Util/StringComparisonHelper.cs new file mode 100644 index 000000000..e7c8bfb7d --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/FileSystemGlobbing/Util/StringComparisonHelper.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Util +{ + internal static class StringComparisonHelper + { + public static StringComparer GetStringComparer(StringComparison comparisonType) + { + switch (comparisonType) + { + case StringComparison.CurrentCulture: + return StringComparer.CurrentCulture; + case StringComparison.CurrentCultureIgnoreCase: + return StringComparer.CurrentCultureIgnoreCase; + case StringComparison.Ordinal: + return StringComparer.Ordinal; + case StringComparison.OrdinalIgnoreCase: + return StringComparer.OrdinalIgnoreCase; +#if NET451 + case StringComparison.InvariantCulture: + return StringComparer.InvariantCulture; + case StringComparison.InvariantCultureIgnoreCase: + return StringComparer.InvariantCultureIgnoreCase; +#endif + default: + throw new InvalidOperationException($"Unexpected StringComparison type: {comparisonType}"); + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/Files/IncludeEntry.cs b/src/Microsoft.DotNet.ProjectModel/Files/IncludeEntry.cs index e7c8b11a0..40d8417ce 100644 --- a/src/Microsoft.DotNet.ProjectModel/Files/IncludeEntry.cs +++ b/src/Microsoft.DotNet.ProjectModel/Files/IncludeEntry.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using Microsoft.Extensions.Internal; +using Microsoft.DotNet.InternalAbstractions; namespace Microsoft.DotNet.ProjectModel.Files { diff --git a/src/Microsoft.DotNet.ProjectModel/Files/IncludeFilesResolver.cs b/src/Microsoft.DotNet.ProjectModel/Files/IncludeFilesResolver.cs index 2c63d8f2f..7d7e6e799 100644 --- a/src/Microsoft.DotNet.ProjectModel/Files/IncludeFilesResolver.cs +++ b/src/Microsoft.DotNet.ProjectModel/Files/IncludeFilesResolver.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.DotNet.ProjectModel.Utilities; -using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing; namespace Microsoft.DotNet.ProjectModel.Files { diff --git a/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs b/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs index 627966e74..80c8e8157 100644 --- a/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs +++ b/src/Microsoft.DotNet.ProjectModel/Files/PatternGroup.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing; using Newtonsoft.Json.Linq; namespace Microsoft.DotNet.ProjectModel.Files diff --git a/src/Microsoft.DotNet.ProjectModel/Graph/LibraryIdentity.cs b/src/Microsoft.DotNet.ProjectModel/Graph/LibraryIdentity.cs index 272e0245a..2833b012f 100644 --- a/src/Microsoft.DotNet.ProjectModel/Graph/LibraryIdentity.cs +++ b/src/Microsoft.DotNet.ProjectModel/Graph/LibraryIdentity.cs @@ -2,8 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using Microsoft.Extensions.Internal; -using NuGet; +using Microsoft.DotNet.InternalAbstractions; using NuGet.Versioning; namespace Microsoft.DotNet.ProjectModel.Graph diff --git a/src/Microsoft.DotNet.ProjectModel/Graph/LibraryRange.cs b/src/Microsoft.DotNet.ProjectModel/Graph/LibraryRange.cs index 0b49938c7..e6bea67e5 100644 --- a/src/Microsoft.DotNet.ProjectModel/Graph/LibraryRange.cs +++ b/src/Microsoft.DotNet.ProjectModel/Graph/LibraryRange.cs @@ -3,7 +3,7 @@ using System; using System.Text; -using Microsoft.Extensions.Internal; +using Microsoft.DotNet.InternalAbstractions; using NuGet.Versioning; namespace Microsoft.DotNet.ProjectModel.Graph diff --git a/src/Microsoft.DotNet.ProjectModel/LibraryDescription.cs b/src/Microsoft.DotNet.ProjectModel/LibraryDescription.cs index 789665c53..a5079473a 100644 --- a/src/Microsoft.DotNet.ProjectModel/LibraryDescription.cs +++ b/src/Microsoft.DotNet.ProjectModel/LibraryDescription.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.DotNet.InternalAbstractions; using Microsoft.DotNet.ProjectModel.Graph; -using Microsoft.Extensions.Internal; using NuGet.Frameworks; namespace Microsoft.DotNet.ProjectModel diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs index 39a8f355e..c8a45ab5c 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs @@ -9,7 +9,6 @@ using System.Text; using Microsoft.DotNet.InternalAbstractions; using Microsoft.DotNet.ProjectModel.Graph; using Microsoft.DotNet.ProjectModel.Resolution; -using Microsoft.Extensions.Internal; using NuGet.Frameworks; namespace Microsoft.DotNet.ProjectModel diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContextIdentity.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContextIdentity.cs index 92a69c900..6b5d8724f 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContextIdentity.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContextIdentity.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.Extensions.Internal; +using Microsoft.DotNet.InternalAbstractions; using NuGet.Frameworks; namespace Microsoft.DotNet.ProjectModel diff --git a/src/Microsoft.DotNet.ProjectModel/project.json b/src/Microsoft.DotNet.ProjectModel/project.json index 101bd5477..5b1c714d7 100644 --- a/src/Microsoft.DotNet.ProjectModel/project.json +++ b/src/Microsoft.DotNet.ProjectModel/project.json @@ -6,11 +6,6 @@ "description": "Types to model a .NET Project", "dependencies": { "Microsoft.Extensions.DependencyModel": "1.0.0-*", - "Microsoft.Extensions.FileSystemGlobbing": "1.0.0-rc2-20581", - "Microsoft.Extensions.HashCodeCombiner.Sources": { - "type": "build", - "version": "1.0.0-rc2-20459" - }, "Newtonsoft.Json": "7.0.1", "NuGet.Packaging": "3.5.0-beta-1233", "NuGet.RuntimeModel": "3.5.0-beta-1233", diff --git a/src/Microsoft.Extensions.DependencyModel/Dependency.cs b/src/Microsoft.Extensions.DependencyModel/Dependency.cs index 76a75fa74..6f7672ff0 100644 --- a/src/Microsoft.Extensions.DependencyModel/Dependency.cs +++ b/src/Microsoft.Extensions.DependencyModel/Dependency.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using Microsoft.Extensions.Internal; +using Microsoft.DotNet.InternalAbstractions; namespace Microsoft.Extensions.DependencyModel { diff --git a/src/Microsoft.Extensions.DependencyModel/project.json b/src/Microsoft.Extensions.DependencyModel/project.json index e3fac5e61..22af45989 100644 --- a/src/Microsoft.Extensions.DependencyModel/project.json +++ b/src/Microsoft.Extensions.DependencyModel/project.json @@ -14,10 +14,6 @@ "target": "project", "version": "1.0.0-*" }, - "Microsoft.Extensions.HashCodeCombiner.Sources": { - "type": "build", - "version": "1.0.0-rc2-20459" - }, "Newtonsoft.Json": "7.0.1" }, "frameworks": { diff --git a/src/dotnet/ProjectGlobbingResolver.cs b/src/dotnet/ProjectGlobbingResolver.cs index dbf20e64f..66f10db67 100644 --- a/src/dotnet/ProjectGlobbingResolver.cs +++ b/src/dotnet/ProjectGlobbingResolver.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Microsoft.Extensions.FileSystemGlobbing; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; namespace Microsoft.DotNet.Tools.Compiler { diff --git a/src/dotnet/commands/dotnet-pack/PackageGenerator.cs b/src/dotnet/commands/dotnet-pack/PackageGenerator.cs index 50c236a49..f0fe7e804 100644 --- a/src/dotnet/commands/dotnet-pack/PackageGenerator.cs +++ b/src/dotnet/commands/dotnet-pack/PackageGenerator.cs @@ -8,12 +8,12 @@ using System.Linq; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.ProjectModel.Files; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; using Microsoft.DotNet.ProjectModel.Graph; using Microsoft.DotNet.ProjectModel.Resources; using Microsoft.DotNet.ProjectModel.Utilities; using Microsoft.DotNet.Tools.Pack; -using Microsoft.Extensions.FileSystemGlobbing; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using NuGet; using NuGet.Frameworks; using NuGet.Packaging.Core; @@ -167,7 +167,11 @@ namespace Microsoft.DotNet.Tools.Compiler } } - internal static IEnumerable CollectAdditionalFiles(DirectoryInfoBase rootDirectory, IEnumerable projectFileGlobs, string projectFilePath, IList diagnostics) + internal static IEnumerable CollectAdditionalFiles( + DirectoryInfoBase rootDirectory, + IEnumerable projectFileGlobs, + string projectFilePath, + IList diagnostics) { foreach (var entry in projectFileGlobs) { diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/FileAbstractionsTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/FileAbstractionsTests.cs new file mode 100644 index 000000000..85f972355 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/FileAbstractionsTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests +{ + public class FileAbstractionsTests + { + [Fact] + public void TempFolderStartsInitiallyEmpty() + { + using (var scenario = new DisposableFileSystem()) + { + var contents = scenario.DirectoryInfo.EnumerateFileSystemInfos(); + + Assert.Equal(Path.GetFileName(scenario.RootPath), scenario.DirectoryInfo.Name); + Assert.Equal(scenario.RootPath, scenario.DirectoryInfo.FullName); + Assert.Equal(0, contents.Count()); + } + } + + [Fact] + public void FilesAreEnumerated() + { + using (var scenario = new DisposableFileSystem() + .CreateFile("alpha.txt")) + { + var contents = new DirectoryInfoWrapper(scenario.DirectoryInfo).EnumerateFileSystemInfos(); + var alphaTxt = contents.OfType().Single(); + + Assert.Equal(1, contents.Count()); + Assert.Equal("alpha.txt", alphaTxt.Name); + } + } + + [Fact] + public void FoldersAreEnumerated() + { + using (var scenario = new DisposableFileSystem() + .CreateFolder("beta")) + { + var contents1 = new DirectoryInfoWrapper(scenario.DirectoryInfo).EnumerateFileSystemInfos(); + var beta = contents1.OfType().Single(); + var contents2 = beta.EnumerateFileSystemInfos(); + + Assert.Equal(1, contents1.Count()); + Assert.Equal("beta", beta.Name); + Assert.Equal(0, contents2.Count()); + } + } + + [Fact] + public void SubFoldersAreEnumerated() + { + using (var scenario = new DisposableFileSystem() + .CreateFolder("beta") + .CreateFile(Path.Combine("beta", "alpha.txt"))) + { + var contents1 = new DirectoryInfoWrapper(scenario.DirectoryInfo).EnumerateFileSystemInfos(); + var beta = contents1.OfType().Single(); + var contents2 = beta.EnumerateFileSystemInfos(); + var alphaTxt = contents2.OfType().Single(); + + Assert.Equal(1, contents1.Count()); + Assert.Equal("beta", beta.Name); + Assert.Equal(1, contents2.Count()); + Assert.Equal("alpha.txt", alphaTxt.Name); + } + } + + [Fact] + public void GetDirectoryCanTakeDotDot() + { + using (var scenario = new DisposableFileSystem() + .CreateFolder("gamma") + .CreateFolder("beta") + .CreateFile(Path.Combine("beta", "alpha.txt"))) + { + var directoryInfoBase = new DirectoryInfoWrapper(scenario.DirectoryInfo); + var gamma = directoryInfoBase.GetDirectory("gamma"); + var dotdot = gamma.GetDirectory(".."); + var contents1 = dotdot.EnumerateFileSystemInfos(); + var beta = dotdot.GetDirectory("beta"); + var contents2 = beta.EnumerateFileSystemInfos(); + var alphaTxt = contents2.OfType().Single(); + + Assert.Equal("..", dotdot.Name); + Assert.Equal(2, contents1.Count()); + Assert.Equal("beta", beta.Name); + Assert.Equal(1, contents2.Count()); + Assert.Equal("alpha.txt", alphaTxt.Name); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/FunctionalTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/FunctionalTests.cs new file mode 100644 index 000000000..b5ba5fbf5 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/FunctionalTests.cs @@ -0,0 +1,469 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests +{ + public class FunctionalTests : IDisposable + { + private readonly DisposableFileSystem _context; + + public FunctionalTests() + { + _context = CreateContext(); + } + + public void Dispose() + { + _context.Dispose(); + } + + [Theory] + [InlineData("sub/source2.cs", "sub/source2.cs")] + [InlineData("sub\\source2.cs", "sub\\source2.cs")] + [InlineData("sub/source2.cs", "sub\\source2.cs")] + public void DuplicatePatterns(string pattern1, string pattern2) + { + var matcher = new Matcher(); + matcher.AddInclude(pattern1); + matcher.AddInclude(pattern2); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/sub/source2.cs"); + } + + [Theory] + [InlineData("src/project", "source1.cs", new string[] { "source1.cs" })] + [InlineData("src/project", "Source1.cs", new string[] { })] + [InlineData("src/project", "compiler/preprocess/**/*.cs", new string[] { "compiler/preprocess/preprocess-source1.cs", + "compiler/preprocess/sub/preprocess-source2.cs", + "compiler/preprocess/sub/sub/preprocess-source3.cs" })] + [InlineData("src/project", "compiler/Preprocess/**.cs", new string[] { })] + public void IncludeCaseSensitive(string root, string includePattern, string[] expectedFiles) + { + var matcher = new Matcher(StringComparison.Ordinal); + matcher.AddInclude(includePattern); + + ExecuteAndVerify(matcher, root, expectedFiles.Select(f => root + "/" + f).ToArray()); + } + + [Theory] + [InlineData("src/project", "source1.cs", new string[] { "source1.cs" })] + [InlineData("src/project", "Source1.cs", new string[] { "Source1.cs" })] + [InlineData("src/project", "compiler/preprocess/**/*.cs", new string[] { "compiler/preprocess/preprocess-source1.cs", + "compiler/preprocess/sub/preprocess-source2.cs", + "compiler/preprocess/sub/sub/preprocess-source3.cs" })] + [InlineData("src/project", "compiler/Preprocess/**.cs", new string[] { "compiler/Preprocess/preprocess-source1.cs", + "compiler/Preprocess/sub/preprocess-source2.cs", + "compiler/Preprocess/sub/sub/preprocess-source3.cs" })] + public void IncludeCaseInsensitive(string root, string includePattern, string[] expectedFiles) + { + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); + matcher.AddInclude(includePattern); + + ExecuteAndVerify(matcher, root, expectedFiles.Select(f => root + "/" + f).ToArray()); + } + + [Theory] + [InlineData("src/project/compiler/preprocess/", "source.cs", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "preprocess-source1.cs", new string[] { + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "preprocesS-source1.cs", new string[] { + "preprocess-source1.cs", + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "**/Preprocess*", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "**/preprocess*", new string[] { })] + [InlineData("src/project/compiler/preprocess/", "**/*source*.cs", new string[] { "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "**/*Source*.cs", new string[] { + "preprocess-source1.cs", + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "sub/sub/*", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs" })] + [InlineData("src/project/compiler/preprocess/", "sub/Sub/*", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + public void ExcludeCaseSensitive(string root, string excludePattern, string[] expectedFiles) + { + var matcher = new Matcher(StringComparison.Ordinal); + matcher.AddInclude("**/*.*"); + matcher.AddExclude(excludePattern); + + ExecuteAndVerify(matcher, root, expectedFiles.Select(f => root + "/" + f).ToArray()); + } + + [Theory] + [InlineData("src/project/compiler/preprocess/", "source.cs", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "preprocess-source1.cs", new string[] { + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "preprocesS-source1.cs", new string[] { + "sub/preprocess-source2.cs", + "sub/sub/preprocess-source3.cs", + "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "**/Preprocess*", new string[] { })] + [InlineData("src/project/compiler/preprocess/", "**/preprocess*", new string[] { })] + [InlineData("src/project/compiler/preprocess/", "**/*source*.cs", new string[] { "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "**/*Source*.cs", new string[] { "sub/sub/preprocess-source3.txt" })] + [InlineData("src/project/compiler/preprocess/", "sub/sub/*", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs" })] + [InlineData("src/project/compiler/preprocess/", "sub/Sub/*", new string[] { "preprocess-source1.cs", + "sub/preprocess-source2.cs" })] + public void ExcludeCaseInsensitive(string root, string excludePattern, string[] expectedFiles) + { + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); + matcher.AddInclude("**/*.*"); + matcher.AddExclude(excludePattern); + + ExecuteAndVerify(matcher, root, expectedFiles.Select(f => root + "/" + f).ToArray()); + } + + [Fact] + public void RecursiveAndDoubleParentsWithRecursiveSearch() + { + var matcher = new Matcher(); + matcher.AddInclude("**/*.cs") + .AddInclude(@"../../lib/**/*.cs"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/source1.cs", + "src/project/sub/source2.cs", + "src/project/sub/source3.cs", + "src/project/sub2/source4.cs", + "src/project/sub2/source5.cs", + "src/project/compiler/preprocess/preprocess-source1.cs", + "src/project/compiler/preprocess/sub/preprocess-source2.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project/compiler/shared/shared1.cs", + "src/project/compiler/shared/sub/shared2.cs", + "src/project/compiler/shared/sub/sub/sharedsub.cs", + "lib/source6.cs", + "lib/sub3/source7.cs", + "lib/sub4/source8.cs"); + } + + [Fact] + public void RecursiveAndDoubleParentsSearch() + { + var matcher = new Matcher(); + matcher.AddInclude("**/*.cs") + .AddInclude(@"../../lib/*.cs"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/source1.cs", + "src/project/sub/source2.cs", + "src/project/sub/source3.cs", + "src/project/sub2/source4.cs", + "src/project/sub2/source5.cs", + "src/project/compiler/preprocess/preprocess-source1.cs", + "src/project/compiler/preprocess/sub/preprocess-source2.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project/compiler/shared/shared1.cs", + "src/project/compiler/shared/sub/shared2.cs", + "src/project/compiler/shared/sub/sub/sharedsub.cs", + "lib/source6.cs"); + } + + [Fact] + public void WildcardAndDoubleParentWithRecursiveSearch() + { + var matcher = new Matcher(); + matcher.AddInclude(@"..\..\lib\**\*.cs"); + matcher.AddInclude(@"*.cs"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/source1.cs", + "lib/source6.cs", + "lib/sub3/source7.cs", + "lib/sub4/source8.cs"); + } + + [Fact] + public void WildcardAndDoubleParentsSearch() + { + var matcher = new Matcher(); + matcher.AddInclude(@"..\..\lib\*.cs"); + matcher.AddInclude(@"*.cs"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/source1.cs", + "lib/source6.cs"); + } + + [Fact] + public void DoubleParentsWithRecursiveSearch() + { + var matcher = new Matcher(); + matcher.AddInclude(@"..\..\lib\**\*.cs"); + + ExecuteAndVerify(matcher, @"src/project", + "lib/source6.cs", + "lib/sub3/source7.cs", + "lib/sub4/source8.cs"); + } + + [Fact] + public void OneLevelParentAndRecursiveSearch() + { + var matcher = new Matcher(); + matcher.AddInclude(@"../project2/**/*.cs"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project2/source1.cs", + "src/project2/sub/source2.cs", + "src/project2/sub/source3.cs", + "src/project2/sub2/source4.cs", + "src/project2/sub2/source5.cs", + "src/project2/compiler/preprocess/preprocess-source1.cs", + "src/project2/compiler/preprocess/sub/preprocess-source2.cs", + "src/project2/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project2/compiler/shared/shared1.cs", + "src/project2/compiler/shared/sub/shared2.cs", + "src/project2/compiler/shared/sub/sub/sharedsub.cs"); + } + + [Fact] + public void RecursiveSuffixSearch() + { + var matcher = new Matcher(); + matcher.AddInclude(@"**.txt"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.txt", + "src/project/compiler/shared/shared1.txt", + "src/project/compiler/shared/sub/shared2.txt", + "src/project/content1.txt"); + } + + [Fact] + public void FolderExclude() + { + var matcher = new Matcher(); + matcher.AddInclude(@"**/*.*"); + matcher.AddExclude(@"obj"); + matcher.AddExclude(@"bin"); + matcher.AddExclude(@".*"); + + ExecuteAndVerify(matcher, @"src/project", + "src/project/source1.cs", + "src/project/sub/source2.cs", + "src/project/sub/source3.cs", + "src/project/sub2/source4.cs", + "src/project/sub2/source5.cs", + "src/project/compiler/preprocess/preprocess-source1.cs", + "src/project/compiler/preprocess/sub/preprocess-source2.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.txt", + "src/project/compiler/shared/shared1.cs", + "src/project/compiler/shared/shared1.txt", + "src/project/compiler/shared/sub/shared2.cs", + "src/project/compiler/shared/sub/shared2.txt", + "src/project/compiler/shared/sub/sub/sharedsub.cs", + "src/project/compiler/resources/resource.res", + "src/project/compiler/resources/sub/resource2.res", + "src/project/compiler/resources/sub/sub/resource3.res", + "src/project/content1.txt"); + } + + [Fact] + public void FolderInclude() + { + var matcher = new Matcher(); + matcher.AddInclude(@"compiler/"); + ExecuteAndVerify(matcher, @"src/project", + "src/project/compiler/preprocess/preprocess-source1.cs", + "src/project/compiler/preprocess/sub/preprocess-source2.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.txt", + "src/project/compiler/shared/shared1.cs", + "src/project/compiler/shared/shared1.txt", + "src/project/compiler/shared/sub/shared2.cs", + "src/project/compiler/shared/sub/shared2.txt", + "src/project/compiler/shared/sub/sub/sharedsub.cs", + "src/project/compiler/resources/resource.res", + "src/project/compiler/resources/sub/resource2.res", + "src/project/compiler/resources/sub/sub/resource3.res"); + } + + [Theory] + [InlineData("source1.cs", "src/project/source1.cs")] + [InlineData("../project2/source1.cs", "src/project2/source1.cs")] + public void SingleFile(string pattern, string expect) + { + var matcher = new Matcher(); + matcher.AddInclude(pattern); + ExecuteAndVerify(matcher, "src/project", expect); + } + + [Fact] + public void SingleFileAndRecursive() + { + var matcher = new Matcher(); + matcher.AddInclude("**/*.cs"); + matcher.AddInclude("../project2/source1.cs"); + ExecuteAndVerify(matcher, "src/project", + "src/project/source1.cs", + "src/project/sub/source2.cs", + "src/project/sub/source3.cs", + "src/project/sub2/source4.cs", + "src/project/sub2/source5.cs", + "src/project/compiler/preprocess/preprocess-source1.cs", + "src/project/compiler/preprocess/sub/preprocess-source2.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project/compiler/shared/shared1.cs", + "src/project/compiler/shared/sub/shared2.cs", + "src/project/compiler/shared/sub/sub/sharedsub.cs", + "src/project2/source1.cs"); + } + + [Fact] + public void StemCorrectWithDifferentWildCards() + { + var matcher = new Matcher(); + matcher.AddInclude("sub/*.cs"); + matcher.AddInclude("**/*.cs"); + + var directoryPath = Path.Combine(_context.RootPath, "src/project"); + var results = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(directoryPath))); + + var actual = results.Files.Select(match => match.Stem); + var expected = new string[] { + "source1.cs", + "source2.cs", + "source3.cs", + "sub2/source4.cs", + "sub2/source5.cs", + "compiler/preprocess/preprocess-source1.cs", + "compiler/preprocess/sub/preprocess-source2.cs", + "compiler/preprocess/sub/sub/preprocess-source3.cs", + "compiler/shared/shared1.cs", + "compiler/shared/sub/shared2.cs", + "compiler/shared/sub/sub/sharedsub.cs" + }; + + Assert.Equal( + expected.OrderBy(e => e), + actual.OrderBy(e => e), + StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void MultipleSubDirsAfterFirstWildcardMatch_HasCorrectStem() + { + var matcher = new Matcher(); + matcher.AddInclude("compiler/**/*.cs"); + + var directoryPath = Path.Combine(_context.RootPath, "src/project"); + var results = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(directoryPath))); + + var actual = results.Files.Select(match => match.Stem); + var expected = new string[] { + "preprocess/preprocess-source1.cs", + "preprocess/sub/preprocess-source2.cs", + "preprocess/sub/sub/preprocess-source3.cs", + "shared/shared1.cs", + "shared/sub/shared2.cs", + "shared/sub/sub/sharedsub.cs" + }; + + Assert.Equal( + expected.OrderBy(e => e), + actual.OrderBy(e => e), + StringComparer.OrdinalIgnoreCase); + } + + private DisposableFileSystem CreateContext() + { + var context = new DisposableFileSystem(); + context.CreateFiles( + "src/project/source1.cs", + "src/project/sub/source2.cs", + "src/project/sub/source3.cs", + "src/project/sub2/source4.cs", + "src/project/sub2/source5.cs", + "src/project/compiler/preprocess/preprocess-source1.cs", + "src/project/compiler/preprocess/sub/preprocess-source2.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project/compiler/preprocess/sub/sub/preprocess-source3.txt", + "src/project/compiler/shared/shared1.cs", + "src/project/compiler/shared/shared1.txt", + "src/project/compiler/shared/sub/shared2.cs", + "src/project/compiler/shared/sub/shared2.txt", + "src/project/compiler/shared/sub/sub/sharedsub.cs", + "src/project/compiler/resources/resource.res", + "src/project/compiler/resources/sub/resource2.res", + "src/project/compiler/resources/sub/sub/resource3.res", + "src/project/content1.txt", + "src/project/obj/object.o", + "src/project/bin/object", + "src/project/.hidden/file1.hid", + "src/project/.hidden/sub/file2.hid", + "src/project2/source1.cs", + "src/project2/sub/source2.cs", + "src/project2/sub/source3.cs", + "src/project2/sub2/source4.cs", + "src/project2/sub2/source5.cs", + "src/project2/compiler/preprocess/preprocess-source1.cs", + "src/project2/compiler/preprocess/sub/preprocess-source2.cs", + "src/project2/compiler/preprocess/sub/sub/preprocess-source3.cs", + "src/project2/compiler/preprocess/sub/sub/preprocess-source3.txt", + "src/project2/compiler/shared/shared1.cs", + "src/project2/compiler/shared/shared1.txt", + "src/project2/compiler/shared/sub/shared2.cs", + "src/project2/compiler/shared/sub/shared2.txt", + "src/project2/compiler/shared/sub/sub/sharedsub.cs", + "src/project2/compiler/resources/resource.res", + "src/project2/compiler/resources/sub/resource2.res", + "src/project2/compiler/resources/sub/sub/resource3.res", + "src/project2/content1.txt", + "src/project2/obj/object.o", + "src/project2/bin/object", + "lib/source6.cs", + "lib/sub3/source7.cs", + "lib/sub4/source8.cs", + "res/resource1.text", + "res/resource2.text", + "res/resource3.text", + ".hidden/file1.hid", + ".hidden/sub/file2.hid"); + + return context; + } + + private void ExecuteAndVerify(Matcher matcher, string directoryPath, params string[] expectFiles) + { + directoryPath = Path.Combine(_context.RootPath, directoryPath); + var results = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(directoryPath))); + + var actual = results.Files.Select(match => Path.GetFullPath(Path.Combine(_context.RootPath, directoryPath, match.Path))); + var expected = expectFiles.Select(relativePath => Path.GetFullPath(Path.Combine(_context.RootPath, relativePath))); + + Assert.Equal( + expected.OrderBy(e => e), + actual.OrderBy(e => e), + StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternContexts/PatternContextLinearTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternContexts/PatternContextLinearTests.cs new file mode 100644 index 000000000..437348df9 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternContexts/PatternContextLinearTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternContexts +{ + public class PatternContextLinearIncludeTests + { + [Fact] + public void PredictBeforeEnterDirectoryShouldThrow() + { + var pattern = MockLinearPatternBuilder.New().Add("a").Build(); + var context = new PatternContextLinearInclude(pattern); + + Assert.Throws(() => + { + context.Declare((segment, last) => + { + Assert.False(true, "No segment should be declared."); + }); + }); + } + + [Theory] + [InlineData(new string[] { "a", "b" }, new string[] { "root" }, "a", false)] + [InlineData(new string[] { "a", "b" }, new string[] { "root", "a" }, "b", true)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root" }, "a", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a" }, "b", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "c", true)] + public void PredictReturnsCorrectResult(string[] testSegments, string[] pushDirectory, string expectSegment, bool expectLast) + { + var pattern = MockLinearPatternBuilder.New().Add(testSegments).Build(); + var context = new PatternContextLinearInclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + context.Declare((segment, last) => + { + var literal = segment as MockNonRecursivePathSegment; + + Assert.NotNull(segment); + Assert.Equal(expectSegment, literal.Value); + Assert.Equal(expectLast, last); + }); + } + + [Theory] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "b" })] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "c" })] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b", "d" })] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b", "c" })] + public void PredictNotCallBackWhenEnterUnmatchDirectory(string[] testSegments, string[] pushDirectory) + { + var pattern = MockLinearPatternBuilder.New().Add(testSegments).Build(); + var context = new PatternContextLinearInclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + context.Declare((segment, last) => + { + Assert.False(true, "No segment should be declared."); + }); + } + + [Theory] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", }, "b", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "d", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "c", true)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b", "c" }, "d", false)] + public void TestFileForIncludeReturnsCorrectResult(string[] testSegments, string[] pushDirectory, string filename, bool expectResult) + { + var pattern = MockLinearPatternBuilder.New().Add(testSegments).Build(); + var context = new PatternContextLinearInclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + var result = context.Test(new FakeFileInfo(filename)); + + Assert.Equal(expectResult, result.IsSuccessful); + } + + [Theory] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", }, "b", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "c", true)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "d", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b", "c" }, "d", false)] + public void TestFileForExcludeReturnsCorrectResult(string[] testSegments, string[] pushDirectory, string filename, bool expectResult) + { + var pattern = MockLinearPatternBuilder.New().Add(testSegments).Build(); + var context = new PatternContextLinearExclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + var result = context.Test(new FakeFileInfo(filename)); + + Assert.Equal(expectResult, result.IsSuccessful); + } + + [Theory] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root" }, "a", true)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a" }, "b", true)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a" }, "c", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "c", false)] + public void TestDirectoryForIncludeReturnsCorrectResult(string[] testSegments, string[] pushDirectory, string directoryName, bool expectResult) + { + var pattern = MockLinearPatternBuilder.New().Add(testSegments).Build(); + var context = new PatternContextLinearInclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + var result = context.Test(new FakeDirectoryInfo(directoryName)); + + Assert.Equal(expectResult, result); + } + + [Theory] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root" }, "a", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a" }, "b", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a" }, "c", false)] + [InlineData(new string[] { "a", "b", "c" }, new string[] { "root", "a", "b" }, "c", true)] + public void TestDirectoryForExcludeReturnsCorrectResult(string[] testSegments, string[] pushDirectory, string directoryName, bool expectResult) + { + var pattern = MockLinearPatternBuilder.New().Add(testSegments).Build(); + var context = new PatternContextLinearExclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + var result = context.Test(new FakeDirectoryInfo(directoryName)); + + Assert.Equal(expectResult, result); + } + + private class FakeDirectoryInfo : DirectoryInfoBase + { + public FakeDirectoryInfo(string name) + { + Name = name; + } + + public override string FullName { get { throw new NotImplementedException(); } } + + public override string Name { get; } + + public override DirectoryInfoBase ParentDirectory { get { throw new NotImplementedException(); } } + + public override IEnumerable EnumerateFileSystemInfos() { throw new NotImplementedException(); } + + public override DirectoryInfoBase GetDirectory(string path) { throw new NotImplementedException(); } + + public override FileInfoBase GetFile(string path) { throw new NotImplementedException(); } + } + + private class FakeFileInfo : FileInfoBase + { + public FakeFileInfo(string name) + { + Name = name; + } + + public override string FullName { get { throw new NotImplementedException(); } } + + public override string Name { get; } + + public override DirectoryInfoBase ParentDirectory { get { throw new NotImplementedException(); } } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternContexts/PatternContextRaggedTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternContexts/PatternContextRaggedTests.cs new file mode 100644 index 000000000..544121383 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternContexts/PatternContextRaggedTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PatternContexts; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.Patterns; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternContexts +{ + public class PatternContextRaggedIncludeTests + { + [Fact] + public void PredictBeforeEnterDirectoryShouldThrow() + { + var builder = new PatternBuilder(); + var pattern = builder.Build("**") as IRaggedPattern; + var context = new PatternContextRaggedInclude(pattern); + + Assert.Throws(() => + { + context.Declare((segment, last) => + { + Assert.False(true, "No segment should be declared."); + }); + }); + } + + [Theory] + [InlineData("/a/b/**/c/d", new string[] { "root" }, "a", false)] + [InlineData("/a/b/**/c/d", new string[] { "root", "a" }, "b", false)] + [InlineData("/a/b/**/c/d", new string[] { "root", "a", "b" }, null, false)] + [InlineData("/a/b/**/c/d", new string[] { "root", "a", "b", "whatever" }, null, false)] + [InlineData("/a/b/**/c/d", new string[] { "root", "a", "b", "whatever", "anything" }, null, false)] + public void PredictReturnsCorrectResult(string patternString, string[] pushDirectory, string expectSegment, bool expectWildcard) + { + var builder = new PatternBuilder(); + var pattern = builder.Build(patternString) as IRaggedPattern; + Assert.NotNull(pattern); + + var context = new PatternContextRaggedInclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + context.Declare((segment, last) => + { + if (expectSegment != null) + { + var mockSegment = segment as LiteralPathSegment; + + Assert.NotNull(mockSegment); + Assert.Equal(false, last); + Assert.Equal(expectSegment, mockSegment.Value); + } + else + { + Assert.Equal(Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments.WildcardPathSegment.MatchAll, segment); + } + }); + } + + [Theory] + [InlineData("/a/b/**/c/d", new string[] { "root", "b" })] + [InlineData("/a/b/**/c/d", new string[] { "root", "a", "c" })] + public void PredictNotCallBackWhenEnterUnmatchDirectory(string patternString, string[] pushDirectory) + { + var builder = new PatternBuilder(); + var pattern = builder.Build(patternString) as IRaggedPattern; + var context = new PatternContextRaggedInclude(pattern); + PatternContextHelper.PushDirectory(context, pushDirectory); + + context.Declare((segment, last) => + { + Assert.False(true, "No segment should be declared."); + }); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternMatchingTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternMatchingTests.cs new file mode 100644 index 000000000..dac41d4fa --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternMatchingTests.cs @@ -0,0 +1,472 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests +{ + public class PatternMatchingTests + { + [Fact] + public void EmptyCollectionWhenNoFilesPresent() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("alpha.txt") + .Execute(); + + scenario.AssertExact(); + } + + [Fact] + public void MatchingFileIsFound() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("alpha.txt") + .Files("alpha.txt") + .Execute(); + + scenario.AssertExact("alpha.txt"); + } + + [Fact] + public void MismatchedFileIsIgnored() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("alpha.txt") + .Files("omega.txt") + .Execute(); + + scenario.AssertExact(); + } + + [Fact] + public void FolderNamesAreTraversed() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("beta/alpha.txt") + .Files("beta/alpha.txt") + .Execute(); + + scenario.AssertExact("beta/alpha.txt"); + } + + [Theory] + [InlineData(@"beta/alpha.txt", @"beta/alpha.txt")] + [InlineData(@"beta\alpha.txt", @"beta/alpha.txt")] + [InlineData(@"beta/alpha.txt", @"beta\alpha.txt")] + [InlineData(@"beta\alpha.txt", @"beta\alpha.txt")] + [InlineData(@"\beta\alpha.txt", @"beta\alpha.txt")] + public void SlashPolarityIsIgnored(string includePattern, string filePath) + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include(includePattern) + .Files("one/two.txt", filePath, "three/four.txt") + .Execute(); + + scenario.AssertExact("beta/alpha.txt"); + } + + [Theory] + [InlineData(@"*.txt", new[] { "alpha.txt", "beta.txt" })] + [InlineData(@"alpha.*", new[] { "alpha.txt" })] + [InlineData(@"*.*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] + [InlineData(@"*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] + [InlineData(@"*et*", new[] { "beta.txt" })] + [InlineData(@"b*et*t", new[] { "beta.txt" })] + [InlineData(@"b*et*x", new string[0])] + public void PatternMatchingWorks(string includePattern, string[] matchesExpected) + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include(includePattern) + .Files("alpha.txt", "beta.txt", "gamma.dat") + .Execute(); + + scenario.AssertExact(matchesExpected); + } + + [Theory] + [InlineData(@"1234*5678", new[] { "12345678" })] + [InlineData(@"12345*5678", new string[0])] + [InlineData(@"12*3456*78", new[] { "12345678" })] + [InlineData(@"12*23*", new string[0])] + [InlineData(@"*67*78", new string[0])] + [InlineData(@"*45*56", new string[0])] + public void PatternBeginAndEndCantOverlap(string includePattern, string[] matchesExpected) + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include(includePattern) + .Files("12345678") + .Execute(); + + scenario.AssertExact(matchesExpected); + } + + + [Theory] + [InlineData(@"*mm*/*", new[] { "gamma/hello.txt" })] + [InlineData(@"/*mm*/*", new[] { "gamma/hello.txt" })] + [InlineData(@"*alpha*/*", new[] { "alpha/hello.txt" })] + [InlineData(@"/*alpha*/*", new[] { "alpha/hello.txt" })] + [InlineData(@"*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"/*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"*.*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"/*.*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + public void PatternMatchingWorksInFolders(string includePattern, string[] matchesExpected) + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include(includePattern) + .Files("alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt") + .Execute(); + + scenario.AssertExact(matchesExpected); + } + + [Theory] + [InlineData(@"", new string[] { })] + [InlineData(@"./", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"./alpha/hello.txt", new string[] { "alpha/hello.txt" })] + [InlineData(@"./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"././**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"././**/./hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"././**/./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + [InlineData(@"./*mm*/hello.txt", new string[] { "gamma/hello.txt" })] + [InlineData(@"./*mm*/*", new string[] { "gamma/hello.txt" })] + public void PatternMatchingCurrent(string includePattern, string[] matchesExpected) + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include(includePattern) + .Files("alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt") + .Execute(); + + scenario.AssertExact(matchesExpected); + } + + [Fact] + public void StarDotStarIsSameAsStar() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*.*") + .Files("alpha.txt", "alpha.", ".txt", ".", "alpha", "txt") + .Execute(); + + scenario.AssertExact("alpha.txt", "alpha.", ".txt", ".", "alpha", "txt"); + } + + [Fact] + public void IncompletePatternsDoNotInclude() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*/*.txt") + .Files("one/x.txt", "two/x.txt", "x.txt") + .Execute(); + + scenario.AssertExact("one/x.txt", "two/x.txt"); + } + + [Fact] + public void IncompletePatternsDoNotExclude() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*/*.txt") + .Exclude("one/hello.txt") + .Files("one/x.txt", "two/x.txt") + .Execute(); + + scenario.AssertExact("one/x.txt", "two/x.txt"); + } + + [Fact] + public void TrailingRecursiveWildcardMatchesAllFiles() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("one/**") + .Files("one/x.txt", "two/x.txt", "one/x/y.txt") + .Execute(); + + scenario.AssertExact("one/x.txt", "one/x/y.txt"); + } + + [Fact] + public void LeadingRecursiveWildcardMatchesAllLeadingPaths() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("**/*.cs") + .Files("one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs") + .Files("one/x.txt", "two/x.txt", "one/two/x.txt", "x.txt") + .Execute(); + + scenario.AssertExact("one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs"); + } + + [Fact] + public void InnerRecursiveWildcardMuseStartWithAndEndWith() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("one/**/*.cs") + .Files("one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs") + .Files("one/x.txt", "two/x.txt", "one/two/x.txt", "x.txt") + .Execute(); + + scenario.AssertExact("one/x.cs", "one/two/x.cs"); + } + + + [Fact] + public void ExcludeMayEndInDirectoryName() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*.cs", "*/*.cs", "*/*/*.cs") + .Exclude("bin", "one/two") + .Files("one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "bin/x.cs", "bin/two/x.cs") + .Execute(); + + scenario.AssertExact("one/x.cs", "two/x.cs", "x.cs"); + } + + + [Fact] + public void RecursiveWildcardSurroundingContainsWith() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("**/x/**") + .Files("x/1", "1/x/2", "1/x", "x", "1", "1/2") + .Execute(); + + scenario.AssertExact("x/1", "1/x/2"); + } + + + [Fact] + public void SequentialFoldersMayBeRequired() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("a/b/**/1/2/**/2/3/**") + .Files("1/2/2/3/x", "1/2/3/y", "a/1/2/4/2/3/b", "a/2/3/1/2/b") + .Files("a/b/1/2/2/3/x", "a/b/1/2/3/y", "a/b/a/1/2/4/2/3/b", "a/b/a/2/3/1/2/b") + .Execute(); + + scenario.AssertExact("a/b/1/2/2/3/x", "a/b/a/1/2/4/2/3/b"); + } + + [Fact] + public void RecursiveAloneIncludesEverything() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("**") + .Files("1/2/2/3/x", "1/2/3/y") + .Execute(); + + scenario.AssertExact("1/2/2/3/x", "1/2/3/y"); + } + + [Fact] + public void ExcludeCanHaveSurroundingRecursiveWildcards() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("**") + .Exclude("**/x/**") + .Files("x/1", "1/x/2", "1/x", "x", "1", "1/2") + .Execute(); + + scenario.AssertExact("1/x", "x", "1", "1/2"); + } + + [Fact] + public void LeadingDotDotCanComeThroughPattern() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*.cs") + .Include("../2/*.cs") + .Files("1/x.cs", "1/x.txt", "2/x.cs", "2/x.txt") + .SubDirectory("1") + .Execute(); + + scenario.AssertExact("x.cs", "../2/x.cs"); + } + + [Fact] + public void LeadingDotDotWithRecursiveCanComeThroughPattern() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*.cs") + .Include("../2/**/*.cs") + .Files("1/x.cs", "1/x.txt", "2/x.cs", "2/x.txt", "2/3/x.cs", "2/3/4/z.cs", "2/3/x.txt") + .SubDirectory("1") + .Execute(); + + scenario.AssertExact("x.cs", "../2/x.cs", "../2/3/x.cs", "../2/3/4/z.cs"); + } + + [Fact] + public void ExcludeFolderRecursively() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*.*") + .Include("../sibling/**/*.*") + .Exclude("../sibling/exc/**/*.*") + .Exclude("../sibling/inc/2.txt") + .Files("main/1.txt", "main/2.txt", "sibling/1.txt", "sibling/inc/1.txt", "sibling/inc/2.txt", "sibling/exc/1.txt", "sibling/exc/2.txt") + .SubDirectory("main") + .Execute(); + + scenario.AssertExact("1.txt", "2.txt", "../sibling/1.txt", "../sibling/inc/1.txt"); + } + + [Fact] + public void ExcludeFolderByName() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("*.*") + .Include("../sibling/**/*.*") + .Exclude("../sibling/exc/") + .Exclude("../sibling/inc/2.txt") + .Files("main/1.txt", "main/2.txt", "sibling/1.txt", "sibling/inc/1.txt", "sibling/inc/2.txt", "sibling/exc/1.txt", "sibling/exc/2.txt") + .SubDirectory("main") + .Execute(); + + scenario.AssertExact("1.txt", "2.txt", "../sibling/1.txt", "../sibling/inc/1.txt"); + } + + [Fact] + public void MultipleRecursiveWildcardStemMatch() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("sub/**/bar/**/*.txt") + .Files("root.txt", "sub/one.txt", "sub/two.txt", "sub/sub2/bar/baz/three.txt", "sub/sub3/sub4/bar/three.txt") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "sub/sub2/bar/baz/three.txt", stem: "sub2/bar/baz/three.txt"), + new FilePatternMatch(path: "sub/sub3/sub4/bar/three.txt", stem: "sub3/sub4/bar/three.txt") + }, scenario.Result.Files.ToArray()); + } + + [Fact] + public void RecursiveWildcardStemMatch() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("sub/**/*.txt") + .Files("root.txt", "sub/one.txt", "sub/two.txt", "sub/sub2/three.txt") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "sub/one.txt", stem: "one.txt"), + new FilePatternMatch(path: "sub/two.txt", stem: "two.txt"), + new FilePatternMatch(path: "sub/sub2/three.txt", stem: "sub2/three.txt") + }, scenario.Result.Files.ToArray()); + } + + [Fact] + public void WildcardMidSegmentMatch() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("sub/w*.txt") + .Files("root.txt", "sub/woah.txt", "sub/wow.txt", "sub/blah.txt") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "sub/woah.txt", stem: "woah.txt"), + new FilePatternMatch(path: "sub/wow.txt", stem: "wow.txt") + }, scenario.Result.Files.ToArray()); + } + + [Fact] + public void StemMatchOnExactFile() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("sub/sub/three.txt") + .Files("root.txt", "sub/one.txt", "sub/two.txt", "sub/sub/three.txt") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "sub/sub/three.txt", stem: "three.txt"), + }, scenario.Result.Files.ToArray()); + } + + [Fact] + public void SimpleStemMatching() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("sub/*") + .Files("root.txt", "sub/one.txt", "sub/two.txt", "sub/sub/three.txt") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "sub/one.txt", stem: "one.txt"), + new FilePatternMatch(path: "sub/two.txt", stem: "two.txt") + }, scenario.Result.Files.ToArray()); + } + + [Fact] + public void StemMatchingWithFileExtension() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("sub/*.txt") + .Files("root.txt", "sub/one.txt", "sub/two.txt", "sub/three.dat") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "sub/one.txt", stem: "one.txt"), + new FilePatternMatch(path: "sub/two.txt", stem: "two.txt") + }, scenario.Result.Files.ToArray()); + } + + [Fact] + public void StemMatchingWithParentDir() + { + var matcher = new Matcher(); + var scenario = new FileSystemGlobbingTestContext(@"c:\files\", matcher) + .Include("../files/sub/*.txt") + .Files("root.txt", "sub/one.txt", "sub/two.txt", "sub/three.dat") + .Execute(); + + // Check the stem of the matched items + Assert.Equal(new[] { + new FilePatternMatch(path: "../files/sub/one.txt", stem: "one.txt"), + new FilePatternMatch(path: "../files/sub/two.txt", stem: "two.txt") + }, scenario.Result.Files.ToArray()); + } + + // exclude: **/.*/** + // exclude: node_modules/* + // exclude: **/.cs + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/CurrentPathSegmentTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/CurrentPathSegmentTests.cs new file mode 100644 index 000000000..58c63e115 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/CurrentPathSegmentTests.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternSegments +{ + public class CurrentPathSegmentTests + { + [Theory] + [InlineData("anything")] + [InlineData("")] + [InlineData(null)] + public void Match(string testSample) + { + var pathSegment = new CurrentPathSegment(); + Assert.False(pathSegment.Match(testSample)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/LiteralPathSegmentTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/LiteralPathSegmentTests.cs new file mode 100644 index 000000000..7e1bcbb3d --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/LiteralPathSegmentTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternSegments +{ + public class LiteralPathSegmentTests + { + [Fact] + public void ThrowArgumentNullException() + { + Assert.Throws(() => + { + var pathSegment = new LiteralPathSegment(value: null, comparisonType: StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void AllowEmptyInDefaultConstructor() + { + var pathSegment = new LiteralPathSegment(string.Empty, comparisonType: StringComparison.Ordinal); + Assert.NotNull(pathSegment); + } + + [Theory] + [InlineData("something", "anything", StringComparison.Ordinal, false)] + [InlineData("something", "Something", StringComparison.Ordinal, false)] + [InlineData("something", "something", StringComparison.Ordinal, true)] + [InlineData("something", "anything", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("something", "Something", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("something", "something", StringComparison.OrdinalIgnoreCase, true)] + public void Match(string initialValue, string testSample, StringComparison comparisonType, bool expectation) + { + var pathSegment = new LiteralPathSegment(initialValue, comparisonType); + Assert.Equal(initialValue, pathSegment.Value); + Assert.Equal(expectation, pathSegment.Match(testSample)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/ParentPathSegmentTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/ParentPathSegmentTests.cs new file mode 100644 index 000000000..76e7f50fd --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/ParentPathSegmentTests.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternSegments +{ + public class ParentPathSegmentTests + { + [Theory] + [InlineData(".", false)] + [InlineData("..", true)] + [InlineData("...", false)] + public void Match(string testSample, bool expectation) + { + var pathSegment = new ParentPathSegment(); + Assert.Equal(expectation, pathSegment.Match(testSample)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/RecursiveWildcardSegmentTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/RecursiveWildcardSegmentTests.cs new file mode 100644 index 000000000..929b24681 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/RecursiveWildcardSegmentTests.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternSegments +{ + public class RecursiveWildcardSegmentTests + { + [Fact] + public void Match() + { + var pathSegment = new RecursiveWildcardSegment(); + Assert.False(pathSegment.Match("Anything")); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/WildcardPathSegmentTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/WildcardPathSegmentTests.cs new file mode 100644 index 000000000..01d708cca --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/PatternSegments/WildcardPathSegmentTests.cs @@ -0,0 +1,166 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.PathSegments; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternSegments +{ + public class WildcardPathSegmentTests + { + [Theory] + [InlineData(StringComparison.Ordinal)] + [InlineData(StringComparison.OrdinalIgnoreCase)] + public void DefaultConstructor(StringComparison comparisonType) + { + var paramBegin = "begin"; + var paramContains = new List { "1", "2", "three" }; + var paramEnd = "end"; + + var segment = new WildcardPathSegment(paramBegin, paramContains, paramEnd, comparisonType); + + Assert.Equal(paramBegin, segment.BeginsWith); + Assert.Equal(paramContains, segment.Contains); + Assert.Equal(paramEnd, segment.EndsWith); + } + + [Theory] + [MemberData("GetPositiveOrdinalIgnoreCaseDataSample")] + public void PositiveOrdinalIgnoreCaseMatch(string testSample, object segment) + { + var wildcardPathSegment = (WildcardPathSegment)segment; + Assert.True( + wildcardPathSegment.Match(testSample), + string.Format("[TestSample: {0}] [Wildcard: {1}]", testSample, Serialize(wildcardPathSegment))); + } + + [Theory] + [MemberData("GetNegativeOrdinalIgnoreCaseDataSample")] + public void NegativeOrdinalIgnoreCaseMatch(string testSample, object segment) + { + var wildcardPathSegment = (WildcardPathSegment)segment; + Assert.False( + wildcardPathSegment.Match(testSample), + string.Format("[TestSample: {0}] [Wildcard: {1}]", testSample, Serialize(wildcardPathSegment))); + } + + [Theory] + [MemberData("GetPositiveOrdinalDataSample")] + public void PositiveOrdinalMatch(string testSample, object segment) + { + var wildcardPathSegment = (WildcardPathSegment)segment; + Assert.True( + wildcardPathSegment.Match(testSample), + string.Format("[TestSample: {0}] [Wildcard: {1}]", testSample, Serialize(wildcardPathSegment))); + } + + [Theory] + [MemberData("GetNegativeOrdinalDataSample")] + public void NegativeOrdinalMatch(string testSample, object segment) + { + var wildcardPathSegment = (WildcardPathSegment)segment; + Assert.False( + wildcardPathSegment.Match(testSample), + string.Format("[TestSample: {0}] [Wildcard: {1}]", testSample, Serialize(wildcardPathSegment))); + } + + public static IEnumerable GetPositiveOrdinalIgnoreCaseDataSample() + { + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "abc", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "abBb123c", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "aaac", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "acccc", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "aacc", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "aacc", "aa", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "acc", "ac", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "abcdefgh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "abCDEfgh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ab123cd321ef123gh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "abcd321ef123gh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ababcd321ef123gh", "ab", "cd", "ef", "gh"); + } + + public static IEnumerable GetNegativeOrdinalIgnoreCaseDataSample() + { + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "aa", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "cc", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ab", "a", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ab", "a", "b", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "bc", "a", "b", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ac", "a", "b", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "abc", "a", "b", "b", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ab", "ab", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ab", "abb", "c"); + yield return WrapResult(StringComparison.OrdinalIgnoreCase, "ac", "ac", "c"); + } + + public static IEnumerable GetPositiveOrdinalDataSample() + { + yield return WrapResult(StringComparison.Ordinal, "abc", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "abBb123c", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "aaac", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "Aaac", "A", "c"); + yield return WrapResult(StringComparison.Ordinal, "acccC", "a", "C"); + yield return WrapResult(StringComparison.Ordinal, "aacc", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "aAcc", "aA", "c"); + yield return WrapResult(StringComparison.Ordinal, "acc", "ac", "c"); + yield return WrapResult(StringComparison.Ordinal, "abcDefgh", "ab", "cD", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "aB123cd321ef123gh", "aB", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "abcd321ef123gh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "ababcdCD321ef123gh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "ababcdCD321ef123gh", "ab", "CD", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "ababcd321eF123gh", "ab", "cd", "eF", "gh"); + } + + public static IEnumerable GetNegativeOrdinalDataSample() + { + yield return WrapResult(StringComparison.Ordinal, "aa", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "abc", "A", "c"); + yield return WrapResult(StringComparison.Ordinal, "cc", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "ab", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "ab", "a", "b", "c"); + yield return WrapResult(StringComparison.Ordinal, "bc", "a", "b", "c"); + yield return WrapResult(StringComparison.Ordinal, "ac", "a", "b", "c"); + yield return WrapResult(StringComparison.Ordinal, "abc", "a", "b", "b", "c"); + yield return WrapResult(StringComparison.Ordinal, "ab", "ab", "c"); + yield return WrapResult(StringComparison.Ordinal, "ab", "abb", "c"); + yield return WrapResult(StringComparison.Ordinal, "ac", "ac", "c"); + yield return WrapResult(StringComparison.Ordinal, "abBb123C", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "Aaac", "a", "c"); + yield return WrapResult(StringComparison.Ordinal, "aAac", "A", "c"); + yield return WrapResult(StringComparison.Ordinal, "aCc", "a", "C"); + yield return WrapResult(StringComparison.Ordinal, "aacc", "aA", "c"); + yield return WrapResult(StringComparison.Ordinal, "acc", "aC", "c"); + yield return WrapResult(StringComparison.Ordinal, "abcDefgh", "ab", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "aB123cd321ef123gh", "aB", "cd", "EF", "gh"); + yield return WrapResult(StringComparison.Ordinal, "abcd321ef123gh", "ab", "cd", "efF", "gh"); + yield return WrapResult(StringComparison.Ordinal, "ababcdCD321ef123gh", "AB", "cd", "ef", "gh"); + yield return WrapResult(StringComparison.Ordinal, "ababcdCD321ef123gh", "ab", "CD", "EF", "gh"); + } + + private static object[] WrapResult(StringComparison comparisonType, params string[] values) + { + if (values == null || values.Length < 3) + { + throw new InvalidOperationException("At least three values are required to create a data sample"); + } + + var beginWith = values[1]; + var endWith = values[values.Length - 1]; + var contains = values.Skip(2).Take(values.Length - 3); + + return new object[] { values[0], new WildcardPathSegment(beginWith, contains.ToList(), endWith, comparisonType) }; + } + + private static string Serialize(WildcardPathSegment segment) + { + return string.Format("{0}:{1}:{2}", + segment.BeginsWith, + string.Join(",", segment.Contains.ToArray()), + segment.EndsWith); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/Patterns/PatternTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/Patterns/PatternTests.cs new file mode 100644 index 000000000..4b12c6757 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/Patterns/PatternTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal.Patterns; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.Patterns +{ + public class PatternTests + { + [Theory] + [InlineData("abc", 1)] + [InlineData("/abc", 1)] + [InlineData("abc/efg", 2)] + [InlineData("abc/efg/h*j", 3)] + [InlineData("abc/efg/h*j/*.*", 4)] + [InlineData("abc/efg/hij", 3)] + [InlineData("abc/efg/hij/klm", 4)] + [InlineData("../abc/efg/hij/klm", 5)] + [InlineData("../../abc/efg/hij/klm", 6)] + public void BuildLinearPattern(string sample, int segmentCount) + { + var builder = new PatternBuilder(); + var pattern = builder.Build(sample); + + Assert.True(pattern is ILinearPattern); + Assert.Equal(segmentCount, (pattern as ILinearPattern).Segments.Count); + } + + [Theory] + [InlineData("abc/efg/**")] + [InlineData("/abc/efg/**")] + [InlineData("abc/efg/**/hij/klm")] + [InlineData("abc/efg/**/hij/**/klm")] + [InlineData("abc/efg/**/hij/**/klm/**")] + [InlineData("abc/efg/**/hij/**/klm/**/")] + [InlineData("**/hij/**/klm")] + [InlineData("**/hij/**")] + [InlineData("/**/hij/**")] + [InlineData("**/**/hij/**")] + [InlineData("ab/**/**/hij/**")] + [InlineData("ab/**/**/hij/**/")] + [InlineData("/ab/**/**/hij/**/")] + [InlineData("/ab/**/**/hij/**")] + public void BuildLinearPatternNegative(string sample) + { + var builder = new PatternBuilder(); + var pattern = builder.Build(sample) as ILinearPattern; + + Assert.Null(pattern); + } + + + [Theory] + [InlineData("/abc/", 2, 1, 0, 0)] + [InlineData("abc/", 2, 1, 0, 0)] + [InlineData("abc/efg/", 3, 2, 0, 0)] + [InlineData("abc/efg/h*j/*.*/", 5, 4, 0, 0)] + [InlineData("abc/efg/**", 3, 2, 0, 0)] + [InlineData("/abc/efg/**", 3, 2, 0, 0)] + [InlineData("abc/efg/**/hij/klm", 5, 2, 0, 2)] + [InlineData("abc/efg/**/hij/**/klm", 6, 2, 1, 1)] + [InlineData("abc/efg/**/hij/**/klm/**", 7, 2, 2, 0)] + [InlineData("abc/efg/**/hij/**/klm/**/", 8, 2, 2, 0)] + [InlineData("**/hij/**/klm", 4, 0, 1, 1)] + [InlineData("**/hij/**", 3, 0, 1, 0)] + [InlineData("/**/hij/**", 3, 0, 1, 0)] + [InlineData("**/**/hij/**", 4, 0, 1, 0)] + [InlineData("ab/**/**/hij/**", 5, 1, 1, 0)] + [InlineData("ab/**/**/hij/**/", 6, 1, 1, 0)] + [InlineData("/ab/**/**/hij/**/", 6, 1, 1, 0)] + [InlineData("/ab/**/**/hij/**", 5, 1, 1, 0)] + [InlineData("**/*.suffix", 2, 0, 0, 1)] + [InlineData("**.suffix", 2, 0, 0, 1)] + [InlineData("ab/**.suffix", 3, 1, 0, 1)] + public void BuildRaggedPattern(string sample, + int segmentCount, + int startSegmentsCount, + int containSegmentCount, + int endSegmentCount) + { + var builder = new PatternBuilder(); + var pattern = builder.Build(sample) as IRaggedPattern; + + Assert.NotNull(pattern); + Assert.Equal(segmentCount, pattern.Segments.Count); + Assert.Equal(startSegmentsCount, pattern.StartsWith.Count); + Assert.Equal(endSegmentCount, pattern.EndsWith.Count); + Assert.Equal(containSegmentCount, pattern.Contains.Count); + } + + [Theory] + [InlineData("abc")] + [InlineData("/abc")] + [InlineData("abc/efg")] + [InlineData("abc/efg/h*j")] + [InlineData("abc/efg/h*j/*.*")] + [InlineData("abc/efg/hij")] + [InlineData("abc/efg/hij/klm")] + public void BuildRaggedPatternNegative(string sample) + { + var builder = new PatternBuilder(); + var pattern = builder.Build(sample) as IRaggedPattern; + + Assert.Null(pattern); + } + + [Theory] + [InlineData("a/../")] + [InlineData("a/..")] + [InlineData("/a/../")] + [InlineData("./a/../")] + [InlineData("**/../")] + [InlineData("*.cs/../")] + public void ThrowExceptionForInvalidParentsPath(string sample) + { + // parent segment is only allowed at the beginning of the pattern + Assert.Throws(() => + { + var builder = new PatternBuilder(); + var pattern = builder.Build(sample); + + Assert.Null(pattern); + }); + } + + [Fact] + public void ThrowExceptionForNull() + { + Assert.Throws(() => + { + var builder = new PatternBuilder(); + builder.Build(null); + }); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/DisposableFileSystem.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/DisposableFileSystem.cs new file mode 100644 index 000000000..42416b951 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/DisposableFileSystem.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility +{ + public class DisposableFileSystem : IDisposable + { + public DisposableFileSystem() + { + RootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(RootPath); + DirectoryInfo = new DirectoryInfo(RootPath); + } + + public string RootPath { get; } + + public DirectoryInfo DirectoryInfo { get; } + + public DisposableFileSystem CreateFolder(string path) + { + Directory.CreateDirectory(Path.Combine(RootPath, path)); + return this; + } + + public DisposableFileSystem CreateFile(string path) + { + File.WriteAllText(Path.Combine(RootPath, path), "temp"); + return this; + } + + public DisposableFileSystem CreateFiles(params string[] fileRelativePaths) + { + foreach (var path in fileRelativePaths) + { + var fullPath = Path.Combine(RootPath, path); + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); + + File.WriteAllText( + fullPath, + string.Format("Automatically generated for testing on {0:yyyy}/{0:MM}/{0:dd} {0:hh}:{0:mm}:{0:ss}", DateTime.UtcNow)); + } + + return this; + } + + public void Dispose() + { + try + { + Directory.Delete(RootPath, true); + } + catch + { + // Don't throw if this fails. + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/FileSystemGlobbingTestContext.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/FileSystemGlobbingTestContext.cs new file mode 100644 index 000000000..65eb8ec2e --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/FileSystemGlobbingTestContext.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility +{ + internal class FileSystemGlobbingTestContext + { + private readonly string _basePath; + private readonly FileSystemOperationRecorder _recorder; + private readonly Matcher _patternMatching; + + private MockDirectoryInfo _directoryInfo; + + public PatternMatchingResult Result { get; private set; } + + public FileSystemGlobbingTestContext(string basePath, Matcher matcher) + { + _basePath = basePath; + _recorder = new FileSystemOperationRecorder(); + _patternMatching = matcher; + + _directoryInfo = new MockDirectoryInfo( + recorder: _recorder, + parentDirectory: null, + fullName: _basePath, + name: ".", + paths: new string[0]); + } + + public FileSystemGlobbingTestContext Include(params string[] patterns) + { + foreach (var pattern in patterns) + { + _patternMatching.AddInclude(pattern); + } + + return this; + } + + public FileSystemGlobbingTestContext Exclude(params string[] patterns) + { + foreach (var pattern in patterns) + { + _patternMatching.AddExclude(pattern); + } + + return this; + } + + public FileSystemGlobbingTestContext Files(params string[] files) + { + _directoryInfo = new MockDirectoryInfo( + _directoryInfo.Recorder, + _directoryInfo.ParentDirectory, + _directoryInfo.FullName, + _directoryInfo.Name, + _directoryInfo.Paths.Concat(files.Select(file => _basePath + file)).ToArray()); + + return this; + } + + public FileSystemGlobbingTestContext Execute() + { + Result = _patternMatching.Execute(_directoryInfo); + + return this; + } + + public FileSystemGlobbingTestContext AssertExact(params string[] files) + { + Assert.Equal(files.OrderBy(file => file), Result.Files.OrderBy(file => file.Path).Select(file => file.Path)); + + return this; + } + + public FileSystemGlobbingTestContext SubDirectory(string name) + { + _directoryInfo = (MockDirectoryInfo)_directoryInfo.GetDirectory(name); + return this; + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/FileSystemOperationRecorder.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/FileSystemOperationRecorder.cs new file mode 100644 index 000000000..0ee4dc40a --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/FileSystemOperationRecorder.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility +{ + internal class FileSystemOperationRecorder + { + public IList> Records = new List>(); + + public void Add(string action, object values) + { + var record = new Dictionary + { + {"action", action } + }; + + foreach (var p in values.GetType().GetTypeInfo().DeclaredProperties) + { + record[p.Name] = p.GetValue(values); + } + + Records.Add(record); + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockDirectoryInfo.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockDirectoryInfo.cs new file mode 100644 index 000000000..f48b163db --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockDirectoryInfo.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility +{ + internal class MockDirectoryInfo : DirectoryInfoBase + { + public MockDirectoryInfo( + FileSystemOperationRecorder recorder, + DirectoryInfoBase parentDirectory, + string fullName, + string name, + string[] paths) + { + ParentDirectory = parentDirectory; + Recorder = recorder; + FullName = fullName; + Name = name; + Paths = paths; + } + + public FileSystemOperationRecorder Recorder { get; } + + public override string FullName { get; } + + public override string Name { get; } + + public override DirectoryInfoBase ParentDirectory { get; } + + public string[] Paths { get; } + + public override IEnumerable EnumerateFileSystemInfos() + { + Recorder.Add("EnumerateFileSystemInfos", new { FullName, Name }); + + var names = new HashSet(); + + foreach (var path in Paths) + { + if (!path.Replace('\\', '/').StartsWith(FullName.Replace('\\', '/'))) + { + continue; + } + var beginPath = FullName.Length; + var endPath = path.Length; + + var beginSegment = beginPath; + var endSegment = NextIndex(path, new[] { '/', '\\' }, beginSegment, path.Length); + + if (endPath == endSegment) + { + yield return new MockFileInfo( + recorder: Recorder, + parentDirectory: this, + fullName: path, + name: path.Substring(beginSegment, endSegment - beginSegment)); + } + else + { + var name = path.Substring(beginSegment, endSegment - beginSegment); + if (!names.Contains(name)) + { + names.Add(name); + yield return new MockDirectoryInfo( + recorder: Recorder, + parentDirectory: this, + fullName: path.Substring(0, endSegment + 1), + name: name, + paths: Paths); + } + } + } + } + + private int NextIndex(string pattern, char[] anyOf, int startIndex, int endIndex) + { + var index = pattern.IndexOfAny(anyOf, startIndex, endIndex - startIndex); + return index == -1 ? endIndex : index; + } + + public override DirectoryInfoBase GetDirectory(string name) + { + if (string.Equals(name, "..", StringComparison.Ordinal)) + { + var indexOfPenultimateSlash = FullName.LastIndexOf('\\', FullName.Length - 2); + var fullName = FullName.Substring(0, indexOfPenultimateSlash + 1); + return new MockDirectoryInfo( + recorder: Recorder, + parentDirectory: this, + fullName: FullName.Substring(0, indexOfPenultimateSlash + 1), + name: name, + paths: Paths); + } + return new MockDirectoryInfo( + recorder: Recorder, + parentDirectory: this, + fullName: FullName + name + "\\", + name: name, + paths: Paths); + } + + public override FileInfoBase GetFile(string name) + { + return new MockFileInfo( + recorder: Recorder, + parentDirectory: this, + fullName: FullName + name, + name: name); + } + } +} diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockFileInfoStub.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockFileInfoStub.cs new file mode 100644 index 000000000..a63cfaf70 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockFileInfoStub.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Abstractions; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility +{ + internal class MockFileInfo : FileInfoBase + { + public MockFileInfo( + FileSystemOperationRecorder recorder, + DirectoryInfoBase parentDirectory, + string fullName, + string name) + { + Recorder = recorder; + FullName = fullName; + Name = name; + } + + public FileSystemOperationRecorder Recorder { get; } + + public override DirectoryInfoBase ParentDirectory { get; } + + public override string FullName { get; } + + public override string Name { get; } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockLinearPatternBuilder.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockLinearPatternBuilder.cs new file mode 100644 index 000000000..c22b151e2 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockLinearPatternBuilder.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternContexts +{ + internal class MockLinearPatternBuilder + { + private List _segments; + + public static MockLinearPatternBuilder New() + { + return new MockLinearPatternBuilder(); + } + + private MockLinearPatternBuilder() + { + _segments = new List(); + } + + public MockLinearPatternBuilder Add(string value) + { + _segments.Add(new MockNonRecursivePathSegment(value)); + + return this; + } + + public MockLinearPatternBuilder Add(string[] values) + { + _segments.AddRange(values.Select(v => new MockNonRecursivePathSegment(v))); + + return this; + } + + public ILinearPattern Build() + { + return new MockLinearPattern(_segments); + } + + class MockLinearPattern : ILinearPattern + { + public MockLinearPattern(List segments) + { + Segments = segments; + } + + public IList Segments { get; } + + public IPatternContext CreatePatternContextForExclude() + { + throw new NotImplementedException(); + } + + public IPatternContext CreatePatternContextForInclude() + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockNonRecursivePathSegment.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockNonRecursivePathSegment.cs new file mode 100644 index 000000000..8faf51f24 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockNonRecursivePathSegment.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternContexts +{ + internal class MockNonRecursivePathSegment : IPathSegment + { + private readonly StringComparison _comparisonType; + + public MockNonRecursivePathSegment(StringComparison comparisonType) + { + _comparisonType = comparisonType; + } + + public MockNonRecursivePathSegment(string value) + { + Value = value; + } + + public bool CanProduceStem { get { return false; } } + + public string Value { get; } + + public bool Match(string value) + { + return string.Compare(Value, value, _comparisonType) == 0; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockRaggedPatternBuilder.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockRaggedPatternBuilder.cs new file mode 100644 index 000000000..46133ed78 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockRaggedPatternBuilder.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternContexts +{ + internal class MockRaggedPatternBuilder + { + private MockRaggedPattern _result; + + public static MockRaggedPatternBuilder New() + { + return new MockRaggedPatternBuilder(); + } + + private MockRaggedPatternBuilder() + { + _result = new MockRaggedPattern(); + } + + public MockRaggedPatternBuilder AddStart(params string[] values) + { + foreach (var value in values) + { + var segment = new MockNonRecursivePathSegment(value); + _result.StartsWith.Add(segment); + _result.Segments.Add(segment); + } + + return this; + } + + public MockRaggedPatternBuilder AddContainsGroup(params string[] values) + { + var last = _result.Segments.Last(); + + if (!(last is MockRecursivePathSegment)) + { + AddRecursive(); + } + + var containSegment = new List(); + foreach (var value in values) + { + var segment = new MockNonRecursivePathSegment(value); + containSegment.Add(segment); + _result.Segments.Add(segment); + } + + _result.Contains.Add(containSegment); + + AddRecursive(); + + return this; + } + + public MockRaggedPatternBuilder AddEnd(params string[] values) + { + foreach (var value in values) + { + _result.EndsWith.Add(new MockNonRecursivePathSegment(value)); + } + + return this; + } + + public MockRaggedPatternBuilder AddRecursive() + { + _result.Segments.Add(new MockRecursivePathSegment()); + + return this; + } + + public IRaggedPattern Build() + { + return _result; + } + + class MockRaggedPattern : IRaggedPattern + { + public IList Segments { get; } = new List(); + + public IList StartsWith { get; } = new List(); + + public IList> Contains { get; } = new List>(); + + public IList EndsWith { get; } = new List(); + + public IPatternContext CreatePatternContextForExclude() + { + throw new NotImplementedException(); + } + + public IPatternContext CreatePatternContextForInclude() + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockRecursivePathSegment.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockRecursivePathSegment.cs new file mode 100644 index 000000000..cf00e6c21 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/MockRecursivePathSegment.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.PatternContexts +{ + internal class MockRecursivePathSegment : IPathSegment + { + public bool CanProduceStem { get { return false; } } + + public bool Match(string value) + { + return false; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/PatternContextHelper.cs b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/PatternContextHelper.cs new file mode 100644 index 000000000..8a14610c7 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/FileSystemGlobbing/TestUtility/PatternContextHelper.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Internal; + +namespace Microsoft.DotNet.ProjectModel.FileSystemGlobbing.Tests.TestUtility +{ + internal static class PatternContextHelper + { + public static void PushDirectory(IPatternContext context, params string[] directoryNames) + { + foreach (var each in directoryNames) + { + var directory = new MockDirectoryInfo(null, null, string.Empty, each, null); + context.PushDirectory(directory); + } + } + } +} \ No newline at end of file diff --git a/test/dotnet-projectmodel-server.Tests/DthTestClient.cs b/test/dotnet-projectmodel-server.Tests/DthTestClient.cs index 177966963..81def3636 100644 --- a/test/dotnet-projectmodel-server.Tests/DthTestClient.cs +++ b/test/dotnet-projectmodel-server.Tests/DthTestClient.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Concurrent; diff --git a/test/dotnet-projectmodel-server.Tests/DthTestServer.cs b/test/dotnet-projectmodel-server.Tests/DthTestServer.cs index 13ca67811..9c17737d2 100644 --- a/test/dotnet-projectmodel-server.Tests/DthTestServer.cs +++ b/test/dotnet-projectmodel-server.Tests/DthTestServer.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Net; diff --git a/test/dotnet-projectmodel-server.Tests/DthTests.cs b/test/dotnet-projectmodel-server.Tests/DthTests.cs index db86b12f6..707b2096b 100644 --- a/test/dotnet-projectmodel-server.Tests/DthTests.cs +++ b/test/dotnet-projectmodel-server.Tests/DthTests.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; diff --git a/test/dotnet-projectmodel-server.Tests/Helpers/DthMessage.cs b/test/dotnet-projectmodel-server.Tests/Helpers/DthMessage.cs index c0a4eee22..b0360d6b3 100644 --- a/test/dotnet-projectmodel-server.Tests/Helpers/DthMessage.cs +++ b/test/dotnet-projectmodel-server.Tests/Helpers/DthMessage.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; using Newtonsoft.Json.Linq; diff --git a/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageCollectionExtension.cs b/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageCollectionExtension.cs index e2f0c4dde..4a8545c28 100644 --- a/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageCollectionExtension.cs +++ b/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageCollectionExtension.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; diff --git a/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageExtension.cs b/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageExtension.cs index d14b4efcb..6230884c9 100644 --- a/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageExtension.cs +++ b/test/dotnet-projectmodel-server.Tests/Helpers/DthMessageExtension.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using Newtonsoft.Json.Linq; diff --git a/test/dotnet-projectmodel-server.Tests/Helpers/JArrayExtensions.cs b/test/dotnet-projectmodel-server.Tests/Helpers/JArrayExtensions.cs index 8dcd21861..fb925fe72 100644 --- a/test/dotnet-projectmodel-server.Tests/Helpers/JArrayExtensions.cs +++ b/test/dotnet-projectmodel-server.Tests/Helpers/JArrayExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using Newtonsoft.Json.Linq; diff --git a/test/dotnet-projectmodel-server.Tests/Helpers/JObjectExtensions.cs b/test/dotnet-projectmodel-server.Tests/Helpers/JObjectExtensions.cs index 4d944ac79..aa513917c 100644 --- a/test/dotnet-projectmodel-server.Tests/Helpers/JObjectExtensions.cs +++ b/test/dotnet-projectmodel-server.Tests/Helpers/JObjectExtensions.cs @@ -1,5 +1,5 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using Newtonsoft.Json.Linq;