v0.1.0 47aaa0f

text_test.go RawHistoryFolder

1//go:build linux
2
3package systemtest
4
5import (
6	"context"
7	"encoding/json"
8	"fmt"
9	"image"
10	"math"
11	"os"
12	"path/filepath"
13	"regexp"
14	"sort"
15	"testing"
16	"time"
17
18	"github.com/bnema/nefergui/internal/css"
19	"github.com/bnema/nefergui/internal/harness"
20	"github.com/bnema/nefergui/internal/layout"
21	"github.com/bnema/nefergui/internal/presentation/vkdevice"
22	"github.com/bnema/nefergui/internal/render"
23	"github.com/bnema/nefergui/internal/text"
24)
25
26const sampleText = "office سلام 日本語"
27
28func TestListText(t *testing.T) {
29	requireHarness(t)
30	bin := binary(t)
31	catalog, err := text.Load(text.DirectorySource("../../../testdata/fonts"))
32	if err != nil {
33		t.Fatal(err)
34	}
35	measured, err := text.NewEngine(catalog).Measure(sampleText, text.Request{Families: []string{"Noto Sans"}, Size: 32}, 0)
36	if err != nil {
37		t.Fatal(err)
38	}
39	cmd := layout.Command{Op: "text", Color: css.Color{R: 1, G: 1, B: 1, A: 1}, Opacity: 1}
40	for _, line := range measured.Lines {
41		for _, shaped := range line.Runs {
42			r := layout.Run{Face: shaped.Face, FaceID: shaped.Face.ID, Size: 32}
43			for _, g := range shaped.Glyphs {
44				r.Glyphs = append(r.Glyphs, layout.Glyph{ID: uint32(g.ID), X: 20 + g.X, Y: 40 + g.Y})
45			}
46			cmd.Runs = append(cmd.Runs, r)
47		}
48	}
49	for _, scale := range []int{1, 2} {
50		t.Run(fmt.Sprint(scale), func(t *testing.T) {
51			prep, err := render.NewPreparer(vkdevice.AtlasSize, vkdevice.AtlasPages)
52			if err != nil {
53				t.Fatal(err)
54			}
55			frame, err := prep.Prepare([]layout.Command{cmd}, float64(scale), 800, 600)
56			if err != nil {
57				t.Fatal(err)
58			}
59			if len(frame.Quads) == 0 || len(frame.Uploads) == 0 {
60				t.Fatal("no glyphs or uploads")
61			}
62			root := filepath.Join(t.TempDir(), "artifacts")
63			res, code := harness.Run(context.Background(), harness.Options{Size: "800x600", Scale: "1", Layout: "us", Background: background, Out: root, Timeout: 60 * time.Second, ReadyTimeout: 10 * time.Second, Client: []string{bin, "--static", "--text", sampleText, "--scale", fmt.Sprint(scale), "--frames", "120"}})
64			t.Logf("artifacts=%s code=%d status=%s", root, code, res.Status)
65			if code != harness.Pass {
66				data, _ := os.ReadFile(filepath.Join(root, "logs", "client.stderr"))
67				t.Fatalf("harness: %s: %s", res.Error, data)
68			}
69			files, _ := filepath.Glob(filepath.Join(res.Artifacts["debug"], "readback-*.png"))
70			sort.Strings(files)
71			if len(files) == 0 {
72				t.Fatal("no readback")
73			}
74			img, err := harness.LoadPNG(files[len(files)-1])
75			if err != nil {
76				t.Fatal(err)
77			}
78			var sum summary
79			data, err := os.ReadFile(filepath.Join(res.Artifacts["debug"], "resource-summary.json"))
80			if err != nil {
81				t.Fatal(err)
82			}
83			if err = json.Unmarshal(data, &sum); err != nil {
84				t.Fatal(err)
85			}
86			if sum.Frames != 120 || sum.ImagesAfter != 0 || sum.FDPeak-sum.FDStart > 2 || sum.FDAfter > sum.FDStart {
87				t.Fatalf("resource counts: %+v", sum)
88			}
89			timing := jsonLines[timing](t, filepath.Join(res.Artifacts["debug"], "frame-timings.jsonl"))
90			if len(timing) != 120 {
91				t.Fatalf("timings=%d", len(timing))
92			}
93			for _, row := range timing {
94				if row.Images < 1 || row.Images > 6 || row.FDCount > sum.FDStart+2 {
95					t.Fatalf("unstable frame: %+v", row)
96				}
97			}
98			if os.Getenv("NEFERGUI_VULKAN_VALIDATION") == "1" {
99				data, err := os.ReadFile(res.Artifacts["client_stderr"])
100				if err != nil {
101					t.Fatal(err)
102				}
103				if regexp.MustCompile(`(?i)(NEFERGUI_VULKAN_VALIDATION_(ERROR|WARNING)|validation (error|warning))`).Match(data) {
104					t.Fatalf("validation: %s", data)
105				}
106			}
107			// Reconstruct the CPU coverage directly from the same rasterized masks
108			// that Prepare packed into atlas upload rectangles.
109			pages := make([][]byte, vkdevice.AtlasPages)
110			for i := range pages {
111				pages[i] = make([]byte, vkdevice.AtlasSize*vkdevice.AtlasSize)
112			}
113			for _, u := range frame.Uploads {
114				for y := 0; y < u.Rect.Dy(); y++ {
115					copy(pages[u.Page][(u.Rect.Min.Y+y)*vkdevice.AtlasSize+u.Rect.Min.X:], u.Bytes[y*u.Rect.Dx():(y+1)*u.Rect.Dx()])
116				}
117			}
118			var box image.Rectangle
119			expected := make([]byte, 800*600)
120			for _, q := range frame.Quads {
121				if q.Op != "glyph" {
122					continue
123				}
124				r := image.Rect(int(q.Rect.X), int(q.Rect.Y), int(q.Rect.X+q.Rect.W), int(q.Rect.Y+q.Rect.H))
125				box = box.Union(r)
126				for y := 0; y < r.Dy(); y++ {
127					for x := 0; x < r.Dx(); x++ {
128						expected[(r.Min.Y+y)*800+r.Min.X+x] = pages[q.Glyph.Page][(q.Glyph.Rect.Min.Y+y)*vkdevice.AtlasSize+q.Glyph.Rect.Min.X+x]
129					}
130				}
131			}
132			var ink image.Rectangle
133			cpu, gpu := 0.0, 0.0
134			for y := 0; y < 600; y++ {
135				for x := 0; x < 800; x++ {
136					p := img.At(x, y)
137					r16, g16, b16, a16 := p.RGBA()
138					r, g, b, a := r16>>8, g16>>8, b16>>8, a16>>8
139					if expected[y*800+x] > 0 {
140						cpu += float64(expected[y*800+x]) / 255
141					}
142					if !image.Pt(x, y).In(box) && (r != 17 || g != 17 || b != 17 || a != 255) {
143						t.Fatalf("outside ink box at %d,%d: %v", x, y, p)
144					}
145					if r > 17 {
146						gpu += float64(r-17) / 238
147						ink = ink.Union(image.Rect(x, y, x+1, y+1))
148					}
149				}
150			}
151			if ink.Empty() || math.Abs(float64(ink.Min.X-box.Min.X)) > 2 || math.Abs(float64(ink.Min.Y-box.Min.Y)) > 2 || math.Abs(float64(ink.Max.X-box.Max.X)) > 2 || math.Abs(float64(ink.Max.Y-box.Max.Y)) > 2 || math.Abs(gpu-cpu) > cpu*.1 {
152				t.Fatalf("ink=%v measure/raster box=%v coverage gpu=%.2f cpu=%.2f", ink, box, gpu, cpu)
153			}
154			t.Logf("scale=%d ink=%v measure/raster box=%v gpu coverage=%.2f cpu coverage=%.2f resources=%+v", scale, ink, box, gpu, cpu, sum)
155		})
156	}
157}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close