mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-05 22:23:50 +00:00
376 lines
8.4 KiB
Go
376 lines
8.4 KiB
Go
package util
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidatePattern(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pattern string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "valid short pattern",
|
|
pattern: "^hello.*world$",
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "valid empty pattern",
|
|
pattern: "",
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "valid pattern at max length",
|
|
pattern: strings.Repeat("a", MaxPatternLength),
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "pattern too long",
|
|
pattern: strings.Repeat("a", MaxPatternLength+1),
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "very long pattern",
|
|
pattern: strings.Repeat("x", MaxPatternLength*2),
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidatePattern(tt.pattern)
|
|
if tt.expectErr && err == nil {
|
|
t.Error("expected error but got nil")
|
|
}
|
|
if !tt.expectErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompileRegex(t *testing.T) {
|
|
// Clear cache before each test
|
|
ClearRegexCache()
|
|
|
|
t.Run("valid pattern is compiled and cached", func(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
pattern := "^test.*pattern$"
|
|
re1, err := CompileRegex(pattern)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if re1 == nil {
|
|
t.Fatal("expected non-nil regex")
|
|
}
|
|
|
|
// Second call should return cached version
|
|
re2, err := CompileRegex(pattern)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error on second call: %v", err)
|
|
}
|
|
|
|
// Should be the same pointer
|
|
if re1 != re2 {
|
|
t.Error("expected same regex instance from cache")
|
|
}
|
|
|
|
// Cache should have one entry
|
|
if stats := CacheStats(); stats != 1 {
|
|
t.Errorf("expected cache size 1, got %d", stats)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid pattern returns error", func(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
pattern := "[invalid(regex"
|
|
_, err := CompileRegex(pattern)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid regex")
|
|
}
|
|
|
|
var regexErr *RegexError
|
|
if !errors.As(err, ®exErr) {
|
|
t.Errorf("expected RegexError, got %T", err)
|
|
}
|
|
})
|
|
|
|
t.Run("pattern too long returns error", func(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
pattern := strings.Repeat("a", MaxPatternLength+1)
|
|
_, err := CompileRegex(pattern)
|
|
if err == nil {
|
|
t.Fatal("expected error for long pattern")
|
|
}
|
|
|
|
var regexErr *RegexError
|
|
if !errors.As(err, ®exErr) {
|
|
t.Errorf("expected RegexError, got %T", err)
|
|
}
|
|
})
|
|
|
|
t.Run("different patterns are cached separately", func(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
re1, _ := CompileRegex("pattern1")
|
|
re2, _ := CompileRegex("pattern2")
|
|
|
|
if re1 == re2 {
|
|
t.Error("different patterns should produce different regex instances")
|
|
}
|
|
|
|
if stats := CacheStats(); stats != 2 {
|
|
t.Errorf("expected cache size 2, got %d", stats)
|
|
}
|
|
})
|
|
|
|
t.Run("regex matches correctly", func(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
re, err := CompileRegex("^hello\\s+world$")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !re.MatchString("hello world") {
|
|
t.Error("expected match for 'hello world'")
|
|
}
|
|
if !re.MatchString("hello world") {
|
|
t.Error("expected match for 'hello world'")
|
|
}
|
|
if re.MatchString("helloworld") {
|
|
t.Error("unexpected match for 'helloworld'")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompileRegexUncached(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
t.Run("valid pattern compiles without caching", func(t *testing.T) {
|
|
initialSize := CacheStats()
|
|
|
|
re, err := CompileRegexUncached("^uncached.*pattern$")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if re == nil {
|
|
t.Fatal("expected non-nil regex")
|
|
}
|
|
|
|
// Cache size should not change
|
|
if stats := CacheStats(); stats != initialSize {
|
|
t.Errorf("cache size changed from %d to %d", initialSize, stats)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid pattern returns error", func(t *testing.T) {
|
|
_, err := CompileRegexUncached("[invalid")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid regex")
|
|
}
|
|
})
|
|
|
|
t.Run("pattern too long returns error", func(t *testing.T) {
|
|
pattern := strings.Repeat("x", MaxPatternLength+1)
|
|
_, err := CompileRegexUncached(pattern)
|
|
if err == nil {
|
|
t.Fatal("expected error for long pattern")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClearRegexCache(t *testing.T) {
|
|
// Add some patterns
|
|
_, _ = CompileRegex("pattern1")
|
|
_, _ = CompileRegex("pattern2")
|
|
_, _ = CompileRegex("pattern3")
|
|
|
|
if stats := CacheStats(); stats < 3 {
|
|
t.Fatalf("expected at least 3 cached patterns, got %d", stats)
|
|
}
|
|
|
|
ClearRegexCache()
|
|
|
|
if stats := CacheStats(); stats != 0 {
|
|
t.Errorf("expected cache size 0 after clear, got %d", stats)
|
|
}
|
|
}
|
|
|
|
func TestCacheStats(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
if stats := CacheStats(); stats != 0 {
|
|
t.Errorf("expected initial cache size 0, got %d", stats)
|
|
}
|
|
|
|
_, _ = CompileRegex("a")
|
|
if stats := CacheStats(); stats != 1 {
|
|
t.Errorf("expected cache size 1, got %d", stats)
|
|
}
|
|
|
|
_, _ = CompileRegex("b")
|
|
if stats := CacheStats(); stats != 2 {
|
|
t.Errorf("expected cache size 2, got %d", stats)
|
|
}
|
|
|
|
// Same pattern should not increase cache size
|
|
_, _ = CompileRegex("a")
|
|
if stats := CacheStats(); stats != 2 {
|
|
t.Errorf("expected cache size 2 after duplicate, got %d", stats)
|
|
}
|
|
}
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
ClearRegexCache()
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
numPatterns := 10
|
|
|
|
// Generate some patterns
|
|
patterns := make([]string, numPatterns)
|
|
for i := range patterns {
|
|
patterns[i] = strings.Repeat("p", i+1)
|
|
}
|
|
|
|
// Concurrent compilation of same patterns
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
pattern := patterns[id%numPatterns]
|
|
re, err := CompileRegex(pattern)
|
|
if err != nil {
|
|
t.Errorf("goroutine %d: unexpected error: %v", id, err)
|
|
return
|
|
}
|
|
if re == nil {
|
|
t.Errorf("goroutine %d: nil regex returned", id)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Should have exactly numPatterns cached
|
|
if stats := CacheStats(); stats != int64(numPatterns) {
|
|
t.Errorf("expected cache size %d, got %d", numPatterns, stats)
|
|
}
|
|
}
|
|
|
|
func TestRegexError(t *testing.T) {
|
|
t.Run("error message with underlying error", func(t *testing.T) {
|
|
underlying := errors.New("underlying error")
|
|
err := &RegexError{
|
|
Pattern: "test.*",
|
|
Reason: "test reason",
|
|
Err: underlying,
|
|
}
|
|
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "test.*") {
|
|
t.Error("error message should contain pattern")
|
|
}
|
|
if !strings.Contains(msg, "test reason") {
|
|
t.Error("error message should contain reason")
|
|
}
|
|
if !strings.Contains(msg, "underlying error") {
|
|
t.Error("error message should contain underlying error")
|
|
}
|
|
})
|
|
|
|
t.Run("error message without underlying error", func(t *testing.T) {
|
|
err := &RegexError{
|
|
Pattern: "test.*",
|
|
Reason: "test reason",
|
|
Err: nil,
|
|
}
|
|
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "test.*") {
|
|
t.Error("error message should contain pattern")
|
|
}
|
|
if !strings.Contains(msg, "test reason") {
|
|
t.Error("error message should contain reason")
|
|
}
|
|
})
|
|
|
|
t.Run("error unwrap", func(t *testing.T) {
|
|
underlying := errors.New("underlying")
|
|
err := &RegexError{
|
|
Pattern: "test",
|
|
Reason: "reason",
|
|
Err: underlying,
|
|
}
|
|
|
|
if errors.Unwrap(err) != underlying {
|
|
t.Error("Unwrap should return underlying error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTruncatePattern(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "short pattern unchanged",
|
|
input: "short",
|
|
expected: "short",
|
|
},
|
|
{
|
|
name: "exactly 50 chars unchanged",
|
|
input: strings.Repeat("x", 50),
|
|
expected: strings.Repeat("x", 50),
|
|
},
|
|
{
|
|
name: "long pattern truncated",
|
|
input: strings.Repeat("x", 60),
|
|
expected: strings.Repeat("x", 47) + "...",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := truncatePattern(tt.input)
|
|
if got != tt.expected {
|
|
t.Errorf("truncatePattern() = %q (len %d), want %q (len %d)",
|
|
got, len(got), tt.expected, len(tt.expected))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkCompileRegex benchmarks regex compilation with caching
|
|
func BenchmarkCompileRegex(b *testing.B) {
|
|
ClearRegexCache()
|
|
pattern := "^test.*pattern\\d+$"
|
|
|
|
// First call to populate cache
|
|
_, _ = CompileRegex(pattern)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = CompileRegex(pattern)
|
|
}
|
|
}
|
|
|
|
// BenchmarkCompileRegexUncached benchmarks regex compilation without caching
|
|
func BenchmarkCompileRegexUncached(b *testing.B) {
|
|
pattern := "^test.*pattern\\d+$"
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = CompileRegexUncached(pattern)
|
|
}
|
|
}
|