mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 15:47:38 +08:00
Avoid rebuilding skipped-node state during wildcard fallback so the matcher no longer loops on the same static branch and stops allocating on the hot path. Add focused route benchmarks and regression coverage to keep the optimized path stable.
1172 lines
37 KiB
Go
1172 lines
37 KiB
Go
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style license that can be found
|
||
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
|
||
// This tree_test.go is gin's fork, you can see https://github.com/gin-gonic/gin/blob/master/tree_test.go
|
||
|
||
package touka
|
||
|
||
import (
|
||
"fmt"
|
||
"reflect"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// Used as a workaround since we can't compare functions or their addresses
|
||
var fakeHandlerValue string
|
||
|
||
func fakeHandler(val string) HandlersChain {
|
||
return HandlersChain{func(c *Context) {
|
||
fakeHandlerValue = val
|
||
}}
|
||
}
|
||
|
||
type testRequests []struct {
|
||
path string
|
||
nilHandler bool
|
||
route string
|
||
ps Params
|
||
}
|
||
|
||
func getParams() *Params {
|
||
ps := make(Params, 0, 20)
|
||
return &ps
|
||
}
|
||
|
||
func getSkippedNodes() *[]skippedNode {
|
||
ps := make([]skippedNode, 0, 20)
|
||
return &ps
|
||
}
|
||
|
||
func getValueWithTimeout(t *testing.T, tree *node, path string, unescape bool) nodeValue {
|
||
t.Helper()
|
||
|
||
resultCh := make(chan nodeValue, 1)
|
||
go func() {
|
||
resultCh <- tree.getValue(path, getParams(), getSkippedNodes(), unescape)
|
||
}()
|
||
|
||
select {
|
||
case value := <-resultCh:
|
||
return value
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatalf("lookup for path %q timed out, likely stuck in backtracking", path)
|
||
return nodeValue{}
|
||
}
|
||
}
|
||
|
||
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
|
||
unescape := false
|
||
if len(unescapes) >= 1 {
|
||
unescape = unescapes[0]
|
||
}
|
||
|
||
for _, request := range requests {
|
||
value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape)
|
||
|
||
if value.handlers == nil {
|
||
if !request.nilHandler {
|
||
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||
}
|
||
} else if request.nilHandler {
|
||
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||
} else {
|
||
value.handlers[0](nil)
|
||
if fakeHandlerValue != request.route {
|
||
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
||
}
|
||
}
|
||
|
||
if value.params != nil {
|
||
if !reflect.DeepEqual(*value.params, request.ps) {
|
||
t.Errorf("Params mismatch for route '%s'", request.path)
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
func checkPriorities(t *testing.T, n *node) uint32 {
|
||
var prio uint32
|
||
for i := range n.children {
|
||
prio += checkPriorities(t, n.children[i])
|
||
}
|
||
|
||
if n.handlers != nil {
|
||
prio++
|
||
}
|
||
|
||
if n.priority != prio {
|
||
t.Errorf(
|
||
"priority mismatch for node '%s': is %d, should be %d",
|
||
n.path, n.priority, prio,
|
||
)
|
||
}
|
||
|
||
return prio
|
||
}
|
||
|
||
func TestCountParams(t *testing.T) {
|
||
if countParams("/path/:param1/static/*catch-all") != 2 {
|
||
t.Fail()
|
||
}
|
||
if countParams(strings.Repeat("/:param", 256)) != 256 {
|
||
t.Fail()
|
||
}
|
||
}
|
||
|
||
func TestTreeAddAndGet(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/hi",
|
||
"/contact",
|
||
"/co",
|
||
"/c",
|
||
"/a",
|
||
"/ab",
|
||
"/doc/",
|
||
"/doc/go_faq.html",
|
||
"/doc/go1.html",
|
||
"/α",
|
||
"/β",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
checkRequests(t, tree, testRequests{
|
||
{"/a", false, "/a", nil},
|
||
{"/", true, "", nil},
|
||
{"/hi", false, "/hi", nil},
|
||
{"/contact", false, "/contact", nil},
|
||
{"/co", false, "/co", nil},
|
||
{"/con", true, "", nil}, // key mismatch
|
||
{"/cona", true, "", nil}, // key mismatch
|
||
{"/no", true, "", nil}, // no matching child
|
||
{"/ab", false, "/ab", nil},
|
||
{"/α", false, "/α", nil},
|
||
{"/β", false, "/β", nil},
|
||
})
|
||
|
||
checkPriorities(t, tree)
|
||
}
|
||
|
||
func TestTreeWildcard(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/",
|
||
"/cmd/:tool/",
|
||
"/cmd/:tool/:sub",
|
||
"/cmd/whoami",
|
||
"/cmd/whoami/root",
|
||
"/cmd/whoami/root/",
|
||
"/src/*filepath",
|
||
"/search/",
|
||
"/search/:query",
|
||
"/search/gin-gonic",
|
||
"/search/google",
|
||
"/user_:name",
|
||
"/user_:name/about",
|
||
"/files/:dir/*filepath",
|
||
"/doc/",
|
||
"/doc/go_faq.html",
|
||
"/doc/go1.html",
|
||
"/info/:user/public",
|
||
"/info/:user/project/:project",
|
||
"/info/:user/project/:project/*filepath",
|
||
"/info/:user/project/golang",
|
||
"/aa/*xx",
|
||
"/ab/*xx",
|
||
"/:cc",
|
||
"/c1/:dd/e",
|
||
"/c1/:dd/e1",
|
||
"/:cc/cc",
|
||
"/:cc/:dd/ee",
|
||
"/:cc/:dd/:ee/ff",
|
||
"/:cc/:dd/:ee/:ff/gg",
|
||
"/:cc/:dd/:ee/:ff/:gg/hh",
|
||
"/get/test/abc/",
|
||
"/get/:param/abc/",
|
||
"/something/:paramname/thirdthing",
|
||
"/something/secondthing/test",
|
||
"/get/abc",
|
||
"/get/:param",
|
||
"/get/abc/123abc",
|
||
"/get/abc/:param",
|
||
"/get/abc/123abc/xxx8",
|
||
"/get/abc/123abc/:param",
|
||
"/get/abc/123abc/xxx8/1234",
|
||
"/get/abc/123abc/xxx8/:param",
|
||
"/get/abc/123abc/xxx8/1234/ffas",
|
||
"/get/abc/123abc/xxx8/1234/:param",
|
||
"/get/abc/123abc/xxx8/1234/kkdd/12c",
|
||
"/get/abc/123abc/xxx8/1234/kkdd/:param",
|
||
"/get/abc/:param/test",
|
||
"/get/abc/123abd/:param",
|
||
"/get/abc/123abddd/:param",
|
||
"/get/abc/123/:param",
|
||
"/get/abc/123abg/:param",
|
||
"/get/abc/123abf/:param",
|
||
"/get/abc/123abfff/:param",
|
||
"/get/abc/escaped_colon/test\\:param",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
checkRequests(t, tree, testRequests{
|
||
{"/", false, "/", nil},
|
||
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
||
{"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
|
||
{"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
|
||
{"/cmd/whoami", false, "/cmd/whoami", nil},
|
||
{"/cmd/whoami/", true, "/cmd/whoami", nil},
|
||
{"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
|
||
{"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
|
||
{"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
|
||
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
|
||
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||
{"/search/", false, "/search/", nil},
|
||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||
{"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
|
||
{"/search/gin-gonic", false, "/search/gin-gonic", nil},
|
||
{"/search/google", false, "/search/google", nil},
|
||
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
|
||
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
|
||
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
|
||
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
|
||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
|
||
{"/info/gordon/project/go/src/file.go", false, "/info/:user/project/:project/*filepath", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}, Param{Key: "filepath", Value: "/src/file.go"}}},
|
||
{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
|
||
{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
|
||
{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
|
||
// * Error with argument being intercepted
|
||
// new PR handle (/all /all/cc /a/cc)
|
||
// fix PR: https://github.com/gin-gonic/gin/pull/2796
|
||
{"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
|
||
{"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
|
||
{"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
|
||
{"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
|
||
{"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
|
||
{"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
|
||
{"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
|
||
{"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
|
||
{"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
|
||
{"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
|
||
{"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
|
||
{"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
|
||
{"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
|
||
{"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
|
||
{"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
|
||
{"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}},
|
||
{"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}},
|
||
{"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}},
|
||
{"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
|
||
{"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
|
||
{"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
|
||
{"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
|
||
{"/get/test/abc/", false, "/get/test/abc/", nil},
|
||
{"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
|
||
{"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
|
||
{"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
|
||
{"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
|
||
{"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
|
||
{"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
|
||
{"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
|
||
{"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
|
||
{"/something/secondthing/test", false, "/something/secondthing/test", nil},
|
||
{"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
|
||
{"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
|
||
{"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
|
||
{"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
|
||
{"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
|
||
{"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
|
||
{"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
|
||
{"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
|
||
{"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
|
||
{"/get/abc", false, "/get/abc", nil},
|
||
{"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
|
||
{"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
|
||
{"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
|
||
{"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
|
||
{"/get/abc/123abc", false, "/get/abc/123abc", nil},
|
||
{"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
|
||
{"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
|
||
{"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
|
||
{"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
|
||
{"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
|
||
{"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
|
||
{"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
|
||
{"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
|
||
{"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
|
||
{"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
|
||
{"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
|
||
{"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
|
||
{"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
|
||
{"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
|
||
{"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
|
||
{"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
|
||
{"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
|
||
{"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
|
||
{"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
|
||
{"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
|
||
{"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
|
||
{"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
|
||
{"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
|
||
{"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
|
||
{"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
|
||
{"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
|
||
{"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
|
||
{"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
|
||
{"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
|
||
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
|
||
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
|
||
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
|
||
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
|
||
})
|
||
|
||
checkPriorities(t, tree)
|
||
}
|
||
|
||
func TestUnescapeParameters(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/",
|
||
"/cmd/:tool/:sub",
|
||
"/cmd/:tool/",
|
||
"/src/*filepath",
|
||
"/search/:query",
|
||
"/files/:dir/*filepath",
|
||
"/info/:user/project/:project",
|
||
"/info/:user",
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
unescape := true
|
||
checkRequests(t, tree, testRequests{
|
||
{"/", false, "/", nil},
|
||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
|
||
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
|
||
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
|
||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
|
||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
|
||
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
|
||
{"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
|
||
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
|
||
}, unescape)
|
||
|
||
checkPriorities(t, tree)
|
||
}
|
||
|
||
func catchPanic(testFunc func()) (recv any) {
|
||
defer func() {
|
||
recv = recover()
|
||
}()
|
||
|
||
testFunc()
|
||
return
|
||
}
|
||
|
||
type testRoute struct {
|
||
path string
|
||
conflict bool
|
||
}
|
||
|
||
func testRoutes(t *testing.T, routes []testRoute) {
|
||
tree := &node{}
|
||
|
||
for _, route := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route.path, nil)
|
||
})
|
||
|
||
if route.conflict {
|
||
if recv == nil {
|
||
t.Errorf("no panic for conflicting route '%s'", route.path)
|
||
}
|
||
} else if recv != nil {
|
||
t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeWildcardConflict(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/cmd/:tool/:sub", false},
|
||
{"/cmd/vet", false},
|
||
{"/foo/bar", false},
|
||
{"/foo/:name", false},
|
||
{"/foo/:names", true},
|
||
{"/cmd/*path", true},
|
||
{"/cmd/:badvar", true},
|
||
{"/cmd/:tool/names", false},
|
||
{"/cmd/:tool/:badsub/details", true},
|
||
{"/src/*filepath", false},
|
||
{"/src/:file", true},
|
||
{"/src/static.json", true},
|
||
{"/src/*filepathx", true},
|
||
{"/src/", true},
|
||
{"/src/foo/bar", true},
|
||
{"/src1/", false},
|
||
{"/src1/*filepath", true},
|
||
{"/src2*filepath", true},
|
||
{"/src2/*filepath", false},
|
||
{"/search/:query", false},
|
||
{"/search/valid", false},
|
||
{"/user_:name", false},
|
||
{"/user_x", false},
|
||
{"/user_:name", false},
|
||
{"/id:id", false},
|
||
{"/id/:id", false},
|
||
{"/static/*file", false},
|
||
{"/static/", true},
|
||
{"/escape/test\\:d1", false},
|
||
{"/escape/test\\:d2", false},
|
||
{"/escape/test:param", false},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestCatchAllAfterSlash(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/non-leading-*catchall", true},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestTreeChildConflict(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/cmd/vet", false},
|
||
{"/cmd/:tool", false},
|
||
{"/cmd/:tool/:sub", false},
|
||
{"/cmd/:tool/misc", false},
|
||
{"/cmd/:tool/:othersub", true},
|
||
{"/src/AUTHORS", false},
|
||
{"/src/*filepath", true},
|
||
{"/user_x", false},
|
||
{"/user_:name", false},
|
||
{"/id/:id", false},
|
||
{"/id:id", false},
|
||
{"/:id", false},
|
||
{"/*filepath", true},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestTreeDuplicatePath(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/",
|
||
"/doc/",
|
||
"/src/*filepath",
|
||
"/search/:query",
|
||
"/user_:name",
|
||
}
|
||
for _, route := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
})
|
||
if recv != nil {
|
||
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||
}
|
||
|
||
// Add again
|
||
recv = catchPanic(func() {
|
||
tree.addRoute(route, nil)
|
||
})
|
||
if recv == nil {
|
||
t.Fatalf("no panic while inserting duplicate route '%s", route)
|
||
}
|
||
}
|
||
|
||
// printChildren(tree, "")
|
||
|
||
checkRequests(t, tree, testRequests{
|
||
{"/", false, "/", nil},
|
||
{"/doc/", false, "/doc/", nil},
|
||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||
})
|
||
}
|
||
|
||
func TestEmptyWildcardName(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/user:",
|
||
"/user:/",
|
||
"/cmd/:/",
|
||
"/src/*",
|
||
}
|
||
for _, route := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, nil)
|
||
})
|
||
if recv == nil {
|
||
t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeCatchAllConflict(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/src/*filepath/x", true},
|
||
{"/src2/", false},
|
||
{"/src2/*filepath/x", true},
|
||
{"/src3/*filepath", false},
|
||
{"/src3/*filepath/x", true},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestTreeCatchAllConflictRoot(t *testing.T) {
|
||
routes := []testRoute{
|
||
{"/", false},
|
||
{"/*filepath", true},
|
||
}
|
||
testRoutes(t, routes)
|
||
}
|
||
|
||
func TestTreeCatchMaxParams(t *testing.T) {
|
||
tree := &node{}
|
||
route := "/cmd/*filepath"
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
func TestTreeDoubleWildcard(t *testing.T) {
|
||
const panicMsg = "only one wildcard per path segment is allowed"
|
||
|
||
routes := [...]string{
|
||
"/:foo:bar",
|
||
"/:foo:bar/",
|
||
"/:foo*bar",
|
||
}
|
||
|
||
for _, route := range routes {
|
||
tree := &node{}
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, nil)
|
||
})
|
||
|
||
if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
|
||
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
|
||
}
|
||
}
|
||
}
|
||
|
||
/*func TestTreeDuplicateWildcard(t *testing.T) {
|
||
tree := &node{}
|
||
routes := [...]string{
|
||
"/:id/:name/:id",
|
||
}
|
||
for _, route := range routes {
|
||
...
|
||
}
|
||
}*/
|
||
|
||
func TestTreeTrailingSlashRedirect(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
routes := [...]string{
|
||
"/hi",
|
||
"/b/",
|
||
"/search/:query",
|
||
"/cmd/:tool/",
|
||
"/src/*filepath",
|
||
"/x",
|
||
"/x/y",
|
||
"/y/",
|
||
"/y/z",
|
||
"/0/:id",
|
||
"/0/:id/1",
|
||
"/1/:id/",
|
||
"/1/:id/2",
|
||
"/aa",
|
||
"/a/",
|
||
"/admin",
|
||
"/admin/:category",
|
||
"/admin/:category/:page",
|
||
"/doc",
|
||
"/doc/go_faq.html",
|
||
"/doc/go1.html",
|
||
"/no/a",
|
||
"/no/b",
|
||
"/api/:page/:name",
|
||
"/api/hello/:name/bar/",
|
||
"/api/bar/:name",
|
||
"/api/baz/foo",
|
||
"/api/baz/foo/bar",
|
||
"/blog/:p",
|
||
"/posts/:b/:c",
|
||
"/posts/b/:c/d/",
|
||
"/vendor/:x/*y",
|
||
}
|
||
for _, route := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
})
|
||
if recv != nil {
|
||
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||
}
|
||
}
|
||
|
||
tsrRoutes := [...]string{
|
||
"/hi/",
|
||
"/b",
|
||
"/search/gopher/",
|
||
"/cmd/vet",
|
||
"/src",
|
||
"/x/",
|
||
"/y",
|
||
"/0/go/",
|
||
"/1/go",
|
||
"/a",
|
||
"/admin/",
|
||
"/admin/config/",
|
||
"/admin/config/permissions/",
|
||
"/doc/",
|
||
"/admin/static/",
|
||
"/admin/cfg/",
|
||
"/admin/cfg/users/",
|
||
"/api/hello/x/bar",
|
||
"/api/baz/foo/",
|
||
"/api/baz/bax/",
|
||
"/api/bar/huh/",
|
||
"/api/baz/foo/bar/",
|
||
"/api/world/abc/",
|
||
"/blog/pp/",
|
||
"/posts/b/c/d",
|
||
"/vendor/x",
|
||
}
|
||
|
||
for _, route := range tsrRoutes {
|
||
value := tree.getValue(route, nil, getSkippedNodes(), false)
|
||
if value.handlers != nil {
|
||
t.Fatalf("non-nil handler for TSR route '%s", route)
|
||
} else if !value.tsr {
|
||
t.Errorf("expected TSR recommendation for route '%s'", route)
|
||
}
|
||
}
|
||
|
||
noTsrRoutes := [...]string{
|
||
"/",
|
||
"/no",
|
||
"/no/",
|
||
"/_",
|
||
"/_/",
|
||
"/api",
|
||
"/api/",
|
||
"/api/hello/x/foo",
|
||
"/api/baz/foo/bad",
|
||
"/foo/p/p",
|
||
}
|
||
for _, route := range noTsrRoutes {
|
||
value := tree.getValue(route, nil, getSkippedNodes(), false)
|
||
if value.handlers != nil {
|
||
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
||
} else if value.tsr {
|
||
t.Errorf("expected no TSR recommendation for route '%s'", route)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeRootTrailingSlashRedirect(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
recv := catchPanic(func() {
|
||
tree.addRoute("/:test", fakeHandler("/:test"))
|
||
})
|
||
if recv != nil {
|
||
t.Fatalf("panic inserting test route: %v", recv)
|
||
}
|
||
|
||
value := tree.getValue("/", nil, getSkippedNodes(), false)
|
||
if value.handlers != nil {
|
||
t.Fatalf("non-nil handler")
|
||
} else if value.tsr {
|
||
t.Errorf("expected no TSR recommendation")
|
||
}
|
||
}
|
||
|
||
func TestRedirectTrailingSlash(t *testing.T) {
|
||
data := []struct {
|
||
path string
|
||
}{
|
||
{"/hello/:name"},
|
||
{"/hello/:name/123"},
|
||
{"/hello/:name/234"},
|
||
}
|
||
|
||
node := &node{}
|
||
for _, item := range data {
|
||
node.addRoute(item.path, fakeHandler("test"))
|
||
}
|
||
|
||
value := node.getValue("/hello/abx/", nil, getSkippedNodes(), false)
|
||
if value.tsr != true {
|
||
t.Fatalf("want true, is false")
|
||
}
|
||
}
|
||
|
||
func TestTreeFindCaseInsensitivePath(t *testing.T) {
|
||
tree := &node{}
|
||
|
||
longPath := "/l" + strings.Repeat("o", 128) + "ng"
|
||
lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
|
||
|
||
routes := [...]string{
|
||
"/hi",
|
||
"/b/",
|
||
"/ABC/",
|
||
"/search/:query",
|
||
"/cmd/:tool/",
|
||
"/src/*filepath",
|
||
"/x",
|
||
"/x/y",
|
||
"/y/",
|
||
"/y/z",
|
||
"/0/:id",
|
||
"/0/:id/1",
|
||
"/1/:id/",
|
||
"/1/:id/2",
|
||
"/aa",
|
||
"/a/",
|
||
"/doc",
|
||
"/doc/go_faq.html",
|
||
"/doc/go1.html",
|
||
"/doc/go/away",
|
||
"/no/a",
|
||
"/no/b",
|
||
"/Π",
|
||
"/u/apfêl/",
|
||
"/u/äpfêl/",
|
||
"/u/öpfêl",
|
||
"/v/Äpfêl/",
|
||
"/v/Öpfêl",
|
||
"/w/♬", // 3 byte
|
||
"/w/♭/", // 3 byte, last byte differs
|
||
"/w/𠜎", // 4 byte
|
||
"/w/𠜏/", // 4 byte
|
||
longPath,
|
||
}
|
||
|
||
for _, route := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
})
|
||
if recv != nil {
|
||
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||
}
|
||
}
|
||
|
||
// Check out == in for all registered routes
|
||
// With fixTrailingSlash = true
|
||
for _, route := range routes {
|
||
out, found := tree.findCaseInsensitivePath(route, true)
|
||
if !found {
|
||
t.Errorf("Route '%s' not found!", route)
|
||
} else if string(out) != route {
|
||
t.Errorf("Wrong result for route '%s': %s", route, string(out))
|
||
}
|
||
}
|
||
// With fixTrailingSlash = false
|
||
for _, route := range routes {
|
||
out, found := tree.findCaseInsensitivePath(route, false)
|
||
if !found {
|
||
t.Errorf("Route '%s' not found!", route)
|
||
} else if string(out) != route {
|
||
t.Errorf("Wrong result for route '%s': %s", route, string(out))
|
||
}
|
||
}
|
||
|
||
tests := []struct {
|
||
in string
|
||
out string
|
||
found bool
|
||
slash bool
|
||
}{
|
||
{"/HI", "/hi", true, false},
|
||
{"/HI/", "/hi", true, true},
|
||
{"/B", "/b/", true, true},
|
||
{"/B/", "/b/", true, false},
|
||
{"/abc", "/ABC/", true, true},
|
||
{"/abc/", "/ABC/", true, false},
|
||
{"/aBc", "/ABC/", true, true},
|
||
{"/aBc/", "/ABC/", true, false},
|
||
{"/abC", "/ABC/", true, true},
|
||
{"/abC/", "/ABC/", true, false},
|
||
{"/SEARCH/QUERY", "/search/QUERY", true, false},
|
||
{"/SEARCH/QUERY/", "/search/QUERY", true, true},
|
||
{"/CMD/TOOL/", "/cmd/TOOL/", true, false},
|
||
{"/CMD/TOOL", "/cmd/TOOL/", true, true},
|
||
{"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
|
||
{"/x/Y", "/x/y", true, false},
|
||
{"/x/Y/", "/x/y", true, true},
|
||
{"/X/y", "/x/y", true, false},
|
||
{"/X/y/", "/x/y", true, true},
|
||
{"/X/Y", "/x/y", true, false},
|
||
{"/X/Y/", "/x/y", true, true},
|
||
{"/Y/", "/y/", true, false},
|
||
{"/Y", "/y/", true, true},
|
||
{"/Y/z", "/y/z", true, false},
|
||
{"/Y/z/", "/y/z", true, true},
|
||
{"/Y/Z", "/y/z", true, false},
|
||
{"/Y/Z/", "/y/z", true, true},
|
||
{"/y/Z", "/y/z", true, false},
|
||
{"/y/Z/", "/y/z", true, true},
|
||
{"/Aa", "/aa", true, false},
|
||
{"/Aa/", "/aa", true, true},
|
||
{"/AA", "/aa", true, false},
|
||
{"/AA/", "/aa", true, true},
|
||
{"/aA", "/aa", true, false},
|
||
{"/aA/", "/aa", true, true},
|
||
{"/A/", "/a/", true, false},
|
||
{"/A", "/a/", true, true},
|
||
{"/DOC", "/doc", true, false},
|
||
{"/DOC/", "/doc", true, true},
|
||
{"/NO", "", false, true},
|
||
{"/DOC/GO", "", false, true},
|
||
{"/π", "/Π", true, false},
|
||
{"/π/", "/Π", true, true},
|
||
{"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
|
||
{"/u/ÄPFÊL", "/u/äpfêl/", true, true},
|
||
{"/u/ÖPFÊL/", "/u/öpfêl", true, true},
|
||
{"/u/ÖPFÊL", "/u/öpfêl", true, false},
|
||
{"/v/äpfêL/", "/v/Äpfêl/", true, false},
|
||
{"/v/äpfêL", "/v/Äpfêl/", true, true},
|
||
{"/v/öpfêL/", "/v/Öpfêl", true, true},
|
||
{"/v/öpfêL", "/v/Öpfêl", true, false},
|
||
{"/w/♬/", "/w/♬", true, true},
|
||
{"/w/♭", "/w/♭/", true, true},
|
||
{"/w/𠜎/", "/w/𠜎", true, true},
|
||
{"/w/𠜏", "/w/𠜏/", true, true},
|
||
{lOngPath, longPath, true, true},
|
||
}
|
||
// With fixTrailingSlash = true
|
||
for _, test := range tests {
|
||
out, found := tree.findCaseInsensitivePath(test.in, true)
|
||
if found != test.found || (found && (string(out) != test.out)) {
|
||
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
|
||
test.in, string(out), found, test.out, test.found)
|
||
return
|
||
}
|
||
}
|
||
// With fixTrailingSlash = false
|
||
for _, test := range tests {
|
||
out, found := tree.findCaseInsensitivePath(test.in, false)
|
||
if test.slash {
|
||
if found { // test needs a trailingSlash fix. It must not be found!
|
||
t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
|
||
}
|
||
} else {
|
||
if found != test.found || (found && (string(out) != test.out)) {
|
||
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
|
||
test.in, string(out), found, test.out, test.found)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeInvalidNodeType(t *testing.T) {
|
||
const panicMsg = "invalid node type"
|
||
|
||
tree := &node{}
|
||
tree.addRoute("/", fakeHandler("/"))
|
||
tree.addRoute("/:page", fakeHandler("/:page"))
|
||
|
||
// set invalid node type
|
||
tree.children[0].nType = 42
|
||
|
||
// normal lookup
|
||
recv := catchPanic(func() {
|
||
tree.getValue("/test", nil, getSkippedNodes(), false)
|
||
})
|
||
if rs, ok := recv.(string); !ok || rs != panicMsg {
|
||
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
|
||
}
|
||
|
||
// case-insensitive lookup
|
||
recv = catchPanic(func() {
|
||
tree.findCaseInsensitivePath("/test", true)
|
||
})
|
||
if rs, ok := recv.(string); !ok || rs != panicMsg {
|
||
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
|
||
}
|
||
}
|
||
|
||
func TestFindCaseInsensitivePathWithStaticAndParamRoutesDoesNotPanicOnMiss(t *testing.T) {
|
||
tree := &node{}
|
||
routes := [...]string{
|
||
"/:user/:repo/info/refs",
|
||
"/healthz",
|
||
"/api/db/data",
|
||
"/api/db/sum",
|
||
}
|
||
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
t.Fatalf("unexpected panic while looking up missing path: %v", r)
|
||
}
|
||
}()
|
||
|
||
if out, found := tree.findCaseInsensitivePath("/does-not-exist", true); found || out != nil {
|
||
t.Fatalf("expected missing path lookup to return no match, got %q, %t", string(out), found)
|
||
}
|
||
|
||
if out, found := tree.findCaseInsensitivePath("/does-not-exist", false); found || out != nil {
|
||
t.Fatalf("expected missing path lookup without trailing slash fix to return no match, got %q, %t", string(out), found)
|
||
}
|
||
}
|
||
|
||
func TestTreeInvalidParamsType(t *testing.T) {
|
||
tree := &node{}
|
||
// add a child with wildcard
|
||
route := "/:path"
|
||
tree.addRoute(route, fakeHandler(route))
|
||
|
||
// set invalid Params type
|
||
params := make(Params, 0)
|
||
|
||
// try to trigger slice bounds out of range with capacity 0
|
||
tree.getValue("/test", ¶ms, getSkippedNodes(), false)
|
||
}
|
||
|
||
func TestTreeExpandParamsCapacity(t *testing.T) {
|
||
data := []struct {
|
||
path string
|
||
}{
|
||
{"/:path"},
|
||
{"/*path"},
|
||
}
|
||
|
||
for _, item := range data {
|
||
tree := &node{}
|
||
tree.addRoute(item.path, fakeHandler(item.path))
|
||
params := make(Params, 0)
|
||
|
||
value := tree.getValue("/test", ¶ms, getSkippedNodes(), false)
|
||
|
||
if value.params == nil {
|
||
t.Errorf("Expected %s params to be set, but they weren't", item.path)
|
||
continue
|
||
}
|
||
|
||
if len(*value.params) != 1 {
|
||
t.Errorf("Wrong number of %s params: got %d, want %d",
|
||
item.path, len(*value.params), 1)
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeWildcardConflictEx(t *testing.T) {
|
||
conflicts := [...]struct {
|
||
route string
|
||
segPath string
|
||
existPath string
|
||
existSegPath string
|
||
}{
|
||
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
|
||
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
|
||
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
|
||
{"/con:nection", ":nection", `/con:tact`, `:tact`},
|
||
}
|
||
|
||
for _, conflict := range conflicts {
|
||
// I have to re-create a 'tree', because the 'tree' will be
|
||
// in an inconsistent state when the loop recovers from the
|
||
// panic which threw by 'addRoute' function.
|
||
tree := &node{}
|
||
routes := [...]string{
|
||
"/con:tact",
|
||
"/who/are/*you",
|
||
"/who/foo/hello",
|
||
}
|
||
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(conflict.route, fakeHandler(conflict.route))
|
||
})
|
||
|
||
if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
|
||
t.Fatalf("invalid wildcard conflict error (%v)", recv)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestTreeInvalidEscape(t *testing.T) {
|
||
routes := map[string]bool{
|
||
"/r1/r": true,
|
||
"/r2/:r": true,
|
||
"/r3/\\:r": true,
|
||
}
|
||
tree := &node{}
|
||
for route, valid := range routes {
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
})
|
||
if recv == nil != valid {
|
||
t.Fatalf("%s should be %t but got %v", route, valid, recv)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestWildcardInvalidSlash(t *testing.T) {
|
||
const panicMsgPrefix = "no / before catch-all in path"
|
||
|
||
routes := map[string]bool{
|
||
"/foo/bar": true,
|
||
"/foo/x*zy": false,
|
||
"/foo/b*r": false,
|
||
}
|
||
|
||
for route, valid := range routes {
|
||
tree := &node{}
|
||
recv := catchPanic(func() {
|
||
tree.addRoute(route, nil)
|
||
})
|
||
|
||
if recv == nil != valid {
|
||
t.Fatalf("%s should be %t but got %v", route, valid, recv)
|
||
}
|
||
|
||
if rs, ok := recv.(string); recv != nil && (!ok || !strings.HasPrefix(rs, panicMsgPrefix)) {
|
||
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsgPrefix, route, recv)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestComplexBacktrackingWithCatchAll 是一个更复杂的回归测试.
|
||
// 它确保在静态路径匹配失败后, 路由器能够正确地回溯并成功匹配一个
|
||
// 包含多个命名参数、静态部分和捕获所有参数的复杂路由.
|
||
// 这个测试对于验证在禁用 RedirectTrailingSlash 时的算法健壮性至关重要.
|
||
func TestComplexBacktrackingWithCatchAll(t *testing.T) {
|
||
// 1. Arrange: 初始化路由树并设置复杂的路由结构
|
||
tree := &node{}
|
||
routes := [...]string{
|
||
"/abc/b", // 静态诱饵路由
|
||
"/abc/:p1/cde", // 一个不相关的、不会被匹配到的干扰路由
|
||
"/abc/:p1/:p2/def/*filepath", // 最终应该匹配到的复杂目标路由
|
||
}
|
||
for _, route := range routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
// 2. Act: 执行一个会触发深度回溯的请求
|
||
// 这个路径会首先尝试匹配静态的 /abc/b, 但因为后续路径不匹配而失败,
|
||
// 从而强制回溯到 /abc/ 节点, 并重新尝试匹配通配符路径.
|
||
reqPath := "/abc/b/d/def/some/file.txt"
|
||
wantRoute := "/abc/:p1/:p2/def/*filepath"
|
||
wantParams := Params{
|
||
{Key: "p1", Value: "b"},
|
||
{Key: "p2", Value: "d"},
|
||
{Key: "filepath", Value: "/some/file.txt"}, // 注意: catch-all 会包含前导斜杠
|
||
}
|
||
|
||
// 使用 defer/recover 来断言整个过程不会发生 panic
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
t.Fatalf("预期不应发生 panic, 但在处理路径 '%s' 时捕获到了: %v", reqPath, r)
|
||
}
|
||
}()
|
||
|
||
// 执行查找操作
|
||
value := tree.getValue(reqPath, getParams(), getSkippedNodes(), false)
|
||
|
||
// 3. Assert: 验证回溯后的结果是否正确
|
||
// 断言找到了一个有效的句柄
|
||
if value.handlers == nil {
|
||
t.Fatalf("处理路径 '%s' 时句柄不匹配: 期望得到非空的句柄, 但实际为 nil", reqPath)
|
||
}
|
||
|
||
// 断言匹配到了正确的路由
|
||
value.handlers[0](nil)
|
||
if fakeHandlerValue != wantRoute {
|
||
t.Errorf("处理路径 '%s' 时句柄不匹配: \n 得到: %s\n 想要: %s", reqPath, fakeHandlerValue, wantRoute)
|
||
}
|
||
|
||
// 断言URL参数被正确地解析和提取
|
||
if value.params == nil || !reflect.DeepEqual(*value.params, wantParams) {
|
||
t.Errorf("处理路径 '%s' 时参数不匹配: \n 得到: %v\n 想要: %v", reqPath, *value.params, wantParams)
|
||
}
|
||
}
|
||
|
||
func TestBacktrackingFallsThroughToWildcardBranch(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
routes []string
|
||
requestPath string
|
||
wantFullPath string
|
||
wantParams Params
|
||
}{
|
||
{
|
||
name: "param route after static dead end",
|
||
routes: []string{"/foo/bar", "/foo/:id/details"},
|
||
requestPath: "/foo/bar/details",
|
||
wantFullPath: "/foo/:id/details",
|
||
wantParams: Params{{Key: "id", Value: "bar"}},
|
||
},
|
||
{
|
||
name: "catch-all route after static dead end",
|
||
routes: []string{"/foo/bar", "/foo/:id/*rest"},
|
||
requestPath: "/foo/bar/baz.txt",
|
||
wantFullPath: "/foo/:id/*rest",
|
||
wantParams: Params{
|
||
{Key: "id", Value: "bar"},
|
||
{Key: "rest", Value: "/baz.txt"},
|
||
},
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
tree := &node{}
|
||
for _, route := range tt.routes {
|
||
tree.addRoute(route, fakeHandler(route))
|
||
}
|
||
|
||
value := getValueWithTimeout(t, tree, tt.requestPath, false)
|
||
if value.handlers == nil {
|
||
t.Fatalf("expected handlers for %q", tt.requestPath)
|
||
}
|
||
if value.fullPath != tt.wantFullPath {
|
||
t.Fatalf("expected full path %q for %q, got %q", tt.wantFullPath, tt.requestPath, value.fullPath)
|
||
}
|
||
if value.params == nil || !reflect.DeepEqual(*value.params, tt.wantParams) {
|
||
t.Fatalf("expected params %v for %q, got %v", tt.wantParams, tt.requestPath, value.params)
|
||
}
|
||
})
|
||
}
|
||
}
|