From 7209cab5e91cc4c8d0c7b285fabae328a0a68c33 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 12 Jul 2018 23:28:51 -0700 Subject: [PATCH] Productize JumpTable (#594) * Productize JumpTable --- .../JumpTableMultipleEntryBenchmark.cs | 134 ++++++++++++++++++ .../Matchers/JumpTableSingleEntryBenchmark.cs | 78 ++++++++++ .../Matchers/JumpTableZeroEntryBenchmark.cs | 66 +++++++++ .../Matchers/DictionaryJumpTable.cs | 68 +++++++++ .../Matchers/JumpTable.cs | 18 +++ .../Matchers/JumpTableBuilder.cs | 62 ++++++++ .../Matchers/LinearSearchJumpTable.cs | 72 ++++++++++ .../Matchers/SingleEntryJumpTable.cs | 54 +++++++ .../Matchers/ZeroEntryJumpTable.cs | 27 ++++ .../Matchers/DfaMatcher.cs | 61 +------- .../Matchers/DfaMatcherBuilder.cs | 19 ++- .../Matchers/DictionaryJumpTableTest.cs | 16 +++ .../Matchers/LinearSearchJumpTableTest.cs | 17 +++ .../Matchers/MultipleEntryJumpTableTest.cs | 80 +++++++++++ .../Matchers/SingleEntryJumpTableTest.cs | 62 ++++++++ .../Matchers/ZeroEntryJumpTableTest.cs | 36 +++++ 16 files changed, 803 insertions(+), 67 deletions(-) create mode 100644 benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableMultipleEntryBenchmark.cs create mode 100644 benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableSingleEntryBenchmark.cs create mode 100644 benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableZeroEntryBenchmark.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/DictionaryJumpTable.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/JumpTable.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/JumpTableBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/LinearSearchJumpTable.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/SingleEntryJumpTable.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matchers/ZeroEntryJumpTable.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/DictionaryJumpTableTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/LinearSearchJumpTableTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/MultipleEntryJumpTableTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/SingleEntryJumpTableTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matchers/ZeroEntryJumpTableTest.cs diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableMultipleEntryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableMultipleEntryBenchmark.cs new file mode 100644 index 0000000000..87d70ae74d --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableMultipleEntryBenchmark.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class JumpTableMultipleEntryBenchmark + { + private string[] _strings; + private PathSegment[] _segments; + + private JumpTable _linearSearch; + private JumpTable _dictionary; + + // All factors of 100 to support sampling + [Params(2, 5, 10, 25, 50, 100)] + public int Count; + + [GlobalSetup] + public void Setup() + { + _strings = GetStrings(100); + _segments = new PathSegment[100]; + + for (var i = 0; i < _strings.Length; i++) + { + _segments[i] = new PathSegment(0, _strings[i].Length); + } + + var samples = new int[Count]; + for (var i = 0; i < samples.Length; i++) + { + samples[i] = i * (_strings.Length / Count); + } + + var entries = new List<(string text, int _)>(); + for (var i = 0; i < samples.Length; i++) + { + entries.Add((_strings[samples[i]], i)); + } + + _linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray()); + _dictionary = new DictionaryJumpTable(0, -1, entries.ToArray()); + } + + // This baseline is similar to SingleEntryJumpTable. We just want + // something stable to compare against. + [Benchmark(Baseline = true, OperationsPerInvoke = 100)] + public int Baseline() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + var @string = strings[i]; + var segment = segments[i]; + + destination = segment.Length == 0 ? -1 : + segment.Length != @string.Length ? 1 : + string.Compare( + @string, + segment.Start, + @string, + 0, + @string.Length, + StringComparison.OrdinalIgnoreCase); + } + + return destination; + } + + [Benchmark(OperationsPerInvoke = 100)] + public int LinearSearch() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _linearSearch.GetDestination(strings[i], segments[i]); + } + + return destination; + } + + [Benchmark(OperationsPerInvoke = 100)] + public int Dictionary() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _dictionary.GetDestination(strings[i], segments[i]); + } + + return destination; + } + + private static string[] GetStrings(int count) + { + var strings = new string[count]; + for (var i = 0; i < count; i++) + { + var guid = Guid.NewGuid().ToString(); + + // Between 5 and 36 characters + var text = guid.Substring(0, Math.Max(5, Math.Min(count, 36))); + if (char.IsDigit(text[0])) + { + // Convert first character to a letter. + text = ((char)(text[0] + ('G' - '0'))) + text.Substring(1); + } + + if (i % 2 == 0) + { + // Lowercase half of them + text = text.ToLowerInvariant(); + } + + strings[i] = text; + } + + return strings; + } + } +} diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableSingleEntryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableSingleEntryBenchmark.cs new file mode 100644 index 0000000000..0ab4bf3380 --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableSingleEntryBenchmark.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class JumpTableSingleEntryBenchmark + { + private JumpTable _table; + private string[] _strings; + private PathSegment[] _segments; + + [GlobalSetup] + public void Setup() + { + _table = new SingleEntryJumpTable(0, -1, "hello-world", 1); + _strings = new string[] + { + "index/foo/2", + "index/hello-world1/2", + "index/hello-world/2", + "index//2", + "index/hillo-goodbye/2", + }; + _segments = new PathSegment[] + { + new PathSegment(6, 3), + new PathSegment(6, 12), + new PathSegment(6, 11), + new PathSegment(6, 0), + new PathSegment(6, 13), + }; + } + + [Benchmark(Baseline = true, OperationsPerInvoke = 5)] + public int Baseline() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + var @string = strings[i]; + var segment = segments[i]; + + destination = segment.Length == 0 ? -1 : + segment.Length != 11 ? 1 : + string.Compare( + @string, + segment.Start, + "hello-world", + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase); + } + + return destination; + } + + [Benchmark(OperationsPerInvoke = 5)] + public int Implementation() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _table.GetDestination(strings[i], segments[i]); + } + + return destination; + } + } +} diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableZeroEntryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableZeroEntryBenchmark.cs new file mode 100644 index 0000000000..d14771e36f --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matchers/JumpTableZeroEntryBenchmark.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class JumpTableZeroEntryBenchmark + { + private JumpTable _table; + private string[] _strings; + private PathSegment[] _segments; + + [GlobalSetup] + public void Setup() + { + _table = new ZeroEntryJumpTable(0, -1); + _strings = new string[] + { + "index/foo/2", + "index/hello-world1/2", + "index/hello-world/2", + "index//2", + "index/hillo-goodbye/2", + }; + _segments = new PathSegment[] + { + new PathSegment(6, 3), + new PathSegment(6, 12), + new PathSegment(6, 11), + new PathSegment(6, 0), + new PathSegment(6, 13), + }; + } + + [Benchmark(Baseline=true, OperationsPerInvoke = 5)] + public int Baseline() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = segments[i].Length == 0 ? -1 : 0; + } + + return destination; + } + + [Benchmark(OperationsPerInvoke = 5)] + public int Implementation() + { + var strings = _strings; + var segments = _segments; + + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _table.GetDestination(strings[i], segments[i]); + } + + return destination; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/DictionaryJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matchers/DictionaryJumpTable.cs new file mode 100644 index 0000000000..2fe734ed3f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/DictionaryJumpTable.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class DictionaryJumpTable : JumpTable + { + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly Dictionary _dictionary; + + public DictionaryJumpTable( + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries) + { + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + + _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < entries.Length; i++) + { + _dictionary.Add(entries[i].text, entries[i].destination); + } + } + + public override int GetDestination(string path, PathSegment segment) + { + if (segment.Length == 0) + { + return _exitDestination; + } + + var text = path.Substring(segment.Start, segment.Length); + if (_dictionary.TryGetValue(text, out var destination)) + { + return destination; + } + + return _defaultDestination; + } + + public override string DebuggerToString() + { + var builder = new StringBuilder(); + builder.Append("{ "); + + builder.Append(string.Join(", ", _dictionary.Select(kvp => $"{kvp.Key}: {kvp.Value}"))); + + builder.Append("$+: "); + builder.Append(_defaultDestination); + builder.Append(", "); + + builder.Append("$0: "); + builder.Append(_defaultDestination); + + builder.Append(" }"); + + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/JumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matchers/JumpTable.cs new file mode 100644 index 0000000000..781ae4d0d9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/JumpTable.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + [DebuggerDisplay("{DebuggerToString(),nq}")] + internal abstract class JumpTable + { + public abstract int GetDestination(string path, PathSegment segment); + + public virtual string DebuggerToString() + { + return GetType().Name; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/JumpTableBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matchers/JumpTableBuilder.cs new file mode 100644 index 0000000000..bf2b4ca340 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/JumpTableBuilder.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class JumpTableBuilder + { + public static readonly int InvalidDestination = -1; + + private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>(); + + // The destination state when none of the text entries match. + public int DefaultDestination { get; set; } = InvalidDestination; + + // The destination state for a zero-length segment. This is a special + // case because parameters don't match a zero-length segment. + public int ExitDestination { get; set; } = InvalidDestination; + + public void AddEntry(string text, int destination) + { + _entries.Add((text, destination)); + } + + public JumpTable Build() + { + if (DefaultDestination == InvalidDestination) + { + var message = $"{nameof(DefaultDestination)} is not set. Please report this as a bug."; + throw new InvalidOperationException(message); + } + + if (ExitDestination == InvalidDestination) + { + var message = $"{nameof(ExitDestination)} is not set. Please report this as a bug."; + throw new InvalidOperationException(message); + } + + // The JumpTable implementation is chosen based on the number of entries. Right + // now this is simple and minimal. + if (_entries.Count == 0) + { + return new ZeroEntryJumpTable(DefaultDestination, ExitDestination); + } + + if (_entries.Count == 1) + { + var entry = _entries[0]; + return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination); + } + + if (_entries.Count < 10) + { + return new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray()); + } + + return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray()); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/LinearSearchJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matchers/LinearSearchJumpTable.cs new file mode 100644 index 0000000000..015c168d19 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/LinearSearchJumpTable.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class LinearSearchJumpTable : JumpTable + { + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly (string text, int destination)[] _entries; + + public LinearSearchJumpTable( + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries) + { + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _entries = entries; + } + + public override int GetDestination(string path, PathSegment segment) + { + if (segment.Length == 0) + { + return _exitDestination; + } + + var entries = _entries; + for (var i = 0; i < entries.Length; i++) + { + var text = entries[i].text; + if (segment.Length == text.Length && + string.Compare( + path, + segment.Start, + text, + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase) == 0) + { + return entries[i].destination; + } + } + + return _defaultDestination; + } + + public override string DebuggerToString() + { + var builder = new StringBuilder(); + builder.Append("{ "); + + builder.Append(string.Join(", ", _entries.Select(e => $"{e.text}: {e.destination}"))); + + builder.Append("$+: "); + builder.Append(_defaultDestination); + builder.Append(", "); + + builder.Append("$0: "); + builder.Append(_defaultDestination); + + builder.Append(" }"); + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/SingleEntryJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matchers/SingleEntryJumpTable.cs new file mode 100644 index 0000000000..d49513c509 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/SingleEntryJumpTable.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class SingleEntryJumpTable : JumpTable + { + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly string _text; + private readonly int _destination; + + public SingleEntryJumpTable( + int defaultDestination, + int exitDestination, + string text, + int destination) + { + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _text = text; + _destination = destination; + } + + public override int GetDestination(string path, PathSegment segment) + { + if (segment.Length == 0) + { + return _exitDestination; + } + + if (segment.Length == _text.Length && + string.Compare( + path, + segment.Start, + _text, + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase) == 0) + { + return _destination; + } + + return _defaultDestination; + } + + public override string DebuggerToString() + { + return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}"; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matchers/ZeroEntryJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matchers/ZeroEntryJumpTable.cs new file mode 100644 index 0000000000..a809801a9f --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matchers/ZeroEntryJumpTable.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + internal class ZeroEntryJumpTable : JumpTable + { + private readonly int _defaultDestination; + private readonly int _exitDestination; + + public ZeroEntryJumpTable(int defaultDestination, int exitDestination) + { + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + } + + public unsafe override int GetDestination(string path, PathSegment segment) + { + return segment.Length == 0 ? _exitDestination : _defaultDestination; + } + + public override string DebuggerToString() + { + return $"{{ $+: {_defaultDestination}, $0: {_exitDestination} }}"; + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcher.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcher.cs index ce28cdd643..58808ab857 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcher.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcher.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers for (var i = 0; i < count; i++) { - current = states[current].Transitions.GetDestination(buffer, i, path); + current = states[current].Transitions.GetDestination(path, buffer[i]); } var matches = new List<(Endpoint, RouteValueDictionary)>(); @@ -88,64 +88,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers public Endpoint Endpoint; public string[] Parameters; } - - public abstract class JumpTable - { - public unsafe abstract int GetDestination(PathSegment* segments, int depth, string path); - } - - public class JumpTableBuilder - { - private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>(); - - public int Depth { get; set; } - - public int Exit { get; set; } - - public void AddEntry(string text, int destination) - { - _entries.Add((text, destination)); - } - - public JumpTable Build() - { - return new SimpleJumpTable(Depth, Exit, _entries.ToArray()); - } - } - - private class SimpleJumpTable : JumpTable - { - private readonly (string text, int destination)[] _entries; - private readonly int _depth; - private readonly int _exit; - - public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries) - { - _depth = depth; - _exit = exit; - _entries = entries; - } - - public unsafe override int GetDestination(PathSegment* segments, int depth, string path) - { - for (var i = 0; i < _entries.Length; i++) - { - var segment = segments[depth]; - if (segment.Length == _entries[i].text.Length && - string.Compare( - path, - segment.Start, - _entries[i].text, - 0, - segment.Length, - StringComparison.OrdinalIgnoreCase) == 0) - { - return _entries[i].destination; - } - } - - return _exit; - } - } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilder.cs index 95b0a2b98a..092aa8b96b 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherBuilder.cs @@ -109,13 +109,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers var exit = states.Count; states.Add(new State() { IsAccepting = false, Matches = Array.Empty(), }); - tables.Add(new JumpTableBuilder() { Exit = exit, }); + tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }); for (var i = 0; i < tables.Count; i++) { - if (tables[i].Exit == -1) + if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination) { - tables[i].Exit = exit; + tables[i].DefaultDestination = exit; + } + + if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination) + { + tables[i].ExitDestination = exit; } } @@ -158,7 +163,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers IsAccepting = node.Matches.Count > 0, }); - var table = new JumpTableBuilder() { Depth = node.Depth, }; + var table = new JumpTableBuilder(); tables.Add(table); foreach (var kvp in node.Literals) @@ -172,13 +177,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers table.AddEntry(kvp.Key, transition); } - var exitIndex = -1; + var defaultIndex = -1; if (node.Literals.TryGetValue("*", out var exit)) { - exitIndex = AddNode(exit, states, tables); + defaultIndex = AddNode(exit, states, tables); } - table.Exit = exitIndex; + table.DefaultDestination = defaultIndex; return index; } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DictionaryJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DictionaryJumpTableTest.cs new file mode 100644 index 0000000000..17ac621637 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DictionaryJumpTableTest.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class DictionaryJumpTableTest : MultipleEntryJumpTableTest + { + internal override JumpTable CreateTable( + int defaultDestination, + int exitDestination, + params (string text, int destination)[] entries) + { + return new DictionaryJumpTable(defaultDestination, exitDestination, entries); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/LinearSearchJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/LinearSearchJumpTableTest.cs new file mode 100644 index 0000000000..aa32a9b6e9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/LinearSearchJumpTableTest.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class LinearSearchJumpTableTest : MultipleEntryJumpTableTest + { + internal override JumpTable CreateTable( + int defaultDestination, + int existDestination, + params (string text, int destination)[] entries) + { + return new LinearSearchJumpTable(defaultDestination, existDestination, entries); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MultipleEntryJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MultipleEntryJumpTableTest.cs new file mode 100644 index 0000000000..81351c3c54 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MultipleEntryJumpTableTest.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public abstract class MultipleEntryJumpTableTest + { + internal abstract JumpTable CreateTable( + int defaultDestination, + int exitDestination, + params (string text, int destination)[] entries); + + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void GetDestination_NonMatchingSegment_JumpsToDefault() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("text", new PathSegment(1, 2)); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetDestination_SegmentMatchingText_JumpsToDestination() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("some-text", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_MultipleEntries() + { + // Arrange + var table = CreateTable(0, 1, ("tezt", 2), ("text", 3)); + + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + + // Assert + Assert.Equal(3, result); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/SingleEntryJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/SingleEntryJumpTableTest.cs new file mode 100644 index 0000000000..b9015e0a48 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/SingleEntryJumpTableTest.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class SingleEntryJumpTableTest + { + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = new SingleEntryJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void GetDestination_NonMatchingSegment_JumpsToDefault() + { + // Arrange + var table = new SingleEntryJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("text", new PathSegment(1, 2)); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetDestination_SegmentMatchingText_JumpsToDestination() + { + // Arrange + var table = new SingleEntryJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("some-text", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() + { + // Arrange + var table = new SingleEntryJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/ZeroEntryJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/ZeroEntryJumpTableTest.cs new file mode 100644 index 0000000000..1da7f33f84 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/ZeroEntryJumpTableTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public class ZeroEntryJumpTableTest + { + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = new ZeroEntryJumpTable(0, 1); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void GetDestination_SegmentWithLength_JumpsToDefault() + { + // Arrange + var table = new ZeroEntryJumpTable(0, 1); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 1)); + + // Assert + Assert.Equal(0, result); + } + } +}