okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
ab6f6cb
upstream
1package conformance_test 2 3import ( 4"strings" 5"testing" 6 7"github.com/charmbracelet/ultraviolet/internal/conformance" 8"github.com/charmbracelet/x/ansi" 9) 10 11// Tests for the fuzzing machinery itself. 12// 13// A fuzzer is only as good as its input decoder and its seeds, and both can rot 14// silently. A decoder that panics turns every fuzz run into a false positive; a 15// seed corpus that no longer decodes into anything interesting leaves the 16// fuzzer exploring plain ASCII forever while still reporting success. Neither 17// failure is visible from the fuzz targets, so they are pinned down here. 18 19// TestDriftPremise guards the assumption the whole suite rests on. If a future 20// width table lines legacy widths up with grapheme widths for these clusters, 21// the emulators stop reproducing drift and every conformance test quietly 22// starts proving nothing. Fail loudly instead. 23func TestDriftPremise (t * testing.T ) { 24clusters := conformance .DriftClusters () 25if len (clusters )== 0 { 26t .Fatal ("no drift clusters defined" ) 27 } 28 29for _ ,cluster := range clusters { 30wc ,grapheme := ansi .StringWidthWc (cluster ),ansi .StringWidth (cluster ) 31if wc == grapheme { 32t .Errorf ("%q measures %d columns under both width methods, so it no " + 33"longer exercises column drift and should be replaced" ,cluster ,wc ) 34 } 35 } 36} 37 38// TestDecodeProgramTotal checks that decoding never panics and always yields an 39// in-range program. 40// 41// The decoder is the fuzzer's only interface to the renderer. A panic or an 42// out-of-range row here would surface as a fuzz failure that has nothing to do 43// with the renderer, so it is worth ruling out separately. 44func TestDecodeProgramTotal (t * testing.T ) { 45inputs := [][]byte { 46nil , 47 {}, 48 {0 }, 49 {255 }, 50 []byte (strings .Repeat ("\xff" ,512 )), 51 []byte (strings .Repeat ("\x00" ,512 )), 52 } 53// A spread of short inputs, since those are the ones most likely to run the 54// decoder off the end of its buffer partway through an operation. 55for i := range 256 { 56inputs = append (inputs , []byte {byte (i ),byte (i * 7 ),byte (i * 13 ),byte (i * 31 )}) 57 } 58inputs = append (inputs ,conformance .Seeds ()... ) 59 60for _ ,in := range inputs { 61p := conformance .DecodeProgram (in ) 62 63if p .Width < 1 || p .Height < 1 { 64t .Fatalf ("DecodeProgram(%x) gave a nonsensical size of %dx%d" ,in ,p .Width ,p .Height ) 65 } 66 67// The live dimensions, which an OpResize changes. Draw and move bounds 68// are checked against these, mirroring how the decoder computes them. 69curW ,curH := p .Width ,p .Height 70for i ,op := range p .Ops { 71switch op .Kind { 72case conformance .OpDrawLine : 73if op .Y < 0 || op .Y >= curH { 74t .Fatalf ("DecodeProgram(%x) op %d draws to row %d, outside the current %d-row screen" , 75in ,i ,op .Y ,curH ) 76 } 77if w := ansi .StringWidth (op .Text );w >= curW { 78t .Fatalf ("DecodeProgram(%x) op %d draws %d columns into the current %d-column screen, " + 79"which would wrap and make failures ambiguous" ,in ,i ,w ,curW ) 80 } 81case conformance .OpMoveTo : 82if op .N < 0 || op .N >= curW { 83t .Fatalf ("DecodeProgram(%x) op %d moves to column %d, outside the current %d-column screen" , 84in ,i ,op .N ,curW ) 85 } 86case conformance .OpResize : 87if op .W < conformance .MinResizeW || op .W > conformance .MaxResizeW { 88t .Fatalf ("DecodeProgram(%x) op %d resizes to width %d, outside [%d,%d]" , 89in ,i ,op .W ,conformance .MinResizeW ,conformance .MaxResizeW ) 90 } 91// An inline frame may collapse to nothing; a fullscreen one 92// may not, since the renderer owns every row of the screen. 93minH := conformance .MinResizeH 94if p .Inline { 95minH = 0 96 } 97if op .H < minH || op .H > conformance .MaxResizeH { 98t .Fatalf ("DecodeProgram(%x) op %d resizes to height %d, outside [%d,%d]" , 99in ,i ,op .H ,minH ,conformance .MaxResizeH ) 100 } 101if p .Inline && op .W < curW { 102t .Fatalf ("DecodeProgram(%x) op %d narrows an inline screen from %d to %d columns, " + 103"which rewraps rows the renderer then has no way to find" , 104in ,i ,curW ,op .W ) 105 } 106curW ,curH = op .W ,op .H 107 } 108 } 109 } 110} 111 112// TestDecodeProgramIsDeterministic checks that one input always decodes to one 113// program. Go's fuzzer reruns a failing input to reproduce it, which only works 114// if decoding is a pure function of the bytes. 115func TestDecodeProgramIsDeterministic (t * testing.T ) { 116for _ ,seed := range conformance .Seeds () { 117a ,b := conformance .DecodeProgram (seed ),conformance .DecodeProgram (seed ) 118if a .String ()!= b .String () { 119t .Fatalf ("DecodeProgram(%x) is not deterministic:\n%s\nvs\n%s" ,seed ,a ,b ) 120 } 121 } 122} 123 124// TestSeedCorpusIsInteresting guards the seeds against quietly going stale. 125// 126// The seeds are hand-encoded bytes that address clusters by index, so a change 127// to the alphabet or the decoder could turn them into noise with nothing 128// failing. A seed that never renders, or that draws no text, costs the fuzzer 129// nothing but also teaches it nothing. 130func TestSeedCorpusIsInteresting (t * testing.T ) { 131seeds := conformance .Seeds () 132if len (seeds )== 0 { 133t .Fatal ("seed corpus is empty" ) 134 } 135 136drift := conformance .DriftClusters () 137seenDrift := map [string ]bool {} 138var withText int 139seenMode := map [bool ]bool {} 140 141for i ,seed := range seeds { 142p := conformance .DecodeProgram (seed ) 143seenMode [p .Inline ]= true 144 145var renders ,draws int 146for _ ,op := range p .Ops { 147switch op .Kind { 148case conformance .OpRender ,conformance .OpRedraw : 149renders ++ 150case conformance .OpDrawLine : 151if op .Text == "" { 152continue 153 } 154draws ++ 155for _ ,c := range drift { 156if strings .Contains (op .Text ,c ) { 157seenDrift [c ]= true 158 } 159 } 160 } 161 } 162 163// One render paints; the bugs this suite exists for need a second frame 164// to diff against the first. 165if renders < 2 { 166t .Errorf ("seed %d renders %d times, too few to exercise the incremental path:\n%s" , 167i ,renders ,p ) 168 } 169if draws > 0 { 170withText ++ 171 } 172 } 173 174if withText == 0 { 175t .Error ("no seed draws any text, so the corpus has gone stale" ) 176 } 177for _ ,c := range drift { 178if ! seenDrift [c ] { 179t .Errorf ("no seed draws %q, so the fuzzer does not start anywhere near it" ,c ) 180 } 181 } 182for _ ,inline := range []bool {false ,true } { 183if ! seenMode [inline ] { 184t .Errorf ("no seed runs with inline=%v, so half the renderer starts unreached" ,inline ) 185 } 186 } 187} 188 189// An inline frame has to leave the terminal a row to spare. A frame that 190// reaches the last row scrolls the screen on the next newline, and content that 191// has scrolled sits at a different absolute row in every run, so the 192// differential targets would report the scroll as a disagreement. 193// 194// The decoder cannot produce a frame that tall today. This is here so that 195// widening the resize bounds fails loudly rather than turning the inline 196// targets into a source of false failures. 197func TestInlineFramesFitTheTerminal (t * testing.T ) { 198tallest := conformance .InlineRowsAbove + max (conformance .MaxResizeH ,decodedMaxHeight (t )) 199if tallest >= conformance .InlineTermHeight { 200t .Errorf ("a program can reach %d rows in a terminal of %d, leaving no room below the frame" , 201tallest ,conformance .InlineTermHeight ) 202 } 203} 204 205// decodedMaxHeight is the tallest starting frame the decoder will produce, found 206// by asking it, since the size mapping lives in the decoder rather than in a 207// constant this test could read. 208func decodedMaxHeight (t * testing.T )int { 209t .Helper () 210 211var tallest int 212for b := range 256 { 213p := conformance .DecodeProgram ([]byte {0 ,byte (b )}) 214tallest = max (tallest ,p .Height ) 215 } 216return tallest 217} 218 219// TestFuzzTargetsRunSeeds runs every fuzz target over its seed corpus, which is 220// what `go test` does for a FuzzXxx function without -fuzz. 221// 222// This is here as documentation as much as verification: it is the reason the 223// fuzz targets are useful in ordinary CI, not just during a dedicated fuzzing 224// run. The seeds alone make a decent regression suite. 225func TestFuzzTargetsRunSeeds (t * testing.T ) { 226if testing .Short () { 227t .Skip ("exercises every seed against both emulators" ) 228 } 229t .Log ("go test runs each FuzzXxx target over its seeds; " + 230"use -fuzz to search for new inputs" ) 231}