319 lines
6.7 KiB
Go
319 lines
6.7 KiB
Go
package router
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func setupTestRoutes(t testing.TB) string {
|
|
t.Helper()
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
// Create test route files
|
|
routes := map[string]string{
|
|
"index.lua": `return "home"`,
|
|
"about.lua": `return "about"`,
|
|
"api/users.lua": `return "users"`,
|
|
"api/users/get.lua": `return "get_users"`,
|
|
"api/users/post.lua": `return "create_user"`,
|
|
"api/users/[id].lua": `return "user_" .. id`,
|
|
"api/posts/[slug]/comments.lua": `return "comments_" .. slug`,
|
|
"files/*path.lua": `return "file_" .. path`,
|
|
"middleware.lua": `-- root middleware`,
|
|
"api/middleware.lua": `-- api middleware`,
|
|
}
|
|
|
|
for path, content := range routes {
|
|
fullPath := filepath.Join(tempDir, path)
|
|
dir := filepath.Dir(fullPath)
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
return tempDir
|
|
}
|
|
|
|
func TestRouterBasicFunctionality(t *testing.T) {
|
|
routesDir := setupTestRoutes(t)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
tests := []struct {
|
|
method string
|
|
path string
|
|
expected bool
|
|
params map[string]string
|
|
}{
|
|
{"GET", "/", true, nil},
|
|
{"GET", "/about", true, nil},
|
|
{"GET", "/api/users", true, nil},
|
|
{"GET", "/api/users", true, nil},
|
|
{"POST", "/api/users", true, nil},
|
|
{"GET", "/api/users/123", true, map[string]string{"id": "123"}},
|
|
{"GET", "/api/posts/hello-world/comments", true, map[string]string{"slug": "hello-world"}},
|
|
{"GET", "/files/docs/readme.txt", true, map[string]string{"path": "docs/readme.txt"}},
|
|
{"GET", "/nonexistent", false, nil},
|
|
{"DELETE", "/api/users", false, nil},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.method+"_"+tt.path, func(t *testing.T) {
|
|
bytecode, params, found := router.Lookup(tt.method, tt.path)
|
|
|
|
if found != tt.expected {
|
|
t.Errorf("expected found=%v, got %v", tt.expected, found)
|
|
}
|
|
|
|
if tt.expected {
|
|
if bytecode == nil {
|
|
t.Error("expected bytecode, got nil")
|
|
}
|
|
|
|
if tt.params != nil {
|
|
for key, expectedValue := range tt.params {
|
|
if actualValue := params.Get(key); actualValue != expectedValue {
|
|
t.Errorf("param %s: expected %s, got %s", key, expectedValue, actualValue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRouterParamsStruct(t *testing.T) {
|
|
params := &Params{
|
|
Keys: []string{"id", "slug"},
|
|
Values: []string{"123", "hello"},
|
|
}
|
|
|
|
if params.Get("id") != "123" {
|
|
t.Errorf("expected '123', got '%s'", params.Get("id"))
|
|
}
|
|
|
|
if params.Get("slug") != "hello" {
|
|
t.Errorf("expected 'hello', got '%s'", params.Get("slug"))
|
|
}
|
|
|
|
if params.Get("missing") != "" {
|
|
t.Errorf("expected empty string for missing param, got '%s'", params.Get("missing"))
|
|
}
|
|
}
|
|
|
|
func TestRouterMethodNodes(t *testing.T) {
|
|
routesDir := setupTestRoutes(t)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
// Test that different methods work independently
|
|
_, _, foundGet := router.Lookup("GET", "/api/users")
|
|
_, _, foundPost := router.Lookup("POST", "/api/users")
|
|
_, _, foundPut := router.Lookup("PUT", "/api/users")
|
|
|
|
if !foundGet {
|
|
t.Error("GET /api/users should be found")
|
|
}
|
|
if !foundPost {
|
|
t.Error("POST /api/users should be found")
|
|
}
|
|
if foundPut {
|
|
t.Error("PUT /api/users should not be found")
|
|
}
|
|
}
|
|
|
|
func TestRouterWildcardValidation(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Create invalid wildcard route (not at end)
|
|
invalidPath := filepath.Join(tempDir, "bad/*path/more.lua")
|
|
if err := os.MkdirAll(filepath.Dir(invalidPath), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(invalidPath, []byte(`return "bad"`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err := New(tempDir)
|
|
if err == nil {
|
|
t.Error("expected error for wildcard not at end")
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupStatic(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
method := "GET"
|
|
path := "/api/users"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupDynamic(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
method := "GET"
|
|
path := "/api/users/12345"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupWildcard(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
method := "GET"
|
|
path := "/files/docs/deep/nested/file.txt"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupComplex(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
method := "GET"
|
|
path := "/api/posts/my-blog-post-title/comments"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupNotFound(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
method := "GET"
|
|
path := "/this/path/does/not/exist"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupMixed(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
paths := []string{
|
|
"/",
|
|
"/about",
|
|
"/api/users",
|
|
"/api/users/123",
|
|
"/api/posts/hello/comments",
|
|
"/files/document.pdf",
|
|
"/nonexistent",
|
|
}
|
|
method := "GET"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
path := paths[i%len(paths)]
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|
|
|
|
// Comparison benchmarks for string vs byte slice performance
|
|
func BenchmarkLookupStringConversion(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
methodStr := "GET"
|
|
pathStr := "/api/users/12345"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
// Direct string usage
|
|
_, _, _ = router.Lookup(methodStr, pathStr)
|
|
}
|
|
}
|
|
|
|
func BenchmarkLookupPreallocated(b *testing.B) {
|
|
routesDir := setupTestRoutes(b)
|
|
router, err := New(routesDir)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer router.Close()
|
|
|
|
// Pre-allocated strings (optimal case)
|
|
method := "GET"
|
|
path := "/api/users/12345"
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = router.Lookup(method, path)
|
|
}
|
|
}
|