cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / e2e / stills_test.go
stills_test.go 6.4K
1package e2e23import (4 "bytes"5 "crypto/sha256"6 "encoding/binary"7 "encoding/hex"8 "fmt"9 "hash/crc32"10 "image"11 "image/png"12 "os"13 "path/filepath"14 "strings"15 "testing"16)1718// Stills are pictures of 012 for the docs, drawn from golden screens:19// each docScreen is recorded again from another screen's setup, on a20// terminal the size a page shows well, dark and light, and drawn in its21// family's dark and light schemes (stillthemes_test.go) to22// docs/media/<name>-dark.png and <name>-light.png. The family is the one23// the pages showing it are in (scripts/doccheck checks it). `make24// screens` records and draws them; `make e2e` fails when a still wasn't25// drawn from its golden as it is.26type docScreen struct {27 name, from string // docs/media/<name>-*.png; the screen whose setup it records28 cols, rows uint1629 family string // a row of the families table in docs/contributing/site.md30}3132var docScreens = []docScreen{33 {name: "trace-view", from: "trace-view", cols: 80, rows: 14, family: "Catppuccin"},34 {name: "evaluate", from: "evaluate", cols: 80, rows: 12, family: "Catppuccin"},35 {name: "rules-bars", from: "rules-bars", cols: 80, rows: 12, family: "Rosé Pine"},36 {name: "rules-panel", from: "rules-panel", cols: 100, rows: 12, family: "Rosé Pine"},37 {name: "table", from: "table", cols: 80, rows: 13, family: "Rosé Pine"},38 {name: "layout", from: "layout", cols: 80, rows: 28, family: "Rosé Pine"},39 {name: "palette", from: "palette", cols: 80, rows: 18, family: "Catppuccin"},40 {name: "notebook-cells", from: "notebook-cells", cols: 80, rows: 26, family: "Tokyo Night"},41 {name: "notebook-nu-error", from: "notebook-nu-error", cols: 80, rows: 16, family: "Tokyo Night"},42 {name: "notebook-nu-hover", from: "notebook-nu-hover", cols: 100, rows: 16, family: "Tokyo Night"},43 {name: "notebook-nu-help", from: "notebook-nu-help", cols: 100, rows: 24, family: "Tokyo Night"},44 {name: "notebook-grid", from: "notebook-grid", cols: 80, rows: 24, family: "Tokyo Night"},45 {name: "share-presence", from: "share-presence", cols: 80, rows: 13, family: "Nord"},46 {name: "live-suggestion", from: "live-suggestion", cols: 80, rows: 12, family: "Nord"},47 {name: "live-review", from: "live-review", cols: 100, rows: 12, family: "Nord"},48 {name: "live-ask", from: "live-ask", cols: 80, rows: 10, family: "Nord"},49 {name: "source-tab", from: "source-tab", cols: 80, rows: 20, family: "Gruvbox"},50 {name: "source-formulas", from: "source-formulas", cols: 80, rows: 14, family: "Gruvbox"},51 {name: "source-formulas-why", from: "source-formulas-why", cols: 80, rows: 19, family: "Gruvbox"},52}5354// stillsVersion changes when the drawing does, so every still is55// drawn again.56const stillsVersion = "2"5758// addDocScreens adds the docs' screens, docs-<name> and59// docs-<name>-light, to the golden screens once every screen they're60// recorded from is listed.61func addDocScreens() {62 byName := map[string]screen{}63 for _, sc := range screens {64 byName[sc.name] = sc65 }66 for _, d := range docScreens {67 from, ok := byName[d.from]68 if !ok {69 panic("docScreens: no golden screen " + d.from)70 }71 sc := from72 sc.name, sc.opts.cols, sc.opts.rows = "docs-"+d.name, d.cols, d.rows73 light := sc74 light.name += "-light"75 light.opts.light = true76 screens = append(screens, sc, light)77 }78}7980func TestStills(t *testing.T) {81 faces, err := loadFaces()82 if err != nil {83 t.Fatal(err)84 }85 fams, err := pictureFamilies()86 if err != nil {87 t.Fatal(err)88 }89 schemes, err := loadSchemes()90 if err != nil {91 t.Fatal(err)92 }93 for _, d := range docScreens {94 fam, ok := fams[d.family]95 if !ok {96 t.Errorf("still %s: no family %q in %s", d.name, d.family, familiesDoc)97 continue98 }99 for _, v := range []struct{ suffix, scheme string }{{"dark", fam.dark}, {"light", fam.light}} {100 p, ok := schemes[v.scheme]101 if !ok {102 t.Errorf("family %s: no scheme %q in %s", d.family, v.scheme, schemesFile)103 continue104 }105 screen := "docs-" + d.name106 if v.suffix == "light" {107 screen += "-light"108 }109 t.Run(d.name+"-"+v.suffix, func(t *testing.T) {110 path := filepath.Join("..", "docs", "media", d.name+"-"+v.suffix+".png")111 checkStill(t, screen, int(d.cols), int(d.rows), p, faces, path)112 })113 }114 }115}116117// checkStill draws the still of screen into path under -update, and118// otherwise checks the one there was drawn from the golden as it is.119func checkStill(t *testing.T, screen string, cols, rows int, p stillPalette, fs *stillFaces, path string) {120 src, err := os.ReadFile(filepath.Join("testdata", "screens", screen+".html"))121 if err != nil {122 t.Fatal(err)123 }124 sum := sha256.Sum256([]byte(fmt.Sprintf("%s\n%v\n%s", stillsVersion, p, src)))125 want := hex.EncodeToString(sum[:])126 if !*update {127 if got := pngText(path, "012-golden"); got != want {128 t.Errorf("%s is not drawn from the golden %s as it is; run make screens", path, screen)129 }130 return131 }132 grid, err := parseScreen(string(src), cols, rows)133 if err != nil {134 t.Fatalf("%s: %v", screen, err)135 }136 img, missing := drawStill(grid, p, fs)137 if len(missing) > 0 {138 t.Errorf("%s: no glyph for%s", screen, runeList(missing))139 }140 if err := writePNG(path, img, "012-golden", want); err != nil {141 t.Fatal(err)142 }143}144145// writePNG writes img with a text chunk key=value after its header, so146// the check can tell which golden it was drawn from without drawing it147// again (drawing isn't bit for bit the same on every CPU).148func writePNG(path string, img image.Image, key, value string) error {149 var buf bytes.Buffer150 enc := png.Encoder{CompressionLevel: png.BestCompression}151 if err := enc.Encode(&buf, img); err != nil {152 return err153 }154 b := buf.Bytes()155 const header = 8 + 8 + 13 + 4 // signature, then IHDR's length, type, data and CRC156 data := []byte(key + "\x00" + value)157 chunk := binary.BigEndian.AppendUint32(nil, uint32(len(data)))158 chunk = append(chunk, "tEXt"...)159 chunk = append(chunk, data...)160 chunk = binary.BigEndian.AppendUint32(chunk, crc32.ChecksumIEEE(chunk[4:]))161 out := append(append(append([]byte{}, b[:header]...), chunk...), b[header:]...)162 return os.WriteFile(path, out, 0o644)163}164165// pngText is the value of the PNG's text chunk named key, or "".166func pngText(path, key string) string {167 b, err := os.ReadFile(path)168 if err != nil || len(b) < 8 {169 return ""170 }171 for b = b[8:]; len(b) >= 12; {172 n := int(binary.BigEndian.Uint32(b))173 if len(b) < 12+n {174 return ""175 }176 if string(b[4:8]) == "tEXt" {177 if k, v, ok := strings.Cut(string(b[8:8+n]), "\x00"); ok && k == key {178 return v179 }180 }181 b = b[12+n:]182 }183 return ""184}