Moonshark/router/router_test.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)
}
}