ci(vt): add standalone compatibility checks
- commit
- 6db0cd727561c7af4d58cf5d331d95e5153fb71d
- author
- brice · · 2026-08-09 11:09 UTC
- parent
- 80c3c4a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +107 -0
M .github/workflows/ci.yml +2 -0
| @@ -50,3 +50,5 @@ jobs: | |||
| 50 | 50 | run: go test . -run '^$' -fuzz FuzzHistoryPreflightMatchesUnmarshal -fuzztime=10s | |
| 51 | 51 | - name: Benchmark smoke | |
| 52 | 52 | run: go test ./... -run '^$' -bench='(VT|History|Snapshot|ANSI)' -benchmem -benchtime=1x | |
| 53 | + | - name: External consumer smoke | |
| 54 | + | run: scripts/consumer-smoke.sh | |
| @@ -50,3 +50,5 @@ jobs: | |||||
| 50 | run: go test . -run '^$' -fuzz FuzzHistoryPreflightMatchesUnmarshal -fuzztime=10s | 50 | run: go test . -run '^$' -fuzz FuzzHistoryPreflightMatchesUnmarshal -fuzztime=10s | ||
| 51 | - name: Benchmark smoke | 51 | - name: Benchmark smoke | ||
| 52 | run: go test ./... -run '^$' -bench='(VT|History|Snapshot|ANSI)' -benchmem -benchtime=1x | 52 | run: go test ./... -run '^$' -bench='(VT|History|Snapshot|ANSI)' -benchmem -benchtime=1x | ||
| 53 | + | - name: External consumer smoke | |||
| 54 | + | run: scripts/consumer-smoke.sh | |||
A fixture_compatibility_test.go +47 -0
| @@ -0,0 +1,47 @@ | |||
| 1 | + | package vt_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "embed" | |
| 6 | + | "encoding/binary" | |
| 7 | + | "encoding/hex" | |
| 8 | + | "hash/crc32" | |
| 9 | + | "strings" | |
| 10 | + | "testing" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | //go:embed testdata/vevs-v4-restore.golden.hex | |
| 14 | + | var fixtures embed.FS | |
| 15 | + | ||
| 16 | + | func TestVEVSV4FixtureIsDeterministic(t *testing.T) { | |
| 17 | + | encoded, err := fixtures.ReadFile("testdata/vevs-v4-restore.golden.hex") | |
| 18 | + | if err != nil { | |
| 19 | + | t.Fatal(err) | |
| 20 | + | } | |
| 21 | + | data, err := hex.DecodeString(strings.TrimSpace(string(encoded))) | |
| 22 | + | if err != nil { | |
| 23 | + | t.Fatalf("decode fixture: %v", err) | |
| 24 | + | } | |
| 25 | + | if len(data) < 16 || !bytes.Equal(data[:4], []byte("VEVS")) { | |
| 26 | + | t.Fatalf("fixture header = %x, want VEVS", data[:min(len(data), 4)]) | |
| 27 | + | } | |
| 28 | + | if version := binary.BigEndian.Uint16(data[4:6]); version != 4 { | |
| 29 | + | t.Fatalf("fixture version = %d, want 4", version) | |
| 30 | + | } | |
| 31 | + | if bodyLength := binary.BigEndian.Uint32(data[8:12]); int(bodyLength) != len(data)-16 { | |
| 32 | + | t.Fatalf("fixture body length = %d, want %d", bodyLength, len(data)-16) | |
| 33 | + | } | |
| 34 | + | if got := binary.BigEndian.Uint32(data[12:16]); got != crc32.ChecksumIEEE(data[16:]) { | |
| 35 | + | t.Fatalf("fixture CRC = %#x, want %#x", got, crc32.ChecksumIEEE(data[16:])) | |
| 36 | + | } | |
| 37 | + | if got := bytes.Count(data, []byte("VTH1")); got != 3 { | |
| 38 | + | t.Fatalf("embedded VTH3 marker count = %d, want 3", got) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func min(a, b int) int { | |
| 43 | + | if a < b { | |
| 44 | + | return a | |
| 45 | + | } | |
| 46 | + | return b | |
| 47 | + | } | |
| @@ -0,0 +1,47 @@ | |||||
| 1 | + | package vt_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "embed" | |||
| 6 | + | "encoding/binary" | |||
| 7 | + | "encoding/hex" | |||
| 8 | + | "hash/crc32" | |||
| 9 | + | "strings" | |||
| 10 | + | "testing" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | //go:embed testdata/vevs-v4-restore.golden.hex | |||
| 14 | + | var fixtures embed.FS | |||
| 15 | + | ||||
| 16 | + | func TestVEVSV4FixtureIsDeterministic(t *testing.T) { | |||
| 17 | + | encoded, err := fixtures.ReadFile("testdata/vevs-v4-restore.golden.hex") | |||
| 18 | + | if err != nil { | |||
| 19 | + | t.Fatal(err) | |||
| 20 | + | } | |||
| 21 | + | data, err := hex.DecodeString(strings.TrimSpace(string(encoded))) | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Fatalf("decode fixture: %v", err) | |||
| 24 | + | } | |||
| 25 | + | if len(data) < 16 || !bytes.Equal(data[:4], []byte("VEVS")) { | |||
| 26 | + | t.Fatalf("fixture header = %x, want VEVS", data[:min(len(data), 4)]) | |||
| 27 | + | } | |||
| 28 | + | if version := binary.BigEndian.Uint16(data[4:6]); version != 4 { | |||
| 29 | + | t.Fatalf("fixture version = %d, want 4", version) | |||
| 30 | + | } | |||
| 31 | + | if bodyLength := binary.BigEndian.Uint32(data[8:12]); int(bodyLength) != len(data)-16 { | |||
| 32 | + | t.Fatalf("fixture body length = %d, want %d", bodyLength, len(data)-16) | |||
| 33 | + | } | |||
| 34 | + | if got := binary.BigEndian.Uint32(data[12:16]); got != crc32.ChecksumIEEE(data[16:]) { | |||
| 35 | + | t.Fatalf("fixture CRC = %#x, want %#x", got, crc32.ChecksumIEEE(data[16:])) | |||
| 36 | + | } | |||
| 37 | + | if got := bytes.Count(data, []byte("VTH1")); got != 3 { | |||
| 38 | + | t.Fatalf("embedded VTH3 marker count = %d, want 3", got) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func min(a, b int) int { | |||
| 43 | + | if a < b { | |||
| 44 | + | return a | |||
| 45 | + | } | |||
| 46 | + | return b | |||
| 47 | + | } | |||
A scripts/consumer-smoke.sh +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | set -euo pipefail | |
| 3 | + | ||
| 4 | + | root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | |
| 5 | + | if grep -q '^replace ' "$root/go.mod"; then | |
| 6 | + | echo 'standalone module must not commit a replace directive' >&2 | |
| 7 | + | exit 1 | |
| 8 | + | fi | |
| 9 | + | ||
| 10 | + | tmp=$(mktemp -d) | |
| 11 | + | trap 'rm -rf "$tmp"' EXIT | |
| 12 | + | mkdir -p "$tmp/consumer" | |
| 13 | + | cat >"$tmp/consumer/go.mod" <<EOF | |
| 14 | + | module example.com/vev-vt-consumer | |
| 15 | + | ||
| 16 | + | go 1.26 | |
| 17 | + | ||
| 18 | + | require github.com/bnema/vev-vt v0.0.0 | |
| 19 | + | ||
| 20 | + | replace github.com/bnema/vev-vt => $root | |
| 21 | + | EOF | |
| 22 | + | cat >"$tmp/consumer/main.go" <<'EOF' | |
| 23 | + | package main | |
| 24 | + | ||
| 25 | + | import ( | |
| 26 | + | "fmt" | |
| 27 | + | "os" | |
| 28 | + | ||
| 29 | + | vt "github.com/bnema/vev-vt" | |
| 30 | + | "github.com/bnema/vev-vt/ansi" | |
| 31 | + | ) | |
| 32 | + | ||
| 33 | + | func main() { | |
| 34 | + | screen := vt.NewScreen(4, 1) | |
| 35 | + | screen.Write([]byte("ok")) | |
| 36 | + | output, err := ansi.New(ansi.Capabilities{}).Draw(screen.Frame.Clone(), []vt.Damage{vt.FullRedraw()}) | |
| 37 | + | if err != nil { | |
| 38 | + | fmt.Fprintln(os.Stderr, err) | |
| 39 | + | os.Exit(1) | |
| 40 | + | } | |
| 41 | + | if len(output) == 0 { | |
| 42 | + | fmt.Fprintln(os.Stderr, "consumer received empty ANSI output") | |
| 43 | + | os.Exit(1) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | EOF | |
| 47 | + | ||
| 48 | + | ( | |
| 49 | + | cd "$tmp/consumer" | |
| 50 | + | GOWORK=off go test ./... | |
| 51 | + | ) | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | #!/usr/bin/env bash | |||
| 2 | + | set -euo pipefail | |||
| 3 | + | ||||
| 4 | + | root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | |||
| 5 | + | if grep -q '^replace ' "$root/go.mod"; then | |||
| 6 | + | echo 'standalone module must not commit a replace directive' >&2 | |||
| 7 | + | exit 1 | |||
| 8 | + | fi | |||
| 9 | + | ||||
| 10 | + | tmp=$(mktemp -d) | |||
| 11 | + | trap 'rm -rf "$tmp"' EXIT | |||
| 12 | + | mkdir -p "$tmp/consumer" | |||
| 13 | + | cat >"$tmp/consumer/go.mod" <<EOF | |||
| 14 | + | module example.com/vev-vt-consumer | |||
| 15 | + | ||||
| 16 | + | go 1.26 | |||
| 17 | + | ||||
| 18 | + | require github.com/bnema/vev-vt v0.0.0 | |||
| 19 | + | ||||
| 20 | + | replace github.com/bnema/vev-vt => $root | |||
| 21 | + | EOF | |||
| 22 | + | cat >"$tmp/consumer/main.go" <<'EOF' | |||
| 23 | + | package main | |||
| 24 | + | ||||
| 25 | + | import ( | |||
| 26 | + | "fmt" | |||
| 27 | + | "os" | |||
| 28 | + | ||||
| 29 | + | vt "github.com/bnema/vev-vt" | |||
| 30 | + | "github.com/bnema/vev-vt/ansi" | |||
| 31 | + | ) | |||
| 32 | + | ||||
| 33 | + | func main() { | |||
| 34 | + | screen := vt.NewScreen(4, 1) | |||
| 35 | + | screen.Write([]byte("ok")) | |||
| 36 | + | output, err := ansi.New(ansi.Capabilities{}).Draw(screen.Frame.Clone(), []vt.Damage{vt.FullRedraw()}) | |||
| 37 | + | if err != nil { | |||
| 38 | + | fmt.Fprintln(os.Stderr, err) | |||
| 39 | + | os.Exit(1) | |||
| 40 | + | } | |||
| 41 | + | if len(output) == 0 { | |||
| 42 | + | fmt.Fprintln(os.Stderr, "consumer received empty ANSI output") | |||
| 43 | + | os.Exit(1) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | EOF | |||
| 47 | + | ||||
| 48 | + | ( | |||
| 49 | + | cd "$tmp/consumer" | |||
| 50 | + | GOWORK=off go test ./... | |||
| 51 | + | ) | |||
A testdata/README.md +7 -0
| @@ -0,0 +1,7 @@ | |||
| 1 | + | # Compatibility fixtures | |
| 2 | + | ||
| 3 | + | `vevs-v4-restore.golden.hex` is an opaque VEVS v4 restore envelope retained for | |
| 4 | + | vev consumer compatibility. The standalone module does not decode or own the | |
| 5 | + | VEVS outer format; its fixture test verifies the envelope header, length, CRC, | |
| 6 | + | and embedded VTH3 payload markers only. The vev repository owns full VEVS restore | |
| 7 | + | semantics. | |
| @@ -0,0 +1,7 @@ | |||||
| 1 | + | # Compatibility fixtures | |||
| 2 | + | ||||
| 3 | + | `vevs-v4-restore.golden.hex` is an opaque VEVS v4 restore envelope retained for | |||
| 4 | + | vev consumer compatibility. The standalone module does not decode or own the | |||
| 5 | + | VEVS outer format; its fixture test verifies the envelope header, length, CRC, | |||
| 6 | + | and embedded VTH3 payload markers only. The vev repository owns full VEVS restore | |||
| 7 | + | semantics. | |||