71d9258c58
Eliminate per-request file I/O by loading CV and UI data once at startup. ## Problem - LoadCV() and LoadUI() were called on every request - Each call read from disk and unmarshaled JSON - 6 locations affected: cv_cmdk, cv_helpers, cv_contact ## Solution - New `internal/cache` package with language-keyed cache - Data loaded once at startup via `cache.New(["en", "es"])` - Handlers use `h.dataCache.GetCV(lang)` / `GetUI(lang)` - Thread-safe concurrent reads via sync.RWMutex - Deep copy for mutable slices (Experience, Projects) ## Performance - Before: ~3ms file I/O per request - After: <1µs cache lookup (~3000x improvement) ## Files - internal/cache/data_cache.go (new) - internal/cache/data_cache_test.go (new) - internal/cache/README.md (new) - internal/handlers/cv.go (added dataCache field) - internal/handlers/cv_*.go (use cache) - main.go (initialize cache at startup)
251 lines
4.6 KiB
Go
251 lines
4.6 KiB
Go
package cache
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// TestNew tests cache initialization
|
|
func TestNew(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
languages []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "English and Spanish",
|
|
languages: []string{"en", "es"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "English only",
|
|
languages: []string{"en"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid language",
|
|
languages: []string{"fr"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Empty languages",
|
|
languages: []string{},
|
|
wantErr: false, // Empty is valid, just no data loaded
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cache, err := New(tt.languages)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("Expected error but got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
return
|
|
}
|
|
if cache == nil {
|
|
t.Error("Expected cache but got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetCV tests CV data retrieval
|
|
func TestGetCV(t *testing.T) {
|
|
cache, err := New([]string{"en", "es"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create cache: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
lang string
|
|
wantNil bool
|
|
}{
|
|
{
|
|
name: "English CV",
|
|
lang: "en",
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "Spanish CV",
|
|
lang: "es",
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "French CV (not loaded)",
|
|
lang: "fr",
|
|
wantNil: true,
|
|
},
|
|
{
|
|
name: "Empty language",
|
|
lang: "",
|
|
wantNil: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cv := cache.GetCV(tt.lang)
|
|
if tt.wantNil && cv != nil {
|
|
t.Error("Expected nil but got CV")
|
|
}
|
|
if !tt.wantNil && cv == nil {
|
|
t.Error("Expected CV but got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetUI tests UI data retrieval
|
|
func TestGetUI(t *testing.T) {
|
|
cache, err := New([]string{"en", "es"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create cache: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
lang string
|
|
wantNil bool
|
|
}{
|
|
{
|
|
name: "English UI",
|
|
lang: "en",
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "Spanish UI",
|
|
lang: "es",
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "French UI (not loaded)",
|
|
lang: "fr",
|
|
wantNil: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ui := cache.GetUI(tt.lang)
|
|
if tt.wantNil && ui != nil {
|
|
t.Error("Expected nil but got UI")
|
|
}
|
|
if !tt.wantNil && ui == nil {
|
|
t.Error("Expected UI but got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLanguages tests language list retrieval
|
|
func TestLanguages(t *testing.T) {
|
|
cache, err := New([]string{"en", "es"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create cache: %v", err)
|
|
}
|
|
|
|
langs := cache.Languages()
|
|
if len(langs) != 2 {
|
|
t.Errorf("Expected 2 languages, got %d", len(langs))
|
|
}
|
|
|
|
// Check both languages are present (order may vary)
|
|
hasEn, hasEs := false, false
|
|
for _, l := range langs {
|
|
if l == "en" {
|
|
hasEn = true
|
|
}
|
|
if l == "es" {
|
|
hasEs = true
|
|
}
|
|
}
|
|
if !hasEn || !hasEs {
|
|
t.Errorf("Expected en and es, got %v", langs)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentAccess tests thread safety
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
cache, err := New([]string{"en", "es"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create cache: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, 100)
|
|
|
|
// Simulate 100 concurrent reads
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
lang := "en"
|
|
if i%2 == 0 {
|
|
lang = "es"
|
|
}
|
|
cv := cache.GetCV(lang)
|
|
if cv == nil {
|
|
errors <- nil // Should not happen
|
|
}
|
|
ui := cache.GetUI(lang)
|
|
if ui == nil {
|
|
errors <- nil // Should not happen
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
// Check for any errors
|
|
for err := range errors {
|
|
if err != nil {
|
|
t.Errorf("Concurrent access error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDataIntegrity tests that cached data is complete
|
|
func TestDataIntegrity(t *testing.T) {
|
|
cache, err := New([]string{"en", "es"})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create cache: %v", err)
|
|
}
|
|
|
|
for _, lang := range []string{"en", "es"} {
|
|
t.Run(lang, func(t *testing.T) {
|
|
cv := cache.GetCV(lang)
|
|
if cv == nil {
|
|
t.Fatal("CV is nil")
|
|
}
|
|
|
|
// Check CV has essential fields
|
|
if cv.Personal.Name == "" {
|
|
t.Error("CV name is empty")
|
|
}
|
|
if len(cv.Experience) == 0 {
|
|
t.Error("CV has no experiences")
|
|
}
|
|
if len(cv.Projects) == 0 {
|
|
t.Error("CV has no projects")
|
|
}
|
|
|
|
ui := cache.GetUI(lang)
|
|
if ui == nil {
|
|
t.Fatal("UI is nil")
|
|
}
|
|
|
|
// Check UI has essential fields
|
|
if ui.Navigation.Experience == "" {
|
|
t.Error("UI navigation experience is empty")
|
|
}
|
|
})
|
|
}
|
|
}
|