icns

Easily create .icns files (Mac Icons) with this Go library or the included CLI.
Log | Files | Refs | LICENSE

oracle_darwin_test.go (5543B)


      1 //go:build darwin
      2 
      3 package appicon
      4 
      5 import (
      6 	"image"
      7 	"image/color"
      8 	"math"
      9 	"os"
     10 	"os/exec"
     11 	"path/filepath"
     12 	"testing"
     13 )
     14 
     15 // These tests hold the package against actool, the tool that turns a bundle
     16 // into the catalog macOS reads. A manifest this package writes has to be one
     17 // actool takes, and nothing short of running it says whether it is: actool
     18 // crashes on some manifests rather than reporting a problem with them, so a
     19 // bundle that is merely well formed proves nothing.
     20 
     21 // compile runs actool over a bundle and returns the directory it wrote into.
     22 // A missing tool fails the test rather than skipping it, since a skipped
     23 // oracle checks nothing while appearing to pass.
     24 func compile(t *testing.T, bundle string) string {
     25 	t.Helper()
     26 	out := filepath.Join(t.TempDir(), "out")
     27 	if err := os.MkdirAll(out, 0o755); err != nil {
     28 		t.Fatal(err)
     29 	}
     30 	name := filepath.Base(bundle)
     31 	name = name[:len(name)-len(filepath.Ext(name))]
     32 	cmd := exec.Command("xcrun", "actool", bundle,
     33 		"--compile", out,
     34 		"--output-format", "human-readable-text",
     35 		"--errors",
     36 		"--output-partial-info-plist", filepath.Join(out, "partial.plist"),
     37 		"--app-icon", name,
     38 		"--include-all-app-icons",
     39 		"--enable-on-demand-resources", "NO",
     40 		"--development-region", "en",
     41 		"--target-device", "mac",
     42 		"--minimum-deployment-target", "26.0",
     43 		"--platform", "macosx",
     44 	)
     45 	printed, err := cmd.CombinedOutput()
     46 	if err != nil {
     47 		t.Fatalf("actool %s: %v\n%s", name, err, printed)
     48 	}
     49 	return out
     50 }
     51 
     52 // layer draws a disc on transparency, which is the shape of artwork a bundle
     53 // carries: full bleed, for the compiler to mask and light.
     54 func layer(radius float64, c color.NRGBA) image.Image {
     55 	const side = 512
     56 	img := image.NewNRGBA(image.Rect(0, 0, side, side))
     57 	centre := float64(side) / 2
     58 	for y := 0; y < side; y++ {
     59 		for x := 0; x < side; x++ {
     60 			if math.Hypot(float64(x)-centre, float64(y)-centre) > radius {
     61 				continue
     62 			}
     63 			img.SetNRGBA(x, y, c)
     64 		}
     65 	}
     66 	return img
     67 }
     68 
     69 // written writes a bundle under a temporary directory and returns its path.
     70 func written(t *testing.T, name string, b Bundle) string {
     71 	t.Helper()
     72 	dir := filepath.Join(t.TempDir(), name+".icon")
     73 	if err := b.Write(dir); err != nil {
     74 		t.Fatalf("writing the bundle: %v", err)
     75 	}
     76 	return dir
     77 }
     78 
     79 // compiled fails unless actool produced the catalog and the plist naming it.
     80 func compiled(t *testing.T, out string) {
     81 	t.Helper()
     82 	for _, name := range []string{"Assets.car", "partial.plist"} {
     83 		info, err := os.Stat(filepath.Join(out, name))
     84 		if err != nil {
     85 			t.Errorf("actool wrote no %s: %v", name, err)
     86 			continue
     87 		}
     88 		if info.Size() == 0 {
     89 			t.Errorf("actool wrote an empty %s", name)
     90 		}
     91 	}
     92 }
     93 
     94 func TestActoolTakesASingleLayer(t *testing.T) {
     95 	bundle := written(t, "Single", New(layer(210, color.NRGBA{R: 40, G: 70, B: 200, A: 255}), "Single"))
     96 	compiled(t, compile(t, bundle))
     97 }
     98 
     99 // TestActoolTakesTheAppearanceFields covers the fields that vary by
    100 // appearance together, arranged the way a shipping app arranges them: one
    101 // group holding the specialised values and the layer they apply to. Spread
    102 // across two groups the same values crash actool, which is the arrangement
    103 // the package documentation warns about.
    104 func TestActoolTakesTheAppearanceFields(t *testing.T) {
    105 	bundle := written(t, "Composed", Bundle{
    106 		Fills: []Specialized[Fill]{
    107 			{Value: NamedFill("system-light")},
    108 			{Appearance: AppearanceDark, Value: NamedFill("system-dark")},
    109 		},
    110 		Groups: []Group{{
    111 			BlendModes: []Specialized[string]{{Appearance: AppearanceTinted, Value: "normal"}},
    112 			Lighting:   "individual",
    113 			Specular:   true,
    114 			Shadow:     &Shadow{Kind: "layer-color", Opacity: 0.5},
    115 			Translucencies: []Specialized[Translucency]{
    116 				{Value: Translucency{Enabled: true, Value: 0.84}},
    117 				{Appearance: AppearanceTinted, Value: Translucency{Enabled: false, Value: 0.84}},
    118 			},
    119 			Layers: []Layer{{
    120 				Name:     "Front",
    121 				Image:    layer(210, color.NRGBA{R: 40, G: 70, B: 200, A: 255}),
    122 				Glass:    true,
    123 				Position: &Position{Scale: 1.24, Translation: [2]float64{0, 0}},
    124 				Fills: []Specialized[Fill]{
    125 					{Appearance: AppearanceDark, Value: NamedFill("automatic")},
    126 					{Appearance: AppearanceTinted, Value: GradientFill(
    127 						"display-p3:0.90000,0.90000,0.90000,0.83000",
    128 						"srgb:1.00000,1.00000,1.00000,0.41987",
    129 					)},
    130 				},
    131 				BlendModes: []Specialized[string]{{Appearance: AppearanceDark, Value: "lighten"}},
    132 			}},
    133 		}},
    134 	})
    135 	compiled(t, compile(t, bundle))
    136 }
    137 
    138 // TestActoolTakesSeveralGroups covers a stack of groups, which is how an
    139 // icon with a background and a mark on top is built.
    140 func TestActoolTakesSeveralGroups(t *testing.T) {
    141 	bundle := written(t, "Stacked", Bundle{
    142 		Groups: []Group{
    143 			{
    144 				Layers: []Layer{{Name: "Back", Image: layer(210, color.NRGBA{R: 40, G: 70, B: 200, A: 255})}},
    145 				Shadow: &Shadow{Kind: ShadowNeutral, Opacity: 0.5},
    146 			},
    147 			{
    148 				Layers: []Layer{{
    149 					Name:   "Front",
    150 					Image:  layer(130, color.NRGBA{R: 240, G: 240, B: 250, A: 255}),
    151 					Glass:  true,
    152 					Hidden: false,
    153 				}},
    154 			},
    155 		},
    156 	})
    157 	compiled(t, compile(t, bundle))
    158 }
    159 
    160 // TestActoolWritesAnIcnsBeside covers the half of the output that keeps older
    161 // systems working, which the compiler produces from the same bundle.
    162 func TestActoolWritesAnIcnsBeside(t *testing.T) {
    163 	bundle := written(t, "Legacy", New(layer(210, color.NRGBA{R: 200, G: 60, B: 40, A: 255}), "Legacy"))
    164 	out := compile(t, bundle)
    165 	if _, err := os.Stat(filepath.Join(out, "Legacy.icns")); err != nil {
    166 		t.Errorf("actool wrote no icns beside the catalog: %v", err)
    167 	}
    168 }