1package text
2
3import (
4 "fmt"
5 "testing"
6
7 "github.com/go-text/typesetting/font"
8 ot "github.com/go-text/typesetting/font/opentype"
9)
10
11func TestMeasureCacheHitsKeysAndEviction(t *testing.T) {
12 e := NewEngine(fixture(t))
13 r := Request{Families: []string{"Noto Sans"}, Size: 16}
14 first, err := e.Measure("Bonjour Ada", r, 0)
15 if err != nil {
16 t.Fatal(err)
17 }
18 allocs := testing.AllocsPerRun(5, func() {
19 if _, err := e.Measure("Bonjour Ada", r, 0); err != nil {
20 panic(err)
21 }
22 })
23 if allocs > 1 {
24 t.Fatalf("cached Measure allocates %.0f times", allocs)
25 }
26 // Any request field, the text or the width must miss.
27 for name, other := range map[string]func() (Layout, error){
28 "size": func() (Layout, error) { return e.Measure("Bonjour Ada", Request{Families: r.Families, Size: 20}, 0) },
29 "family": func() (Layout, error) {
30 return e.Measure("Bonjour Ada", Request{Families: []string{"serif"}, Size: 16}, 0)
31 },
32 "weight": func() (Layout, error) {
33 return e.Measure("Bonjour Ada", Request{Families: r.Families, Size: 16, Weight: 700}, 0)
34 },
35 "width": func() (Layout, error) { return e.Measure("Bonjour Ada", r, 40) },
36 "text": func() (Layout, error) { return e.Measure("Bonjour", r, 0) },
37 } {
38 l, err := other()
39 if err != nil {
40 t.Fatal(name, err)
41 }
42 if l.Width == first.Width && len(l.Lines) == len(first.Lines) && name != "family" && name != "weight" {
43 t.Errorf("%s: expected a different measurement", name)
44 }
45 }
46 if n := len(e.cache.entries); n != 6 {
47 t.Fatalf("entries=%d, want 6", n)
48 }
49 // Entries unused for two frames are evicted; used ones stay.
50 e.EndFrame()
51 if _, err := e.Measure("Bonjour Ada", r, 0); err != nil {
52 t.Fatal(err)
53 }
54 e.EndFrame()
55 e.EndFrame()
56 if n := len(e.cache.entries); n != 1 {
57 t.Fatalf("after eviction entries=%d, want 1", n)
58 }
59 // Evicted entries are recycled with a fresh family list and layout.
60 if n := len(e.cache.free); n != 5 {
61 t.Fatalf("free=%d, want 5", n)
62 }
63 serif := Request{Families: []string{"serif", "sans"}, Size: 16, Variations: []font.Variation{{Tag: ot.MustNewTag("wght"), Value: 500}}}
64 l, err := e.Measure("recycled", serif, 0)
65 if err != nil {
66 t.Fatal(err)
67 }
68 if n := len(e.cache.free); n != 4 {
69 t.Fatalf("put did not take a recycled entry: free=%d, want 4", n)
70 }
71 // The hit verifies the entry's families and variations were replaced, not
72 // kept stale.
73 if hit, ok := e.cache.get("recycled", serif, 0); !ok || hit.Width != l.Width {
74 t.Fatalf("recycled entry missed: %+v %v", hit, ok)
75 }
76}
77
78func TestMeasureCacheBoundAndCloneIsolation(t *testing.T) {
79 e := NewEngine(fixture(t))
80 r := Request{Families: []string{"Noto Sans"}, Size: 16}
81 for i := 0; i < maxMeasureEntries+10; i++ {
82 if _, err := e.Measure(fmt.Sprint(i), r, 0); err != nil {
83 t.Fatal(err)
84 }
85 }
86 if n := len(e.cache.entries); n != maxMeasureEntries {
87 t.Fatalf("entries=%d, want bound %d", n, maxMeasureEntries)
88 }
89 cached, err := e.Measure("0", r, 0)
90 if err != nil || len(cached.Lines) == 0 || len(cached.Lines[0].Runs) == 0 || len(cached.Lines[0].Runs[0].Glyphs) == 0 {
91 t.Fatalf("measure: %+v %v", cached, err)
92 }
93 y := cached.Lines[0].Runs[0].Glyphs[0].Y
94 c := cached.Clone()
95 c.Lines[0].Baseline += 10
96 c.Lines[0].Runs[0].Y += 10
97 c.Lines[0].Runs[0].Glyphs[0].Y += 10
98 again, _ := e.Measure("0", r, 0)
99 if again.Lines[0].Runs[0].Glyphs[0].Y != y || again.Lines[0].Baseline != cached.Lines[0].Baseline {
100 t.Fatal("Clone shares memory with the cache")
101 }
102}