File: Internal\FilePatternParser.cs
Web Access
Project: src\src\vstest\src\vstest.console\vstest.console.csproj (vstest.console)
// Copyright (c) Microsoft Corporation. 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.Globalization;
using System.IO;
using System.Runtime.InteropServices;

using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
using Microsoft.VisualStudio.TestPlatform.CommandLine;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;

using CommandLineResources = Microsoft.VisualStudio.TestPlatform.CommandLine.Resources.Resources;

#pragma warning disable IDE1006 // Naming Styles
namespace vstest.console.Internal;
#pragma warning restore IDE1006 // Naming Styles

/// <summary>
/// Class for getting matching files from wild card pattern file name
/// Microsoft.Extensions.FileSystemGlobbing methods used to get matching file names
/// </summary>
public class FilePatternParser
{
    private readonly Matcher _matcher;
    private readonly IFileHelper _fileHelper;
    private readonly char[] _wildCardCharacters = ['*'];

    public FilePatternParser()
        : this(new Matcher(), new FileHelper())
    {
    }

    internal FilePatternParser(Matcher matcher, IFileHelper fileHelper)
    {
        _matcher = matcher;
        _fileHelper = fileHelper;
    }

    /// <summary>
    /// Used to get matching files with pattern
    /// </summary>
    /// <returns>Returns the list of matching files</returns>
    public List<string> GetMatchingFiles(string filePattern)
    {
        var matchingFiles = new List<string>();

        // Convert the relative path to absolute path
        if (!Path.IsPathRooted(filePattern))
        {
            filePattern = Path.Combine(_fileHelper.GetCurrentDirectory(), filePattern);
        }

        // If there is no wild card simply add the file to the list of matching files.
        if (filePattern.IndexOfAny(_wildCardCharacters) == -1)
        {
            EqtTrace.Info($"FilePatternParser: The given file {filePattern} is a full path.");

            // Check if the file exists.
            if (!_fileHelper.Exists(filePattern))
            {
                throw new TestSourceException(
                    string.Format(CultureInfo.CurrentCulture, CommandLineResources.TestSourceFileNotFound, filePattern));
            }

            matchingFiles.Add(filePattern);

            return matchingFiles;
        }

        // Split the given wild card into search directory and pattern to be searched.
        var splitPattern = SplitFilePatternOnWildCard(filePattern);
        EqtTrace.Info($"FilePatternParser: Matching file pattern '{splitPattern.Item2}' within directory '{splitPattern.Item1}'");

        _matcher.AddInclude(splitPattern.Item2);

        // Execute the given pattern in the search directory.
        var matches = _matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(splitPattern.Item1)));

        // Add all the files to the list of matching files.
        foreach (var match in matches.Files)
        {
            matchingFiles.Add(Path.Combine(splitPattern.Item1, match.Path));
        }

        return matchingFiles;
    }

    /// <summary>
    /// Splits full pattern into search directory and pattern.
    /// </summary>
    private Tuple<string, string> SplitFilePatternOnWildCard(string filePattern)
    {
        // Split the pattern based on first wild card character found.
        var splitOnWildCardIndex = filePattern.IndexOfAny(_wildCardCharacters);
        var pathBeforeWildCard = filePattern.Substring(0, splitOnWildCardIndex);

        // Find the last directory separator before the wildcard
        // On Windows, we need to check both \ and / as both are valid
        // On Unix-like systems, only / is the directory separator
        int directorySeparatorIndex;
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            // On Windows, check both separators and take the last one found
            directorySeparatorIndex = Math.Max(
                pathBeforeWildCard.LastIndexOf(Path.DirectorySeparatorChar),
                pathBeforeWildCard.LastIndexOf(Path.AltDirectorySeparatorChar));
        }
        else
        {
            // On Unix-like systems, only use the forward slash
            directorySeparatorIndex = pathBeforeWildCard.LastIndexOf(Path.DirectorySeparatorChar);
        }

        string searchDir = filePattern.Substring(0, directorySeparatorIndex);
        string pattern = filePattern.Substring(directorySeparatorIndex + 1);

        Tuple<string, string> splitPattern = new(searchDir, pattern);
        return splitPattern;
    }
}