diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..19be10e2fa --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,30 @@ +This is a .NET based repository that contains the VSTest test platform. Please follow these guidelines when contributing: + +## Code Standards + +You MUST follow all code-formatting and naming conventions defined in [`.editorconfig`](../.editorconfig). + +In addition to the rules enforced by `.editorconfig`, you SHOULD: + +- Favor style and conventions that are consistent with the existing codebase. +- Prefer file-scoped namespace declarations and single-line using directives. +- Ensure that the final return statement of a method is on its own line. +- Use pattern matching and switch expressions wherever possible. +- Use `nameof` instead of string literals when referring to member names. +- Always use `is null` or `is not null` instead of `== null` or `!= null`. +- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null. +- Prefer `?.` if applicable (e.g. `scope?.Dispose()`). +- Use `ObjectDisposedException.ThrowIf` where applicable. +- Respect StyleCop.Analyzers rules, in particular: + - SA1028: Code must not contain trailing whitespace + - SA1316: Tuple element names should use correct casing + - SA1518: File is required to end with a single newline character + +You MUST minimize adding public API surface area but any newly added public API MUST be declared in the related `PublicAPI.Unshipped.txt` file. + +## Localization Guidelines + +Anytime you add a new localization resource, you MUST: +- Add a corresponding entry in the localization resource file. +- Add an entry in all `*.xlf` files related to the modified `.resx` file. +- Do not modify existing entries in '*.xlf' files unless you are also modifying the corresponding `.resx` file. diff --git a/src/vstest.console/Internal/FilePatternParser.cs b/src/vstest.console/Internal/FilePatternParser.cs index 804739ca7e..9dfcd08678 100644 --- a/src/vstest.console/Internal/FilePatternParser.cs +++ b/src/vstest.console/Internal/FilePatternParser.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Runtime.InteropServices; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; @@ -96,7 +97,24 @@ private Tuple SplitFilePatternOnWildCard(string filePattern) { // Split the pattern based on first wild card character found. var splitOnWildCardIndex = filePattern.IndexOfAny(_wildCardCharacters); - var directorySeparatorIndex = filePattern.Substring(0, splitOnWildCardIndex).LastIndexOf(Path.DirectorySeparatorChar); + 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); diff --git a/test/vstest.console.UnitTests/Internal/FilePatternParserTests.cs b/test/vstest.console.UnitTests/Internal/FilePatternParserTests.cs index 571936d859..4c7e7e0f29 100644 --- a/test/vstest.console.UnitTests/Internal/FilePatternParserTests.cs +++ b/test/vstest.console.UnitTests/Internal/FilePatternParserTests.cs @@ -114,6 +114,61 @@ public void FilePatternParserShouldThrowCommandLineExceptionIfFileDoesNotExist() Assert.ThrowsException(() => _filePatternParser.GetMatchingFiles(TranslatePath(@"E:\path\to\project\tests\Blame.Tests\\abc.Tests.dll"))); } + [TestMethod] + // only on windows because we don't translate the path to be valid linux / mac path + [OSCondition(OperatingSystems.Windows)] + public void FilePatternParserShouldCorrectlySplitPatternAndDirectoryWithForwardSlashes() + { + var patternMatchingResult = new PatternMatchingResult(new List()); + _mockMatcherHelper.Setup(x => x.Execute(It.IsAny())).Returns(patternMatchingResult); + + // Test with forward slashes - this should work on all platforms + // This specifically tests the fix for issue #14993 + _filePatternParser.GetMatchingFiles("C:/Users/someUser/Desktop/a/c/*bc.dll"); + + // Assert that the pattern is parsed correctly + _mockMatcherHelper.Verify(x => x.AddInclude("*bc.dll")); + // On Windows, the path may be normalized, so we verify the key components are present + _mockMatcherHelper.Verify(x => x.Execute(It.Is(y => + y.FullName.Contains("someUser") && y.FullName.Contains("Desktop") && + y.FullName.Contains("a") && y.FullName.EndsWith("c")))); + } + + [TestMethod] + // only on windows because we don't translate the path to be valid linux / mac path + [OSCondition(OperatingSystems.Windows)] + public void FilePatternParserShouldCorrectlySplitWithArbitraryDirectoryDepthWithForwardSlashes() + { + var patternMatchingResult = new PatternMatchingResult(new List()); + _mockMatcherHelper.Setup(x => x.Execute(It.IsAny())).Returns(patternMatchingResult); + + // Test with forward slashes and recursive patterns + _filePatternParser.GetMatchingFiles("C:/Users/someUser/**/c/*bc.txt"); + + // Assert + _mockMatcherHelper.Verify(x => x.AddInclude("**/c/*bc.txt")); + _mockMatcherHelper.Verify(x => x.Execute(It.Is(y => + y.FullName.Contains("someUser")))); + } + + [TestMethod] + // only on windows because we don't translate the path to be valid linux / mac path + [OSCondition(OperatingSystems.Windows)] + public void FilePatternParserShouldHandleForwardSlashesWithoutThrowingException() + { + var patternMatchingResult = new PatternMatchingResult(new List()); + _mockMatcherHelper.Setup(x => x.Execute(It.IsAny())).Returns(patternMatchingResult); + + // This is the specific case from the original bug report that was throwing ArgumentOutOfRangeException + // Before the fix: System.ArgumentOutOfRangeException: length ('-1') must be a non-negative value + _filePatternParser.GetMatchingFiles("C:/path/to/my/tests/*_Tests.dll"); + + // Assert that we successfully parse without throwing and get the expected pattern + _mockMatcherHelper.Verify(x => x.AddInclude("*_Tests.dll")); + _mockMatcherHelper.Verify(x => x.Execute(It.Is(y => + y.FullName.Contains("path") && y.FullName.Contains("tests")))); + } + private static string TranslatePath(string path) { // RuntimeInformation has conflict when used