|
1 | 1 | package octicons |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "io/fs" |
4 | 5 | "strings" |
| 6 | + "sync" |
5 | 7 | "testing" |
6 | 8 |
|
7 | 9 | "github.com/modelcontextprotocol/go-sdk/mcp" |
@@ -53,6 +55,65 @@ func TestDataURI(t *testing.T) { |
53 | 55 | } |
54 | 56 | } |
55 | 57 |
|
| 58 | +func TestDataURIForEveryEmbeddedIcon(t *testing.T) { |
| 59 | + paths, err := fs.Glob(iconsFS, "icons/*.png") |
| 60 | + assert.NoError(t, err) |
| 61 | + assert.NotEmpty(t, paths) |
| 62 | + |
| 63 | + for _, path := range paths { |
| 64 | + filename := strings.TrimSuffix(strings.TrimPrefix(path, "icons/"), ".png") |
| 65 | + separator := strings.LastIndexByte(filename, '-') |
| 66 | + if separator <= 0 { |
| 67 | + t.Errorf("cannot parse embedded icon path %q", path) |
| 68 | + continue |
| 69 | + } |
| 70 | + name := filename[:separator] |
| 71 | + theme := Theme(filename[separator+1:]) |
| 72 | + t.Run(filename, func(t *testing.T) { |
| 73 | + assert.True(t, strings.HasPrefix(DataURI(name, theme), "data:image/png;base64,")) |
| 74 | + }) |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +func TestDataURICacheOnlyStoresSuccessfulReads(t *testing.T) { |
| 79 | + var cache dataURICache |
| 80 | + missingKey := dataURIKey{name: "nonexistent-icon", theme: ThemeLight} |
| 81 | + |
| 82 | + assert.Empty(t, cache.load(missingKey.name, missingKey.theme)) |
| 83 | + assert.Nil(t, cache.values, "missing icons must not initialize the cache") |
| 84 | + _, found := cache.values[missingKey] |
| 85 | + assert.False(t, found, "missing icons must not be cached") |
| 86 | + |
| 87 | + validKey := dataURIKey{name: "repo", theme: ThemeLight} |
| 88 | + assert.NotEmpty(t, cache.load(validKey.name, validKey.theme)) |
| 89 | + _, found = cache.values[validKey] |
| 90 | + assert.True(t, found, "successful reads should be cached") |
| 91 | +} |
| 92 | + |
| 93 | +func TestDataURICacheConcurrentFirstUse(t *testing.T) { |
| 94 | + var cache dataURICache |
| 95 | + want := readDataURI("repo", ThemeLight) |
| 96 | + assert.NotEmpty(t, want) |
| 97 | + |
| 98 | + const workers = 64 |
| 99 | + start := make(chan struct{}) |
| 100 | + results := make(chan string, workers) |
| 101 | + var wg sync.WaitGroup |
| 102 | + for range workers { |
| 103 | + wg.Go(func() { |
| 104 | + <-start |
| 105 | + results <- cache.load("repo", ThemeLight) |
| 106 | + }) |
| 107 | + } |
| 108 | + |
| 109 | + close(start) |
| 110 | + wg.Wait() |
| 111 | + close(results) |
| 112 | + for result := range results { |
| 113 | + assert.Equal(t, want, result) |
| 114 | + } |
| 115 | +} |
| 116 | + |
56 | 117 | func TestIcons(t *testing.T) { |
57 | 118 | tests := []struct { |
58 | 119 | name string |
@@ -99,6 +160,23 @@ func TestIcons(t *testing.T) { |
99 | 160 | } |
100 | 161 | } |
101 | 162 |
|
| 163 | +func TestIconsReturnsFreshSlice(t *testing.T) { |
| 164 | + lightSource := DataURI("repo", ThemeLight) |
| 165 | + darkSource := DataURI("repo", ThemeDark) |
| 166 | + |
| 167 | + icons := Icons("repo") |
| 168 | + icons[0] = mcp.Icon{} |
| 169 | + icons[1].Source = "mutated" |
| 170 | + |
| 171 | + fresh := Icons("repo") |
| 172 | + assert.Equal(t, lightSource, fresh[0].Source) |
| 173 | + assert.Equal(t, darkSource, fresh[1].Source) |
| 174 | + assert.Equal(t, "image/png", fresh[0].MIMEType) |
| 175 | + assert.Equal(t, "image/png", fresh[1].MIMEType) |
| 176 | + assert.Equal(t, mcp.IconThemeLight, fresh[0].Theme) |
| 177 | + assert.Equal(t, mcp.IconThemeDark, fresh[1].Theme) |
| 178 | +} |
| 179 | + |
102 | 180 | func TestThemeConstants(t *testing.T) { |
103 | 181 | assert.Equal(t, Theme("light"), ThemeLight) |
104 | 182 | assert.Equal(t, Theme("dark"), ThemeDark) |
|
0 commit comments