okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
0f20d9f
upstream
1package conformance_test 2 3import ( 4"strings" 5"sync" 6"testing" 7 8"github.com/charmbracelet/x/vt" 9"go.mitchellh.com/libghostty" 10) 11 12// Two reference emulators, because they disagree about how wide a grapheme 13// cluster is and that disagreement is the whole subject of these tests. 14// 15// libghostty is ghostty's real VT engine. It models legacy widths per 16// codepoint, which is what stock terminals actually do, and it implements DEC 17// mode 2027 so both width models can be exercised. x/vt always measures whole 18// clusters, so it reproduces a mode-2027 terminal only. 19// 20// Neither is redundant. A renderer bug that only shows up under legacy widths 21// is invisible to x/vt, and keeping both means a disagreement between them is 22// itself a signal worth investigating. 23 24// oracle is a terminal emulator the renderer can write to and whose screen can 25// be read back column by column. 26// 27// Reading by column is the point. A formatter that returns the screen as a 28// string collapses the spacer cells that follow a wide glyph, which is exactly 29// the information these tests need: it cannot distinguish a cluster at column 0 30// with a marker at column 2 from the same cluster with a marker at column 1. 31type oracle interface { 32// Write feeds renderer output into the emulator. 33Write (p []byte ) (n int ,err error ) 34 35// Row returns the visible text of a row, trailing blanks removed. 36Row (t * testing.T ,y int )string 37 38// CellAt returns the content of one cell. A cell that holds nothing, or 39// that continues a wide glyph starting to its left, returns "". 40CellAt (t * testing.T ,x ,y int )string 41 42// Size reports the emulator's dimensions. 43Size () (w ,h int ) 44 45// Resize changes the emulator's dimensions, mirroring a SIGWINCH. 46Resize (w ,h int ) 47 48// Close releases emulator resources. 49Close () 50 51// Name identifies the emulator in failure messages. 52Name ()string 53} 54 55// ghosttyOracle wraps ghostty's VT engine. 56type ghosttyOracle struct { 57term * libghostty.Terminal 58w ,h int 59} 60 61// newGhostty creates a ghostty emulator. When grapheme is false it measures 62// clusters with legacy per-codepoint widths, the way an ordinary terminal that 63// has not negotiated mode 2027 does. 64func newGhostty (t * testing.T ,w ,h int ,grapheme bool )oracle { 65t .Helper () 66 67term ,err := libghostty .NewTerminal (libghostty .WithSize (uint16 (w ),uint16 (h ))) 68if err != nil { 69t .Fatalf ("libghostty.NewTerminal(%d, %d): %v" ,w ,h ,err ) 70 } 71if err := term .ModeSet (libghostty .ModeGraphemeCluster ,grapheme );err != nil { 72term .Close () 73t .Fatalf ("ModeSet(ModeGraphemeCluster, %v): %v" ,grapheme ,err ) 74 } 75return & ghosttyOracle {term :term ,w :w ,h :h } 76} 77 78func (g * ghosttyOracle )Write (p []byte ) (int ,error ) {return g .term .Write (p ) } 79func (g * ghosttyOracle )Size () (int ,int ) {return g .w ,g .h } 80func (g * ghosttyOracle )Close () {g .term .Close () } 81func (g * ghosttyOracle )Name ()string {return "ghostty" } 82 83func (g * ghosttyOracle )Resize (w ,h int ) { 84g .w ,g .h = w ,h 85// Cell pixel dimensions are irrelevant to these tests, so pass zero. 86_ = g .term .Resize (uint16 (w ),uint16 (h ),0 ,0 ) 87} 88 89func (g * ghosttyOracle )CellAt (t * testing.T ,x ,y int )string { 90t .Helper () 91 92ref ,err := g .term .GridRef (libghostty.Point { 93Tag :libghostty .PointTagActive , 94X :uint16 (x ), 95Y :uint32 (y ), 96 }) 97if err != nil { 98t .Fatalf ("GridRef(%d, %d): %v" ,x ,y ,err ) 99 } 100cell ,err := ref .Cell () 101if err != nil { 102t .Fatalf ("Cell(%d, %d): %v" ,x ,y ,err ) 103 } 104 105// Graphemes returns the whole cluster including its base codepoint, so 106// reading Codepoint as well would duplicate the base. Fall back to 107// Codepoint only for cells that hold a single rune. 108var b strings.Builder 109if cps ,err := ref .Graphemes ();err == nil && len (cps )> 0 { 110for _ ,cp := range cps { 111b .WriteRune (rune (cp )) 112 } 113return b .String () 114 } 115if cp ,err := cell .Codepoint ();err == nil && cp != 0 { 116b .WriteRune (rune (cp )) 117 } 118return b .String () 119} 120 121func (g * ghosttyOracle )Row (t * testing.T ,y int )string { 122t .Helper () 123 124var b strings.Builder 125for x := range g .w { 126cell := g .CellAt (t ,x ,y ) 127if cell == "" { 128// A cell that was erased reports no codepoint, while a cell 129// someone wrote a space into reports one. They look the same on 130// screen and mean the same thing, so read them back the same way. 131// Otherwise a row painted by erasing compares unequal to the same 132// row painted with spaces. 133cell = " " 134 } 135b .WriteString (cell ) 136 } 137 138// Empty cells read back as NUL rather than a space, so trim both. 139return strings .TrimRight (b .String ()," \x00" ) 140} 141 142// clusterWidth reports how many columns an emulator advances when it prints one 143// grapheme cluster. 144// 145// Measured rather than modelled. How wide a cluster is happens to be the exact 146// thing these tests are about, so a width table here would be a second opinion 147// standing in for the emulator's, and wrong in the cases that matter: ghostty's 148// legacy mode paints a regional indicator pair in four columns where wcwidth 149// says two. 150// 151// The measurement writes the cluster followed by a marker and reports the 152// column the marker landed in, which is the advance width by definition. The 153// answers are cached because a fuzz target asks for the same handful of 154// clusters millions of times. 155var clusterWidths sync.Map 156 157func clusterWidth (t * testing.T ,o oracleSpec ,grapheme bool ,cluster string )int { 158t .Helper () 159 160type key struct { 161name string 162grapheme bool 163cluster string 164 } 165k := key {o .name ,grapheme ,cluster } 166if w ,ok := clusterWidths .Load (k );ok { 167return w .(int ) 168 } 169 170// Wide enough for any cluster in the corpus, with room for the marker. 171const scratch = 64 172const marker = "|" 173 174term := o .mk (t ,scratch ,1 ,grapheme ) 175defer term .Close () 176if _ ,err := term .Write ([]byte (cluster + marker ));err != nil { 177t .Fatalf ("measuring %q: %v" ,cluster ,err ) 178 } 179 180w := 0 181for x := range scratch { 182if term .CellAt (t ,x ,0 )== marker { 183w = x 184break 185 } 186 } 187clusterWidths .Store (k ,w ) 188return w 189} 190 191// vtOracle wraps x/vt, which always measures whole grapheme clusters and so 192// behaves like a terminal with mode 2027 enabled. 193type vtOracle struct { 194em * vt.Emulator 195} 196 197func newVT (t * testing.T ,w ,h int )oracle { 198t .Helper () 199return & vtOracle {em :vt .NewEmulator (w ,h )} 200} 201 202func (v * vtOracle )Write (p []byte ) (int ,error ) {return v .em .Write (p ) } 203func (v * vtOracle )Size () (int ,int ) {return v .em .Width (),v .em .Height () } 204func (v * vtOracle )Close () {} 205func (v * vtOracle )Name ()string {return "vt" } 206func (v * vtOracle )Resize (w ,h int ) {v .em .Resize (w ,h ) } 207 208func (v * vtOracle )CellAt (t * testing.T ,x ,y int )string { 209t .Helper () 210 211if cell := v .em .CellAt (x ,y );cell != nil { 212return cell .Content 213 } 214return "" 215} 216 217func (v * vtOracle )Row (t * testing.T ,y int )string { 218t .Helper () 219 220var b strings.Builder 221for x := range v .em .Width () { 222b .WriteString (v .CellAt (t ,x ,y )) 223 } 224return strings .TrimRight (b .String ()," " ) 225}