package gozstd import ( "bytes" "fmt" "math/rand" "os" "os/exec" "path/filepath" "sync" "testing" ) // These tests prove the load-bearing interop property of the shim: the pure-Go // klauspost backend decodes zstd frames produced by upstream libzstd — plain // AND dictionary-compressed — which is exactly what dolt's NBS archive reader // asks of gozstd at runtime (it decompresses .darc archives authored by the // real `dolt`/libzstd). They shell out to the `zstd` CLI (libzstd) to author // the fixtures and skip cleanly when it is absent, so CI without the tool still // passes. The internal round-trip test needs no external tool. func zstdBin(t *testing.T) string { t.Helper() p, err := exec.LookPath("zstd") if err != nil { t.Skip("zstd CLI (libzstd) not found; skipping libzstd-interop test") } return p } // TestDecompressLibzstdPlain: libzstd compresses, the shim decompresses. func TestDecompressLibzstdPlain(t *testing.T) { zstd := zstdBin(t) orig := bytes.Repeat([]byte("the quick brown fox jumps over the lazy dog\n"), 500) dir := t.TempDir() in := filepath.Join(dir, "in") out := filepath.Join(dir, "in.zst") if err := os.WriteFile(in, orig, 0o644); err != nil { t.Fatal(err) } run(t, zstd, "-q", "-19", in, "-o", out) comp, err := os.ReadFile(out) if err != nil { t.Fatal(err) } got, err := Decompress(nil, comp) if err != nil { t.Fatalf("Decompress: %v", err) } if !bytes.Equal(got, orig) { t.Fatalf("plain: round-trip mismatch (%d vs %d bytes)", len(got), len(orig)) } } // TestDecompressDictLibzstd: libzstd trains a dictionary and compresses with it, // then the shim decompresses with the same dictionary via NewDDict — // the exact archive-read path dolt exercises. This is the interop risk the // feasibility review flagged (klauspost matches decoder dicts by embedded // dict-ID, structured ZDICT format), proven end-to-end here. func TestDecompressDictLibzstd(t *testing.T) { zstd := zstdBin(t) dir := t.TempDir() // Enough varied-but-similar samples for the trainer to build a dictionary. var samples []string for i := 0; i < 512; i++ { p := filepath.Join(dir, fmt.Sprintf("s%03d", i)) line := fmt.Sprintf("issue memestudio-%04d: lorem ipsum dolor sit amet consectetur %d\n", i, i*7) if err := os.WriteFile(p, []byte(line), 0o644); err != nil { t.Fatal(err) } samples = append(samples, p) } dictPath := filepath.Join(dir, "dict") if out, err := exec.Command(zstd, append([]string{"--train", "--maxdict=8192", "-o", dictPath}, samples...)...).CombinedOutput(); err != nil { t.Skipf("zstd --train unavailable/failed (%v): %s", err, out) } dict, err := os.ReadFile(dictPath) if err != nil { t.Fatalf("read trained dict: %v", err) } orig := []byte("issue memestudio-0042: lorem ipsum dolor sit amet consectetur 294 — payload body\n") in := filepath.Join(dir, "payload") out := filepath.Join(dir, "payload.zst") if err := os.WriteFile(in, orig, 0o644); err != nil { t.Fatal(err) } run(t, zstd, "-q", "-19", "-D", dictPath, in, "-o", out) comp, err := os.ReadFile(out) if err != nil { t.Fatal(err) } dd, err := NewDDict(dict) if err != nil { t.Fatalf("NewDDict on libzstd-trained dict: %v", err) } defer dd.Release() got, err := DecompressDict(nil, comp, dd) if err != nil { t.Fatalf("DecompressDict on libzstd frame: %v", err) } if !bytes.Equal(got, orig) { t.Fatalf("dict: round-trip mismatch\n got=%q\nwant=%q", got, orig) } } // TestShimRoundTripPlain exercises the shim's own compress+decompress with no // external tool and no dictionary. func TestShimRoundTripPlain(t *testing.T) { orig := bytes.Repeat([]byte("parade lane rolling lined-up stalled past-stand\n"), 200) comp := Compress(nil, orig) got, err := Decompress(nil, comp) if err != nil { t.Fatalf("Decompress: %v", err) } if !bytes.Equal(got, orig) { t.Fatal("plain shim round-trip mismatch") } } // TestShimRoundTripDict exercises the shim's CompressDict + DecompressDict over // a real structured dictionary. The dictionary is trained by libzstd (the zstd // CLI) rather than the shim's own BuildDict, because BuildDict is off this // project's runtime path (dolt only calls it from archive-writer/gc, which this // binary never runs) and klauspost's trainer is stricter about corpus size than // libzstd's ZDICT — training quality is irrelevant here; what matters is that // the shim's encode+decode agree over a standard dictionary. func TestShimRoundTripDict(t *testing.T) { zstd := zstdBin(t) dir := t.TempDir() var samples []string for i := 0; i < 512; i++ { p := filepath.Join(dir, fmt.Sprintf("s%03d", i)) line := fmt.Sprintf("bead memestudio-%04d status open priority %d assignee eugene\n", i, i%4) if err := os.WriteFile(p, []byte(line), 0o644); err != nil { t.Fatal(err) } samples = append(samples, p) } dictPath := filepath.Join(dir, "dict") if out, err := exec.Command(zstd, append([]string{"--train", "--maxdict=8192", "-o", dictPath}, samples...)...).CombinedOutput(); err != nil { t.Skipf("zstd --train unavailable/failed (%v): %s", err, out) } dict, err := os.ReadFile(dictPath) if err != nil { t.Fatal(err) } orig := bytes.Repeat([]byte("bead memestudio-0042 status open priority 2 assignee eugene\n"), 50) cd, err := NewCDict(dict) if err != nil { t.Fatalf("NewCDict: %v", err) } defer cd.Release() dd, err := NewDDict(dict) if err != nil { t.Fatalf("NewDDict: %v", err) } defer dd.Release() dcomp := CompressDict(nil, orig, cd) dgot, err := DecompressDict(nil, dcomp, dd) if err != nil { t.Fatalf("DecompressDict: %v", err) } if !bytes.Equal(dgot, orig) { t.Fatal("dict shim round-trip mismatch") } } // TestCompressReadableByLibzstd proves the reverse interop direction: frames the // shim produces are decodable by upstream libzstd (the zstd CLI), both plain and // dictionary-compressed. dolt never consumes shim-written archives at runtime, // but this confirms the shim emits standard RFC-8878 frames, not a klauspost // dialect. func TestCompressReadableByLibzstd(t *testing.T) { zstd := zstdBin(t) dir := t.TempDir() orig := bytes.Repeat([]byte("shim output must be standard zstd\n"), 300) // plain: shim compresses, zstd -d decompresses. comp := Compress(nil, orig) czst := filepath.Join(dir, "c.zst") if err := os.WriteFile(czst, comp, 0o644); err != nil { t.Fatal(err) } cout := filepath.Join(dir, "c.out") run(t, zstd, "-q", "-d", czst, "-o", cout) if got, _ := os.ReadFile(cout); !bytes.Equal(got, orig) { t.Fatal("plain: libzstd could not round-trip shim output") } // dict: shim compresses with a libzstd-trained dict, zstd -d -D decompresses. var samples []string for i := 0; i < 512; i++ { p := filepath.Join(dir, fmt.Sprintf("s%03d", i)) if err := os.WriteFile(p, []byte(fmt.Sprintf("bead memestudio-%04d status open priority %d\n", i, i%4)), 0o644); err != nil { t.Fatal(err) } samples = append(samples, p) } dictPath := filepath.Join(dir, "dict") if out, err := exec.Command(zstd, append([]string{"--train", "--maxdict=8192", "-o", dictPath}, samples...)...).CombinedOutput(); err != nil { t.Skipf("zstd --train failed (%v): %s", err, out) } dict, err := os.ReadFile(dictPath) if err != nil { t.Fatal(err) } cd, err := NewCDict(dict) if err != nil { t.Fatalf("NewCDict: %v", err) } defer cd.Release() dorig := bytes.Repeat([]byte("bead memestudio-0042 status open priority 2\n"), 40) dcomp := CompressDict(nil, dorig, cd) dzst := filepath.Join(dir, "d.zst") if err := os.WriteFile(dzst, dcomp, 0o644); err != nil { t.Fatal(err) } dout := filepath.Join(dir, "d.out") run(t, zstd, "-q", "-d", "-D", dictPath, dzst, "-o", dout) if got, _ := os.ReadFile(dout); !bytes.Equal(got, dorig) { t.Fatal("dict: libzstd could not round-trip shim dict output") } } // TestAppendSemantics verifies the load-bearing dst-append contract: gozstd's // Compress/Decompress/CompressDict/DecompressDict append to dst and return the // grown slice, preserving any existing prefix. dolt passes non-empty buffers, so // a klauspost mismatch here would silently corrupt its data. func TestAppendSemantics(t *testing.T) { orig := []byte("append into an existing buffer without clobbering the prefix") prefix := []byte("PREFIX|") comp := Compress(append([]byte{}, prefix...), orig) if !bytes.HasPrefix(comp, prefix) { t.Fatal("Compress dropped the dst prefix") } got, err := Decompress(append([]byte{}, prefix...), comp[len(prefix):]) if err != nil { t.Fatal(err) } if !bytes.Equal(got, append(append([]byte{}, prefix...), orig...)) { t.Fatal("Decompress did not append after the dst prefix") } // same for the dict variants cd, dd := shimDict(t) defer cd.Release() defer dd.Release() dc := CompressDict(append([]byte{}, prefix...), orig, cd) if !bytes.HasPrefix(dc, prefix) { t.Fatal("CompressDict dropped the dst prefix") } dg, err := DecompressDict(append([]byte{}, prefix...), dc[len(prefix):], dd) if err != nil { t.Fatal(err) } if !bytes.Equal(dg, append(append([]byte{}, prefix...), orig...)) { t.Fatal("DecompressDict did not append after the dst prefix") } } // TestRoundTripEdgeCases covers empty, tiny, incompressible-random, and large // inputs through the plain path. func TestRoundTripEdgeCases(t *testing.T) { rng := rand.New(rand.NewSource(1)) // deterministic big := make([]byte, 4<<20) rng.Read(big) cases := map[string][]byte{ "empty": {}, "one-byte": {0x42}, "incompressible": randBytes(rng, 64<<10), "large-random": big, "large-text": bytes.Repeat([]byte("mardi gras parade "), 300000), } for name, orig := range cases { got, err := Decompress(nil, Compress(nil, orig)) if err != nil { t.Fatalf("%s: Decompress: %v", name, err) } if !bytes.Equal(got, orig) { t.Fatalf("%s: round-trip mismatch (%d vs %d bytes)", name, len(got), len(orig)) } } } // TestConcurrent exercises the concurrency-safety the shim claims (dolt calls // these from many goroutines): shared coders for plain Compress/Decompress and a // single shared DDict for DecompressDict. func TestConcurrent(t *testing.T) { cd, dd := shimDict(t) defer cd.Release() defer dd.Release() payload := bytes.Repeat([]byte("concurrent parade "), 500) dictFrame := CompressDict(nil, payload, cd) const n = 64 errs := make(chan error, n) var wg sync.WaitGroup for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() // plain round-trip on distinct data src := []byte(fmt.Sprintf("goroutine-%d-%s", i, payload)) if got, err := Decompress(nil, Compress(nil, src)); err != nil || !bytes.Equal(got, src) { errs <- fmt.Errorf("plain g%d: err=%v equal=%v", i, err, bytes.Equal(got, src)) return } // concurrent DecompressDict on the shared DDict if got, err := DecompressDict(nil, dictFrame, dd); err != nil || !bytes.Equal(got, payload) { errs <- fmt.Errorf("dict g%d: err=%v equal=%v", i, err, bytes.Equal(got, payload)) return } }(i) } wg.Wait() close(errs) for err := range errs { t.Error(err) } } // TestErrorPaths confirms bad input yields an error, never a panic — dolt's // archive reader handles the error return. func TestErrorPaths(t *testing.T) { if _, err := Decompress(nil, []byte("not a zstd frame at all")); err == nil { t.Fatal("Decompress of garbage: want error, got nil") } // Empty input is a benign divergence from libzstd (which errors): klauspost // yields empty output, no error, no panic. dolt never decompresses empty // input, so this is documented rather than treated as a fault. if got, err := Decompress(nil, nil); err != nil || len(got) != 0 { t.Fatalf("Decompress of empty input: want (empty, nil), got (%d bytes, %v)", len(got), err) } // A frame compressed with dict A must not decode under dict B (dict-ID // mismatch) — and must fail cleanly rather than return wrong bytes. a := buildShimDict(t, "alpha") b := buildShimDict(t, "bravo") cdA, _ := NewCDict(a) defer cdA.Release() ddB, _ := NewDDict(b) defer ddB.Release() frame := CompressDict(nil, bytes.Repeat([]byte("alpha payload "), 50), cdA) if _, err := DecompressDict(nil, frame, ddB); err == nil { t.Fatal("DecompressDict under the wrong dict: want error, got nil") } } // --- test helpers --- // shimDict builds a matching CDict/DDict pair from a libzstd-trained dictionary // (via the zstd CLI); it skips the calling test if the CLI is unavailable. func shimDict(t *testing.T) (*CDict, *DDict) { t.Helper() d := buildShimDict(t, "shared") cd, err := NewCDict(d) if err != nil { t.Fatalf("NewCDict: %v", err) } dd, err := NewDDict(d) if err != nil { t.Fatalf("NewDDict: %v", err) } return cd, dd } // buildShimDict trains a distinct structured dictionary via the zstd CLI, seeded // by tag so different tags yield different dict-IDs. func buildShimDict(t *testing.T, tag string) []byte { t.Helper() zstd := zstdBin(t) dir := t.TempDir() var samples []string for i := 0; i < 512; i++ { p := filepath.Join(dir, fmt.Sprintf("s%03d", i)) if err := os.WriteFile(p, []byte(fmt.Sprintf("%s bead-%04d status open priority %d assignee eugene\n", tag, i, i%4)), 0o644); err != nil { t.Fatal(err) } samples = append(samples, p) } dictPath := filepath.Join(dir, "dict") if out, err := exec.Command(zstd, append([]string{"--train", "--maxdict=8192", "-o", dictPath}, samples...)...).CombinedOutput(); err != nil { t.Skipf("zstd --train failed (%v): %s", err, out) } d, err := os.ReadFile(dictPath) if err != nil { t.Fatal(err) } return d } func randBytes(rng *rand.Rand, n int) []byte { b := make([]byte, n) rng.Read(b) return b } func run(t *testing.T, name string, args ...string) { t.Helper() if out, err := exec.Command(name, args...).CombinedOutput(); err != nil { t.Fatalf("%s %v: %v\n%s", name, args, err, out) } }