package main import ( "bytes" "encoding/binary" "image/png" "runtime" "testing" ) // The tray icon shipped as a PNG for a while. systray hands the bytes to // LoadImageW, which decodes .ico only, so Windows logged one line and drew // nothing — and nothing on a Mac could notice. These tests are the substitute // for the Windows box we do not have. func TestEncodeICOIsAValidIconFile(t *testing.T) { b := encodeICO(circle(colorFor("ok"))) if len(b) < 22 { t.Fatalf("far too short: %d bytes", len(b)) } if got := b[:6]; !bytes.Equal(got, []byte{0, 0, 1, 0, 1, 0}) { t.Errorf("ICONDIR = % x, want 00 00 01 00 01 00", got) } if b[6] != iconSize || b[7] != iconSize { t.Errorf("entry is %dx%d, want %dx%d", b[6], b[7], iconSize, iconSize) } if bpp := binary.LittleEndian.Uint16(b[12:14]); bpp != 32 { t.Errorf("bits per pixel = %d, want 32", bpp) } // A wrong length here loads as a truncated or garbage icon rather than // failing outright, which is the harder version of this bug to spot. size := binary.LittleEndian.Uint32(b[14:18]) off := binary.LittleEndian.Uint32(b[18:22]) if off != 22 { t.Errorf("image offset = %d, want 22", off) } if int(off)+int(size) != len(b) { t.Errorf("entry claims %d bytes at %d, file is %d", size, off, len(b)) } if h := binary.LittleEndian.Uint32(b[22:26]); h != 40 { t.Errorf("BITMAPINFOHEADER size = %d, want 40", h) } // Height must be doubled for the implied AND mask or Windows draws the // bottom half of the icon stretched over the whole square. if hh := int32(binary.LittleEndian.Uint32(b[30:34])); hh != int32(iconSize*2) { t.Errorf("biHeight = %d, want %d", hh, iconSize*2) } } func TestEncodeICOPixelsAreBGRABottomUp(t *testing.T) { want := colorFor("error") // red: distinguishable from B and G if swapped img := circle(want) b := encodeICO(img) // Centre of the circle, which is solid fill. Bottom-up means image row // iconSize/2 lands at DIB row iconSize/2-1 counting from the start. row := iconSize - 1 - iconSize/2 i := 22 + 40 + (row*iconSize+iconSize/2)*4 got := b[i : i+4] if !bytes.Equal(got, []byte{want.B, want.G, want.R, 0xFF}) { t.Errorf("centre pixel = % x, want % x (BGRA)", got, []byte{want.B, want.G, want.R, 0xFF}) } } func TestIconForMatchesThePlatformDecoder(t *testing.T) { b := iconFor("ok") pngMagic := []byte{0x89, 'P', 'N', 'G'} if runtime.GOOS == "windows" { if bytes.HasPrefix(b, pngMagic) { t.Fatal("Windows tray icon is a PNG; LoadImageW will refuse it") } if !bytes.HasPrefix(b, []byte{0, 0, 1, 0}) { t.Fatalf("Windows tray icon is not an ICO: % x", b[:4]) } return } if _, err := png.Decode(bytes.NewReader(b)); err != nil { t.Fatalf("non-Windows tray icon is not a decodable PNG: %v", err) } } // The four states exist to be told apart at a glance; identical bytes would // mean the icon never changes and the amber "running but blind" state — the // one that otherwise goes unnoticed for weeks — looks exactly like healthy. func TestEveryStateLooksDifferent(t *testing.T) { seen := map[string]string{} for _, s := range []string{"ok", "warn", "error", "stopped"} { k := string(iconFor(s)) if prev, dup := seen[k]; dup { t.Errorf("%q and %q render identically", prev, s) } seen[k] = s } }