Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix input path directory handling #1842 #2684

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ What's changed since pre-release v3.0.0-B0351:
- Bug fixes:
- Fixed string formatting of semantic version and constraints by @BernieWhite.
[#1828](https://github.com/microsoft/PSRule/issues/1828)
- Fixed directory handling of input paths without trailing slash by @BernieWhite.
[#1842](https://github.com/microsoft/PSRule/issues/1842)

## v3.0.0-B0351 (pre-release)

Expand Down
8 changes: 5 additions & 3 deletions src/PSRule/Pipeline/InputPathBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

namespace PSRule.Pipeline;

internal sealed class InputPathBuilder : PathBuilder
/// <summary>
/// A builder for input paths.
/// </summary>
internal sealed class InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
: PathBuilder(logger, basePath, searchPattern, filter, required)
{
public InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
: base(logger, basePath, searchPattern, filter, required) { }
}
66 changes: 27 additions & 39 deletions src/PSRule/Pipeline/PathBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,7 @@

namespace PSRule.Pipeline;

//public interface IPathBuilder
//{
// void Add(string path);

// void Add(FileInfo[] fileInfo);

// void Add(PathInfo[] pathInfo);

// InputFileInfo[] Build();
//}

internal abstract class PathBuilder
internal abstract class PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
{
// Path separators
private const char Slash = '/';
Expand All @@ -28,27 +17,16 @@ internal abstract class PathBuilder
private const string CurrentPath = ".";
private const string RecursiveSearchOperator = "**";

private static readonly char[] PathLiteralStopCharacters = new char[] { '*', '[', '?' };
private static readonly char[] PathSeparatorCharacters = new char[] { '\\', '/' };
private static readonly char[] PathLiteralStopCharacters = ['*', '[', '?'];
private static readonly char[] PathSeparatorCharacters = ['\\', '/'];

private readonly IPipelineWriter _Logger;
private readonly List<InputFileInfo> _Files;
private readonly HashSet<string> _Paths;
private readonly string _BasePath;
private readonly string _DefaultSearchPattern;
private readonly PathFilter _GlobalFilter;
private readonly PathFilter _Required;

protected PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
{
_Logger = logger;
_Files = [];
_Paths = [];
_BasePath = NormalizePath(Environment.GetRootedBasePath(basePath));
_DefaultSearchPattern = searchPattern;
_GlobalFilter = filter;
_Required = required;
}
private readonly IPipelineWriter _Logger = logger;
private readonly List<InputFileInfo> _Files = [];
private readonly HashSet<string> _Paths = [];
private readonly string _BasePath = NormalizePath(Environment.GetRootedBasePath(basePath));
private readonly string _DefaultSearchPattern = searchPattern;
private readonly PathFilter _GlobalFilter = filter;
private readonly PathFilter _Required = required;

/// <summary>
/// The number of files found.
Expand Down Expand Up @@ -89,7 +67,7 @@ public InputFileInfo[] Build()
{
try
{
return _Files.ToArray();
return [.. _Files];
}
finally
{
Expand All @@ -105,9 +83,14 @@ private void FindFiles(string path)

var pathLiteral = GetSearchParameters(path, out var searchPattern, out var searchOption, out var filter);
var files = Directory.EnumerateFiles(pathLiteral, searchPattern, searchOption);

foreach (var file in files)
{
if (ShouldInclude(file, filter))
{
AddFile(file);
}
}
}

private bool TryUrl(string path)
Expand All @@ -128,9 +111,7 @@ private bool TryPath(string path, out string normalPath)
var rootedPath = GetRootedPath(path);
if (Directory.Exists(rootedPath) || path == CurrentPath)
{
if (IsBasePath(rootedPath))
normalPath = CurrentPath;

normalPath = IsBasePath(rootedPath) ? CurrentPath : NormalizeDirectoryPath(path);
return false;
}
if (!File.Exists(rootedPath))
Expand All @@ -144,8 +125,7 @@ private bool TryPath(string path, out string normalPath)

private bool IsBasePath(string path)
{
path = IsSeparator(path[path.Length - 1]) ? path : string.Concat(path, Path.DirectorySeparatorChar);
return NormalizePath(path) == _BasePath;
return NormalizeDirectoryPath(path) == _BasePath;
}

private void ErrorNotFound(string path)
Expand Down Expand Up @@ -251,12 +231,20 @@ private static bool IsSeparator(char c)
[DebuggerStepThrough]
private static bool UseSimpleSearch(string s)
{
return s.IndexOf(RecursiveSearchOperator, System.StringComparison.OrdinalIgnoreCase) == -1;
return s.IndexOf(RecursiveSearchOperator, StringComparison.OrdinalIgnoreCase) == -1;
}

[DebuggerStepThrough]
private static string NormalizePath(string path)
{
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}

[DebuggerStepThrough]
private static string NormalizeDirectoryPath(string path)
{
return NormalizePath(
IsSeparator(path[path.Length - 1]) ? path : string.Concat(path, Path.DirectorySeparatorChar)
);
}
}
98 changes: 0 additions & 98 deletions tests/PSRule.Tests/InputPathBuilderTests.cs

This file was deleted.

100 changes: 100 additions & 0 deletions tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Linq;

namespace PSRule.Pipeline;

/// <summary>
/// Tests for <see cref="InputPathBuilder"/>.
/// </summary>
public sealed class InputPathBuilderTests
{
[Theory]
[InlineData("./.github/*.yml", 1)]
[InlineData("./.github/**/*.yaml", 9)]
[InlineData("./.github/", 12)]
[InlineData(".github/", 12)]
[InlineData(".github", 12)]
[InlineData("./*.json", 8)]
public void Build_WithValidPathAdded_ShouldReturnFiles(string path, int expected)
{
var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
builder.Add(path);
var actual = builder.Build();

Assert.Equal(expected, actual.Length);
}

[Theory]
[InlineData(".")]
[InlineData("./")]
[InlineData("./src")]
[InlineData("./src/")]
[InlineData("src/")]
public void Build_WithValidPathAdded_ShouldReturnManyFiles(string path)
{
var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
builder.Add(path);
var actual = builder.Build();

Assert.True(actual.Length > 100);
}

[Fact]
public void Build_WithWorkingPathAdded_ShouldReturnFiles()
{
var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
builder.Add(GetWorkingPath());
var actual = builder.Build();

Assert.True(actual.Length > 100);
}

/// <summary>
/// Test that an invalid path is handled correctly.
/// Should not return any files, and should log an error.
/// </summary>
[Fact]
public void Build_WithInvalidPathAdded_ShouldReturnEmpty()
{
var writer = new TestWriter(new Configuration.PSRuleOption());
var builder = new InputPathBuilder(writer, GetWorkingPath(), "*", null, null);
builder.Add("ZZ://not/path");
var actual = builder.Build();

Assert.Empty(actual);
Assert.True(writer.Errors.Count(r => r.FullyQualifiedErrorId == "PSRule.ReadInputFailed") == 1);
}

[Fact]
public void GetPathRequired()
{
var required = PathFilter.Create(GetWorkingPath(), ["README.md"]);
var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required);
builder.Add(".");
var actual = builder.Build();
Assert.True(actual.Length == 1);

builder.Add(GetWorkingPath());
actual = builder.Build();
Assert.True(actual.Length == 1);

required = PathFilter.Create(GetWorkingPath(), ["**"]);
builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required);
builder.Add(".");
actual = builder.Build();
Assert.True(actual.Length > 100);
}

#region Helper methods

private static string GetWorkingPath()
{
return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../../.."));
}

#endregion Helper methods
}
Loading