oracle_test.go (4730B)
1 //go:build darwin 2 3 package icns 4 5 import ( 6 "bytes" 7 "image" 8 "image/color" 9 "image/png" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "sort" 14 "testing" 15 ) 16 17 // These tests hold the package against iconutil, the tool Apple ships, in 18 // both directions: a file it wrote has to read back as the artwork that went 19 // in, and a file this package wrote has to come apart under its hands. 20 21 // iconutil runs the tool, failing the test with whatever it printed. 22 func iconutil(t *testing.T, args ...string) { 23 t.Helper() 24 cmd := exec.Command("iconutil", args...) 25 out, err := cmd.CombinedOutput() 26 if err != nil { 27 t.Fatalf("iconutil %v: %v\n%s", args, err, out) 28 } 29 } 30 31 func writePNG(t *testing.T, path string, img image.Image) { 32 t.Helper() 33 f, err := os.Create(path) 34 if err != nil { 35 t.Fatal(err) 36 } 37 defer f.Close() 38 if err := png.Encode(f, img); err != nil { 39 t.Fatal(err) 40 } 41 } 42 43 func readPNG(t *testing.T, path string) image.Image { 44 t.Helper() 45 f, err := os.Open(path) 46 if err != nil { 47 t.Fatal(err) 48 } 49 defer f.Close() 50 img, err := png.Decode(f) 51 if err != nil { 52 t.Fatalf("decoding %s: %v", path, err) 53 } 54 return img 55 } 56 57 // oracleArtwork paints one flat colour per slot, so a colour identifies the 58 // file it came from wherever it ends up. 59 func oracleArtwork() (map[Slot]image.Image, map[Slot]color.NRGBA) { 60 var ( 61 images = map[Slot]image.Image{} 62 want = map[Slot]color.NRGBA{} 63 ) 64 for i, slot := range Slots() { 65 c := color.NRGBA{R: uint8(10 + i*20), G: uint8(200 - i*15), B: 0x40, A: 0xFF} 66 images[slot] = solid(int(slot.Pixels()), c) 67 want[slot] = c 68 } 69 return images, want 70 } 71 72 func listDir(t *testing.T, dir string) []string { 73 t.Helper() 74 entries, err := os.ReadDir(dir) 75 if err != nil { 76 t.Fatalf("reading %s: %v", dir, err) 77 } 78 var names []string 79 for _, e := range entries { 80 names = append(names, e.Name()) 81 } 82 sort.Strings(names) 83 return names 84 } 85 86 // TestIconutilWrites checks that a file built by Apple's tool reads back as 87 // the artwork that went into it, element by element. 88 func TestIconutilWrites(t *testing.T) { 89 dir := t.TempDir() 90 set := filepath.Join(dir, "Oracle.iconset") 91 if err := os.MkdirAll(set, 0o755); err != nil { 92 t.Fatal(err) 93 } 94 images, want := oracleArtwork() 95 for slot, img := range images { 96 writePNG(t, filepath.Join(set, "icon_"+slot.String()+".png"), img) 97 } 98 out := filepath.Join(dir, "apple.icns") 99 iconutil(t, "-c", "icns", "-o", out, set) 100 101 data, err := os.ReadFile(out) 102 if err != nil { 103 t.Fatal(err) 104 } 105 d, err := NewDecoder(bytes.NewReader(data)) 106 if err != nil { 107 t.Fatalf("decoding what iconutil wrote: %v", err) 108 } 109 icons := d.Icons() 110 if len(icons) == 0 { 111 t.Fatal("no icons were read back") 112 } 113 // Each slot was painted its own colour, so the colour that comes out 114 // names the file that went in. That reports the mapping for the types 115 // iconutil writes and this package does not, rather than guessing it. 116 source := map[color.NRGBA]Slot{} 117 for slot, c := range want { 118 source[c] = slot 119 } 120 for _, icon := range icons { 121 img, err := icon.Decode() 122 if err != nil { 123 t.Errorf("%s: %v", icon.ID, err) 124 continue 125 } 126 got := centre(img) 127 slot, ok := source[got] 128 if !ok { 129 t.Errorf("%s holds %v, which is no slot's artwork", icon.ID, got) 130 continue 131 } 132 t.Logf("iconutil wrote %-4s %4dpx from slot %s", icon.ID, img.Bounds().Dx(), slot) 133 if px := uint(img.Bounds().Dx()); px != slot.Pixels() { 134 t.Errorf("%s is %d pixels, but slot %s is %d", icon.ID, px, slot, slot.Pixels()) 135 } 136 if ours, ok := slotOf[icon.ID]; ok && ours != slot { 137 t.Errorf("%s carries slot %s, but this package fills it from %s", icon.ID, slot, ours) 138 } 139 } 140 } 141 142 // TestIconutilReads checks that Apple's tool takes a file this package wrote 143 // apart into the artwork that went in, which covers the run-length encoded 144 // elements and the table of contents. 145 func TestIconutilReads(t *testing.T) { 146 dir := t.TempDir() 147 images, want := oracleArtwork() 148 buf := bytes.NewBuffer(nil) 149 if err := NewEncoder(buf).EncodeSlots(images); err != nil { 150 t.Fatal(err) 151 } 152 ours := filepath.Join(dir, "ours.icns") 153 if err := os.WriteFile(ours, buf.Bytes(), 0o644); err != nil { 154 t.Fatal(err) 155 } 156 set := filepath.Join(dir, "ours.iconset") 157 iconutil(t, "-c", "iconset", "-o", set, ours) 158 159 t.Logf("iconutil extracted %v", listDir(t, set)) 160 for slot, c := range want { 161 path := filepath.Join(set, "icon_"+slot.String()+".png") 162 if _, err := os.Stat(path); err != nil { 163 t.Errorf("iconutil did not extract slot %s", slot) 164 continue 165 } 166 img := readPNG(t, path) 167 if got := uint(img.Bounds().Dx()); got != slot.Pixels() { 168 t.Errorf("slot %s came out %d pixels, want %d", slot, got, slot.Pixels()) 169 } 170 if got := centre(img); got != c { 171 t.Errorf("slot %s came out %v, want %v", slot, got, c) 172 } 173 } 174 }