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 }