okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

nekomimi.petfix(input): keep control strings whole when they span reads7bfbaab

main
1 folder9 files

files

testdata/
README.md5.8 KiB
corpus_test.go6.2 KiB
fuzz_test.go21.2 KiB
go.mod1.1 KiB
go.sum3.3 KiB
oracle_test.go6.8 KiB
program.go21.5 KiB
program_test.go7.9 KiB
reflow_test.go2.1 KiB

Renderer conformance tests

Fuzz tests that drive the terminal renderer against real terminal emulators and compare the resulting screen against ground truth.

Why this is a separate module

The renderer keeps a model of what the terminal is currently showing and writes only the difference between that model and the next frame. When the model drifts out of step with reality the output is still valid ANSI and still looks plausible; it just paints the wrong thing. Asserting on escape sequences cannot catch that, because there is no single correct sequence to compare against.

Catching it needs a real terminal emulator to read the screen back from, and the most accurate one available is libghostty, ghostty's own VT engine. That is a cgo package requiring Go 1.26 and a C library, and neither cost belongs in the library that ships to users.

Keeping it in its own module means the root module never sees it: no dependency, no cgo, no Go version bump. Nothing here reaches anyone who imports ultraviolet.

Running

Everything needs libghostty-vt. Build it once with zig and CMake, from this directory:

# Build the commit go.mod depends on. go-libghostty's CMakeLists pins the
# ghostty revision it fetches, and a newer one exports a different set of
# symbols than these bindings call, so building its HEAD fails at link time.
version=$(awk '$1 == "go.mitchellh.com/libghostty" { print $2 }' go.mod)
rev=${version%+incompatible}
[[ $rev =~ -([0-9a-f]{12})$ ]] && rev=${BASH_REMATCH[1]}
git clone https://github.com/mitchellh/go-libghostty /tmp/go-libghostty
git -C /tmp/go-libghostty checkout "$rev"
make -C /tmp/go-libghostty build
export PKG_CONFIG_PATH="/tmp/go-libghostty/build/_deps/ghostty-src/zig-out/share/pkgconfig"

CI does the same thing from .github/actions/libghostty-vt, reading the pin out of the same line of go.mod. Splitting on the last dash is not enough: only a pseudo-version ends in a commit, so a tag has to be used as it stands.

Then, still from this directory:

# Run the fuzz targets over their seed corpus, plus the regular tests.
go test ./...

# Search for new failing inputs. One target at a time.
go test -run='XXX' -fuzz='FuzzRenderer$' -fuzztime=5m

Because these tests live outside the root module, go test ./... from the repository root does not run them. They have to be invoked here.

What is tested

Each target decodes its input as a small program of renderer operations, replays it, and checks a property of the result. Go's fuzzer mutates the raw bytes and uses coverage feedback to steer toward programs that reach new renderer code, which is what finds the combinations nobody thinks to write by hand.

TargetProperty
FuzzRendererAn incrementally rendered screen matches a full repaint of the same frame.
FuzzRendererIdempotentRendering an unchanged buffer twice leaves the screen alone.
FuzzRedrawResyncsA forced repaint recovers from any state the renderer drifted into.
FuzzScreenShowsContentEvery cluster the buffer holds on the last drawn row reaches the screen.

Each target also runs both ways the renderer can own a screen, chosen per program:

Every target runs against two emulators, because they disagree about how wide a grapheme cluster is and that disagreement is the subject of these tests:

Neither is redundant. A bug that only appears under legacy widths is invisible to x/vt, and a disagreement between the two is itself worth investigating.

The corpus

testdata/fuzz/ holds the inputs the nightly run has found. Each one was a real renderer bug when it landed, so keeping it there is what stops the bug coming back: the test job replays the whole corpus on every push, and Go validates it before any mutation starts.

Add to it rather than pruning it. When a nightly run fails, download the crashers-* artifact and commit the new file alongside the fix.