Ensure results of GlobbingUrlBuilder are sorted:

- #2062
This commit is contained in:
damianedwards 2015-02-24 15:07:36 -08:00
parent 9b69e6f234
commit e282d2a861
2 changed files with 313 additions and 12 deletions

View File

@ -20,11 +20,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
private static readonly char[] PatternSeparator = new[] { ',' };
private static readonly PathComparer DefaultPathComparer = new PathComparer();
private readonly FileProviderGlobbingDirectory _baseGlobbingDirectory;
// Internal for testing
internal GlobbingUrlBuilder() { }
/// <summary>
/// Creates a new <see cref="GlobbingUrlBuilder"/>.
/// </summary>
@ -95,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
return Enumerable.Empty<string>();
}
if (Cache != null)
{
var cacheKey = $"{nameof(GlobbingUrlBuilder)}-inc:{include}-exc:{exclude}";
@ -127,7 +129,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var matches = matcher.Execute(_baseGlobbingDirectory);
return matches.Files.Select(ResolveMatchedPath);
return matches.Files.Select(ResolveMatchedPath)
.OrderBy(path => path, DefaultPathComparer);
}
private string ResolveMatchedPath(string matchedPath)
@ -137,6 +140,72 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
return RequestPathBase.Add(relativePath).ToString();
}
private class PathComparer : IComparer<string>
{
public int Compare(string x, string y)
{
// < 0 = x < y
// > 0 = x > y
if (string.Equals(x, y, StringComparison.Ordinal))
{
return 0;
}
if (string.IsNullOrEmpty(x) || string.IsNullOrEmpty(y))
{
return string.Compare(x, y, StringComparison.Ordinal);
}
var xExtIndex = x.LastIndexOf('.');
var yExtIndex = y.LastIndexOf('.');
// Ensure extension index is in the last segment, i.e. in the file name
var xSlashIndex = x.LastIndexOf('/');
var ySlashIndex = y.LastIndexOf('/');
xExtIndex = xExtIndex > xSlashIndex ? xExtIndex : -1;
yExtIndex = yExtIndex > ySlashIndex ? yExtIndex : -1;
// Get paths without their extensions, if they have one
var xNoExt = xExtIndex >= 0 ? x.Substring(0, xExtIndex) : x;
var yNoExt = yExtIndex >= 0 ? y.Substring(0, yExtIndex) : y;
if (string.Equals(xNoExt, yNoExt, StringComparison.Ordinal))
{
// Only extension differs so just compare the extension
var xExt = xExtIndex >= 0 ? x.Substring(xExtIndex) : string.Empty;
var yExt = yExtIndex >= 0 ? y.Substring(yExtIndex) : string.Empty;
return string.Compare(xExt, yExt, StringComparison.Ordinal);
}
var xSegments = xNoExt.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var ySegments = yNoExt.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (xSegments.Length != ySegments.Length)
{
// Different path depths so shallower path wins
return xSegments.Length.CompareTo(ySegments.Length);
}
// Depth is the same so compare each segment
for (int i = 0; i < xSegments.Length; i++)
{
var xSegment = xSegments[i];
var ySegment = ySegments[i];
var xToY = string.Compare(xSegment, ySegment, StringComparison.Ordinal);
if (xToY != 0)
{
return xToY;
}
}
// Should't get here, but if we do, hey, they're the same :)
return 0;
}
}
private static string TrimLeadingSlash(string value)
{
var result = value;

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Http;
@ -51,6 +52,179 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
url => Assert.Equal("/blank.css", url));
}
public static TheoryData OrdersGlobbedMatchResultsCorrectly_Data
{
get
{
return new TheoryData<string, FileNode, string[]>
{
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("B", new [] {
new FileNode("a.css"),
new FileNode("b.css"),
new FileNode("ba.css"),
new FileNode("b", new [] {
new FileNode("a.css")
})
}),
new FileNode("A", new [] {
new FileNode("c.css"),
new FileNode("d.css")
}),
new FileNode("a.css")
}),
/* expectedPaths */ new []
{
"/site.css",
"/a.css",
"/A/c.css", "/A/d.css",
"/B/a.css", "/B/b.css", "/B/ba.css",
"/B/b/a.css"
}
},
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("A", new [] {
new FileNode("c.css"),
new FileNode("d.css")
}),
new FileNode("_A", new [] {
new FileNode("1.css"),
new FileNode("2.css")
}),
new FileNode("__A", new [] {
new FileNode("1.css"),
new FileNode("_1.css")
})
}),
/* expectedPaths */ new []
{
"/site.css",
"/A/c.css", "/A/d.css",
"/_A/1.css", "/_A/2.css",
"/__A/1.css", "/__A/_1.css"
}
},
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("A", new [] {
new FileNode("a.b.css"),
new FileNode("a-b.css"),
new FileNode("a_b.css"),
new FileNode("a.css"),
new FileNode("a.c.css")
})
}),
/* expectedPaths */ new []
{
"/site.css",
"/A/a.css", "/A/a-b.css", "/A/a.b.css", "/A/a.c.css", "/A/a_b.css"
}
},
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("B", new [] {
new FileNode("a.bss"),
new FileNode("a.css")
}),
new FileNode("A", new [] {
new FileNode("a.css"),
new FileNode("a.bss")
})
}),
/* expectedPaths */ new []
{
"/site.css",
"/A/a.bss", "/A/a.css",
"/B/a.bss", "/B/a.css"
}
},
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("B", new [] {
new FileNode("site2.css"),
new FileNode("site11.css")
}),
new FileNode("A", new [] {
new FileNode("site2.css"),
new FileNode("site11.css")
})
}),
/* expectedPaths */ new []
{
"/site.css",
"/A/site11.css", "/A/site2.css",
"/B/site11.css", "/B/site2.css"
}
},
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("B", new [] {
new FileNode("site"),
new FileNode("site.css")
}),
new FileNode("A", new [] {
new FileNode("site.css"),
new FileNode("site")
})
}),
/* expectedPaths */ new []
{
"/site.css",
"/A/site", "/A/site.css",
"/B/site", "/B/site.css"
}
},
{
/* staticUrl */ "/site.css",
/* dirStructure */ new FileNode(null, new [] {
new FileNode("B.B", new [] {
new FileNode("site"),
new FileNode("site.css")
}),
new FileNode("A.A", new [] {
new FileNode("site.css"),
new FileNode("site")
})
}),
/* expectedPaths */ new []
{
"/site.css",
"/A.A/site", "/A.A/site.css",
"/B.B/site", "/B.B/site.css"
}
}
};
}
}
[Theory]
[MemberData(nameof(OrdersGlobbedMatchResultsCorrectly_Data))]
public void OrdersGlobbedMatchResultsCorrectly(string staticUrl, FileNode dirStructure, string[] expectedPaths)
{
// Arrange
var fileProvider = MakeFileProvider(dirStructure);
IMemoryCache cache = null;
var requestPathBase = PathString.Empty;
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
// Act
var urlList = globbingUrlBuilder.BuildUrlList(staticUrl, "**/*.*", excludePattern: null);
// Assert
var collectionAssertions = expectedPaths.Select<string, Action<string>>(expected =>
actual => Assert.Equal(expected, actual));
Assert.Collection(urlList, collectionAssertions.ToArray());
}
[Theory]
[InlineData("/sub")]
[InlineData("/sub/again")]
@ -70,8 +244,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
// Assert
Assert.Collection(urlList,
url => Assert.Equal($"{pathBase}/site.css", url),
url => Assert.Equal($"{pathBase}/blank.css", url));
url => Assert.Equal($"{pathBase}/blank.css", url),
url => Assert.Equal($"{pathBase}/site.css", url));
}
[Fact]
@ -79,7 +253,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
// Arrange
var fileProvider = MakeFileProvider();
var cache = MakeCache(new List<string> { "/site.css", "/blank.css" });
var cache = MakeCache(new List<string> { "/blank.css", "/site.css" });
var requestPathBase = PathString.Empty;
var globbingUrlBuilder = new GlobbingUrlBuilder(fileProvider, cache, requestPathBase);
@ -91,8 +265,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
// Assert
Assert.Collection(urlList,
url => Assert.Equal("/site.css", url),
url => Assert.Equal("/blank.css", url));
url => Assert.Equal("/blank.css", url),
url => Assert.Equal("/site.css", url));
}
[Fact]
@ -128,8 +302,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
// Assert
Assert.Collection(urlList,
url => Assert.Equal("/site.css", url),
url => Assert.Equal("/blank.css", url));
url => Assert.Equal("/blank.css", url),
url => Assert.Equal("/site.css", url));
cacheSetContext.VerifyAll();
Mock.Get(cache).VerifyAll();
}
@ -187,13 +361,71 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
Assert.Collection(excludePatterns, pattern => Assert.Equal($"{leadingSlash}**/*.min.css", pattern));
}
private static IFileInfo MakeFileInfo(string name)
public class FileNode
{
public FileNode(string name)
{
Name = name;
}
public FileNode(string name, IList<FileNode> children)
{
Name = name;
Children = children;
}
public string Name { get; }
public IList<FileNode> Children { get; }
public bool IsDirectory => Children != null && Children.Any();
}
private static IFileInfo MakeFileInfo(string name, bool isDirectory = false)
{
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.Name).Returns(name);
fileInfo.Setup(f => f.IsDirectory).Returns(isDirectory);
return fileInfo.Object;
}
private static IFileProvider MakeFileProvider(FileNode rootNode)
{
if (rootNode.Children == null || !rootNode.Children.Any())
{
throw new ArgumentException(nameof(rootNode));
}
var fileProvider = new Mock<IFileProvider>(MockBehavior.Strict);
fileProvider.Setup(fp => fp.GetDirectoryContents(string.Empty))
.Returns(MakeDirectoryContents(rootNode, fileProvider));
return fileProvider.Object;
}
private static IDirectoryContents MakeDirectoryContents(FileNode fileNode, Mock<IFileProvider> fileProviderMock)
{
var children = new List<IFileInfo>();
foreach (var node in fileNode.Children)
{
children.Add(MakeFileInfo(node.Name, node.IsDirectory));
if (node.IsDirectory)
{
var subPath = fileNode.Name != null
? (fileNode.Name + "/" + node.Name)
: node.Name;
fileProviderMock.Setup(fp => fp.GetDirectoryContents(subPath))
.Returns(MakeDirectoryContents(node, fileProviderMock));
}
}
var directoryContents = new Mock<IDirectoryContents>();
directoryContents.Setup(dc => dc.GetEnumerator()).Returns(children.GetEnumerator());
return directoryContents.Object;
}
private static IDirectoryContents MakeDirectoryContents(params string[] fileNames)
{
var files = fileNames.Select(name => MakeFileInfo(name));
@ -215,7 +447,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
.Returns(directoryContents);
return fileProvider.Object;
}
private static IMemoryCache MakeCache(object result = null)
{
var cache = new Mock<IMemoryCache>();