Skip to content

Commit 01a387d

Browse files
committed
test: add safe catalog override helper and remove sync.Once copying
1 parent 75e9f3f commit 01a387d

2 files changed

Lines changed: 54 additions & 12 deletions

File tree

internal/exercises/exercises.go

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,51 @@ type Catalog struct {
3131
Projects []Exercise
3232
}
3333

34+
// --- Catalog Loader Infrastructure ---
35+
36+
// defaultCatalogLoader loads the catalog from embedded FS.
37+
// Tests override this to inject fake catalogs.
38+
var defaultCatalogLoader = func() (Catalog, error) {
39+
return loadCatalogFromFS(catalogFS)
40+
}
41+
3442
var (
43+
catalogMu sync.Mutex
3544
catalogOnce sync.Once
3645
catalogData Catalog
3746
)
3847

39-
// Get the singleton catalog instance
40-
// Loads from embedded FS on first call
48+
// withTestCatalogLoader temporarily overrides the catalog loader
49+
// and resets internal singleton state for the duration of the test.
50+
func withTestCatalogLoader(loader func() (Catalog, error), fn func()) {
51+
catalogMu.Lock()
52+
53+
oldLoader := defaultCatalogLoader
54+
55+
// override loader + reset singleton
56+
defaultCatalogLoader = loader
57+
catalogOnce = sync.Once{}
58+
catalogData = Catalog{}
59+
60+
catalogMu.Unlock()
61+
62+
fn()
63+
64+
// restore loader, and reset the once/data again
65+
catalogMu.Lock()
66+
defaultCatalogLoader = oldLoader
67+
catalogOnce = sync.Once{}
68+
catalogData = Catalog{}
69+
catalogMu.Unlock()
70+
}
71+
72+
73+
// Get the singleton catalog instance.
74+
// Loads from embedded FS on first call,
4175
// or falls back to default if loading fails.
4276
func catalog() Catalog {
4377
catalogOnce.Do(func() {
44-
cat, err := loadCatalogFromFS(catalogFS)
78+
cat, err := defaultCatalogLoader()
4579
if err != nil || (len(cat.Concepts) == 0 && len(cat.Projects) == 0) {
4680
catalogData = fallbackCatalog()
4781
return
@@ -90,7 +124,7 @@ func loadExercisesDir(fsys fs.FS, dir string) ([]Exercise, error) {
90124
}
91125

92126
name := e.Name()
93-
if filepath.Ext(name) != ".yaml" && filepath.Ext(name) != ".yml" {
127+
if ext := filepath.Ext(name); ext != ".yaml" && ext != ".yml" {
94128
continue
95129
}
96130

@@ -161,7 +195,6 @@ func ListAll() (Catalog, error) {
161195
}
162196

163197
if len(locals) > 0 {
164-
// If local exercises exist, prefer them.
165198
return Catalog{Concepts: locals}, nil
166199
}
167200

@@ -205,16 +238,13 @@ func Reset(ex Exercise) error {
205238
return copyExerciseTemplate(ex.Slug)
206239
}
207240

208-
// Check if template for given slug exists
209-
// in embedded FS.
210241
func templateExists(slug string) bool {
211242
root := filepath.Join("templates", slug)
212243
_, err := fs.Stat(templatesFS, root)
213244
return err == nil
214245
}
215246

216-
// Initialize all exercises from embedded templates
217-
// into local exercises directory.
247+
// Initialize all exercises from embedded templates.
218248
func InitAll() error {
219249
for _, ex := range catalog().Concepts {
220250
if err := copyExerciseTemplate(ex.Slug); err != nil {
@@ -229,8 +259,7 @@ func InitAll() error {
229259
return nil
230260
}
231261

232-
// Copy exercise template from embedded FS to local exercises dir
233-
// for a given slug.
262+
// Copy exercise template from embedded FS to local exercises dir.
234263
func copyExerciseTemplate(slug string) error {
235264
targetDir := filepath.Join("exercises", slug)
236265

@@ -269,4 +298,4 @@ func copyExerciseTemplate(slug string) error {
269298
})
270299
}
271300

272-
var ErrNoTemplates = errors.New("no templates found")
301+
var ErrNoTemplates = errors.New("no templates found")

internal/exercises/exercises_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,16 @@ func TestDiscoverLocal_NoDir(t *testing.T) {
182182
t.Fatalf("should not error if exercises directory is missing")
183183
}
184184
}
185+
186+
func TestCatalogOverride(t *testing.T) {
187+
withTestCatalogLoader(func() (Catalog, error) {
188+
return Catalog{
189+
Concepts: []Exercise{{Slug: "01_mock"}},
190+
}, nil
191+
}, func() {
192+
c := catalog()
193+
if len(c.Concepts) != 1 || c.Concepts[0].Slug != "01_mock" {
194+
t.Fatalf("expected mock catalog")
195+
}
196+
})
197+
}

0 commit comments

Comments
 (0)