From d1628f7efe497634bd57848c148dbf0b2d3753fa Mon Sep 17 00:00:00 2001 From: Jonathan Bout Date: Wed, 4 Dec 2024 12:33:25 +0100 Subject: [PATCH] Benchmarks and more unit tests, plus resulting bug fixes (#23) * Benchmarks and more unit tests, plus resulting bug fixes * change benchmarks * error suppression was not needed anymore --- SimpleCDN.Benchmarks/Benchmarks.cs | 45 ++++++++++++++ SimpleCDN.Benchmarks/Program.cs | 4 ++ .../SimpleCDN.Benchmarks.csproj | 18 ++++++ SimpleCDN.Tests/NormalizeTests.cs | 28 +++++++++ SimpleCDN.sln | 7 ++- SimpleCDN/Helpers/EnumerableExtensions.cs | 23 ++++++++ SimpleCDN/Helpers/Extensions.cs | 58 +++++++++---------- 7 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 SimpleCDN.Benchmarks/Benchmarks.cs create mode 100644 SimpleCDN.Benchmarks/Program.cs create mode 100644 SimpleCDN.Benchmarks/SimpleCDN.Benchmarks.csproj create mode 100644 SimpleCDN.Tests/NormalizeTests.cs create mode 100644 SimpleCDN/Helpers/EnumerableExtensions.cs diff --git a/SimpleCDN.Benchmarks/Benchmarks.cs b/SimpleCDN.Benchmarks/Benchmarks.cs new file mode 100644 index 0000000..f1f3220 --- /dev/null +++ b/SimpleCDN.Benchmarks/Benchmarks.cs @@ -0,0 +1,45 @@ +using BenchmarkDotNet.Attributes; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SimpleCDN.Benchmarks +{ + [MemoryDiagnoser(false)] + public class Benchmarks + { + public IEnumerable Paths => + [ + "/".ToCharArray(), + "/../".ToCharArray(), + "/data/../".ToCharArray(), + "/test.txt".ToCharArray(), + "/data/../test.txt".ToCharArray(), + "/data/test.json".ToCharArray(), + "/data/../data/test.json".ToCharArray(), + "/data/./../data/test.json".ToCharArray(), + "/data/data/../../data/test.json".ToCharArray(), + "/data/data/../../data/../data/test.json".ToCharArray(), + "/data/data/../../data/../data/../data/test.json".ToCharArray(), + "/data/data/.././data/../../../data/../data/test.json".ToCharArray(), + ]; + + [ParamsSource(nameof(Paths))] + public char[] Path { get; set; } = []; + + const int NormalizationBenchmarkIterationsPerInvoke = 100; + + [Benchmark(OperationsPerInvoke = NormalizationBenchmarkIterationsPerInvoke)] + public void NormalizationBenchmark() + { + var span = Path.AsSpan(); + for (var i = 0; i < NormalizationBenchmarkIterationsPerInvoke; i++) + { + Helpers.Extensions.Normalize(ref span); + } + } + } +} diff --git a/SimpleCDN.Benchmarks/Program.cs b/SimpleCDN.Benchmarks/Program.cs new file mode 100644 index 0000000..e5d12b5 --- /dev/null +++ b/SimpleCDN.Benchmarks/Program.cs @@ -0,0 +1,4 @@ +using BenchmarkDotNet.Running; +using SimpleCDN.Benchmarks; + +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/SimpleCDN.Benchmarks/SimpleCDN.Benchmarks.csproj b/SimpleCDN.Benchmarks/SimpleCDN.Benchmarks.csproj new file mode 100644 index 0000000..ab6926e --- /dev/null +++ b/SimpleCDN.Benchmarks/SimpleCDN.Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + diff --git a/SimpleCDN.Tests/NormalizeTests.cs b/SimpleCDN.Tests/NormalizeTests.cs new file mode 100644 index 0000000..b2f2116 --- /dev/null +++ b/SimpleCDN.Tests/NormalizeTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SimpleCDN.Tests +{ + [TestFixture] + public class NormalizeTests + { + [TestCase("/aaaa/../bbbb", "/bbbb")] + [TestCase("/aaaa/./bbbb", "/aaaa/bbbb")] + [TestCase("/aaaa/./bbbb/./cccc", "/aaaa/bbbb/cccc")] + [TestCase("/../aaaa/./bbbb/./cccc", "/aaaa/bbbb/cccc")] + [TestCase("/aaaa/./bbbb/./cccc/..", "/aaaa/bbbb")] + [TestCase("/aaaa/./bbbb/./cccc/../../dddd", "/aaaa/dddd")] + public void Test_Normalize_Normalizes(string path, string expected) + { + var pathChars = path.ToCharArray(); + var span = pathChars.AsSpan(); + + Helpers.Extensions.Normalize(ref span); + + Assert.That(span.ToString(), Is.EqualTo(expected)); + } + } +} diff --git a/SimpleCDN.sln b/SimpleCDN.sln index 2ee3494..8b1f9e5 100644 --- a/SimpleCDN.sln +++ b/SimpleCDN.sln @@ -10,6 +10,8 @@ Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleCDN.Tests.Integration", "SimpleCDN.Tests.Integration\SimpleCDN.Tests.Integration.csproj", "{F79E71E8-89D8-46F7-802C-CFDF3A77447D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleCDN.Benchmarks", "SimpleCDN.Benchmarks\SimpleCDN.Benchmarks.csproj", "{73BC9966-46FF-40D8-89F4-C990B15370A2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,9 +31,12 @@ Global {81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.Build.0 = Release|Any CPU {F79E71E8-89D8-46F7-802C-CFDF3A77447D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F79E71E8-89D8-46F7-802C-CFDF3A77447D}.Debug|Any CPU.Build.0 = Debug|Any CPU {F79E71E8-89D8-46F7-802C-CFDF3A77447D}.Release|Any CPU.ActiveCfg = Release|Any CPU {F79E71E8-89D8-46F7-802C-CFDF3A77447D}.Release|Any CPU.Build.0 = Release|Any CPU + {73BC9966-46FF-40D8-89F4-C990B15370A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73BC9966-46FF-40D8-89F4-C990B15370A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73BC9966-46FF-40D8-89F4-C990B15370A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73BC9966-46FF-40D8-89F4-C990B15370A2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SimpleCDN/Helpers/EnumerableExtensions.cs b/SimpleCDN/Helpers/EnumerableExtensions.cs new file mode 100644 index 0000000..10d8d6b --- /dev/null +++ b/SimpleCDN/Helpers/EnumerableExtensions.cs @@ -0,0 +1,23 @@ +namespace SimpleCDN.Helpers +{ + public static class EnumerableExtensions + { + public static (T left, IEnumerable right) RemoveFirst(this IEnumerable source) + { + var enumerator = source.GetEnumerator(); + + if (!enumerator.MoveNext()) + ArgumentOutOfRangeException.ThrowIfLessThan(0, 1, nameof(source)); + + return (enumerator.Current, RestEnumerator(enumerator)); + + static IEnumerable RestEnumerator(IEnumerator enumerator) + { + while (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + } + } + } +} diff --git a/SimpleCDN/Helpers/Extensions.cs b/SimpleCDN/Helpers/Extensions.cs index 6fc842e..0eeef2f 100644 --- a/SimpleCDN/Helpers/Extensions.cs +++ b/SimpleCDN/Helpers/Extensions.cs @@ -34,64 +34,54 @@ public static string FormatByteCount(this long number) return $"{result:0.##}{sizeNames[sizeNameIndex]}B"; } - public static (T left, IEnumerable right) RemoveFirst(this IEnumerable source) - { - var enumerator = source.GetEnumerator(); - - if (!enumerator.MoveNext()) - ArgumentOutOfRangeException.ThrowIfLessThan(0, 1, nameof(source)); - - return (enumerator.Current, RestEnumerator(enumerator)); - - static IEnumerable RestEnumerator(IEnumerator enumerator) - { - while (enumerator.MoveNext()) - { - yield return enumerator.Current; - } - } - } - + /// + /// Normalizes a path by removing all . and .. segments in-place. When ready, + /// will be shortened to contain just the normalized path. + /// + /// The path to normalize in-place public static void Normalize(ref this Span path) { var segments = MemoryExtensions.Split(path, '/'); - var segmentsToRemove = new List(); + var rangesToRemove = new List(); - Range lastSegment = Range.All; + var resultRanges = new Stack(); + + var originalLength = path.Length; foreach (var segment in segments) { - if (segment.GetOffsetAndLength(path.Length).Length == 0) + if (segment.GetOffsetAndLength(originalLength).Length == 0) { continue; } if (path[segment] is ['.', '.']) { - if (!lastSegment.Equals(Range.All)) + if (resultRanges.TryPop(out Range lastSegment)) { - segmentsToRemove.Add(lastSegment); + rangesToRemove.Add(lastSegment); } - segmentsToRemove.Add(segment); + rangesToRemove.Add(segment); } else if (path[segment] is ['.']) { // if the segment is . it should be removed - segmentsToRemove.Add(segment); + rangesToRemove.Add(segment); + } else + { + resultRanges.Push(segment); } - - lastSegment = segment; } int offset = 0; - foreach (var segmentToRemove in segmentsToRemove) - { - // transform path, so that + var segmentsToRemove = rangesToRemove.Select(s => s.GetOffsetAndLength(originalLength)).OrderBy(s => s.Offset); - var (start, length) = segmentToRemove.GetOffsetAndLength(path.Length); + foreach (var segment in segmentsToRemove) + { + var (start, length) = segment; // include the / before the segment // and subtract the offset to account for previously removed segments @@ -123,6 +113,12 @@ public static void Normalize(ref this Span path) } } + /// + /// Sanitizes a string for use in log messages:
+ /// - replaces all whitespace (including newlines, tabs, ...) with a single space + ///
+ /// + /// public static string ForLog(this string input) => WhitespaceRegex().Replace(input, " "); [GeneratedRegex(@"\s+", RegexOptions.Multiline | RegexOptions.Compiled)]