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) } }