@@ -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+
3442var (
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.
4276func 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.
210241func 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.
218248func 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.
234263func 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" )
0 commit comments