Improved test coverage

This commit is contained in:
Eduard Urbach 2023-09-06 16:52:22 +02:00
parent bd91077f83
commit 3008940025
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
5 changed files with 80 additions and 71 deletions

View File

@ -46,10 +46,13 @@ data := router.LookupNoAlloc("GET", "/users/42", func(key string, value string)
PASS: TestStatic PASS: TestStatic
PASS: TestParameter PASS: TestParameter
PASS: TestWildcard PASS: TestWildcard
PASS: TestMap
PASS: TestMethods PASS: TestMethods
PASS: TestGitHub PASS: TestGitHub
PASS: TestTrailingSlash
PASS: TestOverwrite PASS: TestOverwrite
coverage: 82.5% of statements PASS: TestInvalidMethod
coverage: 99.1% of statements
``` ```
## Benchmarks ## Benchmarks

View File

@ -1,7 +1,5 @@
package router package router
import "os"
// Router is a high-performance router. // Router is a high-performance router.
type Router[T any] struct { type Router[T any] struct {
get Tree[T] get Tree[T]
@ -46,23 +44,17 @@ func (router *Router[T]) LookupNoAlloc(method string, path string, addParameter
return tree.LookupNoAlloc(path, addParameter) return tree.LookupNoAlloc(path, addParameter)
} }
// Bind traverses all trees and calls the given function on every node. // Map traverses all trees and calls the given function on every node.
func (router *Router[T]) Bind(transform func(T) T) { func (router *Router[T]) Map(transform func(T) T) {
router.get.Bind(transform) router.get.Map(transform)
router.post.Bind(transform) router.post.Map(transform)
router.delete.Bind(transform) router.delete.Map(transform)
router.put.Bind(transform) router.put.Map(transform)
router.patch.Bind(transform) router.patch.Map(transform)
router.head.Bind(transform) router.head.Map(transform)
router.connect.Bind(transform) router.connect.Map(transform)
router.trace.Bind(transform) router.trace.Map(transform)
router.options.Bind(transform) router.options.Map(transform)
}
// Print shows a pretty print of all the routes.
func (router *Router[T]) Print(method string) {
tree := router.selectTree(method)
tree.root.PrettyPrint(os.Stdout)
} }
// selectTree returns the tree by the given HTTP method. // selectTree returns the tree by the given HTTP method.

View File

@ -1,6 +1,7 @@
package router_test package router_test
import ( import (
"strings"
"testing" "testing"
"git.akyoto.dev/go/assert" "git.akyoto.dev/go/assert"
@ -60,6 +61,7 @@ func TestWildcard(t *testing.T) {
r := router.New[string]() r := router.New[string]()
r.Add("GET", "/", "Front page") r.Add("GET", "/", "Front page")
r.Add("GET", "/:post", "Blog post") r.Add("GET", "/:post", "Blog post")
r.Add("GET", "/*any", "Wildcard")
r.Add("GET", "/users/:id", "Parameter") r.Add("GET", "/users/:id", "Parameter")
r.Add("GET", "/images/static", "Static") r.Add("GET", "/images/static", "Static")
r.Add("GET", "/images/*path", "Wildcard") r.Add("GET", "/images/*path", "Wildcard")
@ -80,6 +82,9 @@ func TestWildcard(t *testing.T) {
assert.Equal(t, params[0].Value, "42") assert.Equal(t, params[0].Value, "42")
assert.Equal(t, data, "Parameter") assert.Equal(t, data, "Parameter")
data, _ = r.Lookup("GET", "/users/42/test.txt")
assert.Equal(t, data, "Wildcard")
data, params = r.Lookup("GET", "/images/static") data, params = r.Lookup("GET", "/images/static")
assert.Equal(t, len(params), 0) assert.Equal(t, len(params), 0)
assert.Equal(t, data, "Static") assert.Equal(t, data, "Static")
@ -103,6 +108,34 @@ func TestWildcard(t *testing.T) {
assert.Equal(t, data, "Wildcard") assert.Equal(t, data, "Wildcard")
} }
func TestMap(t *testing.T) {
r := router.New[string]()
r.Add("GET", "/hello", "Hello")
r.Add("GET", "/world", "World")
r.Add("GET", "/user/:user", "User")
r.Add("GET", "/*path", "Path")
r.Map(func(data string) string {
return strings.Repeat(data, 2)
})
data, params := r.Lookup("GET", "/hello")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "HelloHello")
data, params = r.Lookup("GET", "/world")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "WorldWorld")
data, params = r.Lookup("GET", "/user/123")
assert.Equal(t, len(params), 1)
assert.Equal(t, data, "UserUser")
data, params = r.Lookup("GET", "/test.txt")
assert.Equal(t, len(params), 1)
assert.Equal(t, data, "PathPath")
}
func TestMethods(t *testing.T) { func TestMethods(t *testing.T) {
methods := []string{ methods := []string{
"GET", "GET",
@ -145,6 +178,20 @@ func TestGitHub(t *testing.T) {
} }
} }
func TestTrailingSlash(t *testing.T) {
r := router.New[string]()
r.Add("GET", "/hello", "Hello 1")
r.Add("GET", "/hello/", "Hello 2")
data, params := r.Lookup("GET", "/hello")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "Hello 1")
data, params = r.Lookup("GET", "/hello/")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "Hello 2")
}
func TestOverwrite(t *testing.T) { func TestOverwrite(t *testing.T) {
r := router.New[string]() r := router.New[string]()
r.Add("GET", "/", "1") r.Add("GET", "/", "1")
@ -158,6 +205,17 @@ func TestOverwrite(t *testing.T) {
assert.Equal(t, data, "5") assert.Equal(t, data, "5")
} }
func TestInvalidMethod(t *testing.T) {
defer func() {
if recover() == nil {
t.FailNow()
}
}()
r := router.New[string]()
r.Add("?", "/hello", "Hello")
}
func TestMemoryUsage(t *testing.T) { func TestMemoryUsage(t *testing.T) {
escape := func(a any) {} escape := func(a any) {}

12
Tree.go
View File

@ -130,6 +130,8 @@ begin:
return node.data return node.data
} }
// node: /|*any
// path: /|image.png
if lastWildcard != nil { if lastWildcard != nil {
addParameter(lastWildcard.prefix, path[lastWildcardOffset:]) addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
return lastWildcard.data return lastWildcard.data
@ -193,9 +195,9 @@ begin:
// node: /|*any // node: /|*any
// path: /|image.png // path: /|image.png
if node.wildcard != nil { if lastWildcard != nil {
addParameter(node.wildcard.prefix, path[i:]) addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
return node.wildcard.data return lastWildcard.data
} }
return empty return empty
@ -217,8 +219,8 @@ begin:
} }
} }
// Bind binds all handlers to a new one provided by the callback. // Map binds all handlers to a new one provided by the callback.
func (tree *Tree[T]) Bind(transform func(T) T) { func (tree *Tree[T]) Map(transform func(T) T) {
tree.root.each(func(node *treeNode[T]) { tree.root.each(func(node *treeNode[T]) {
node.data = transform(node.data) node.data = transform(node.data)
}) })

View File

@ -1,8 +1,6 @@
package router package router
import ( import (
"fmt"
"io"
"strings" "strings"
) )
@ -283,47 +281,3 @@ func (node *treeNode[T]) each(callback func(*treeNode[T])) {
node.wildcard.each(callback) node.wildcard.each(callback)
} }
} }
// PrettyPrint prints a human-readable form of the tree to the given writer.
func (node *treeNode[T]) PrettyPrint(writer io.Writer) {
node.prettyPrint(writer, -1)
}
// prettyPrint is the underlying pretty printer.
func (node *treeNode[T]) prettyPrint(writer io.Writer, level int) {
prefix := ""
if level >= 0 {
prefix = strings.Repeat(" ", level) + "|_ "
}
switch node.kind {
case ':':
prefix += ":"
case '*':
prefix += "*"
}
fmt.Fprintf(writer, "%s%s [%v]\n", prefix, node.prefix, node.data)
for _, child := range node.children {
if child == nil {
continue
}
child.prettyPrint(writer, level+1)
}
if node.parameter != nil {
node.parameter.prettyPrint(writer, level+1)
}
if node.wildcard != nil {
node.wildcard.prettyPrint(writer, level+1)
}
}
// String returns the node prefix.
func (node *treeNode[T]) String() string {
return node.prefix
}