Skip to content
go-libfossil Artifact [b8d970fc8b]
Login

Artifact [b8d970fc8b]

Artifact b8d970fc8ba47d3f67490853eac62d41ec9149ef3cff7ca7b2144cd5992b932c:


package blob

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"
	"testing"

	"github.com/danmestas/libfossil/db"
	_ "github.com/danmestas/libfossil/internal/testdriver"
	"github.com/danmestas/libfossil/simio"
)

func setupTestDB(t *testing.T) *db.DB {
	t.Helper()
	path := filepath.Join(t.TempDir(), "test.fossil")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("db.Open: %v", err)
	}
	if err := db.CreateRepoSchema(d); err != nil {
		t.Fatalf("CreateRepoSchema: %v", err)
	}
	t.Cleanup(func() { d.Close() })
	return d
}

func TestStoreAndLoad(t *testing.T) {
	d := setupTestDB(t)
	content := []byte("hello fossil world")

	rid, uuid, err := Store(d, content)
	if err != nil {
		t.Fatalf("Store: %v", err)
	}
	if rid <= 0 {
		t.Fatalf("rid = %d, want > 0", rid)
	}
	if len(uuid) != 40 && len(uuid) != 64 {
		t.Fatalf("uuid length = %d, want 40 or 64", len(uuid))
	}

	got, err := Load(d, rid)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if !bytes.Equal(got, content) {
		t.Fatalf("Load = %q, want %q", got, content)
	}
}

func TestExists(t *testing.T) {
	d := setupTestDB(t)
	content := []byte("existence test")

	_, uuid, err := Store(d, content)
	if err != nil {
		t.Fatalf("Store: %v", err)
	}

	rid, ok := Exists(d, uuid)
	if !ok {
		t.Fatal("Exists returned false for stored blob")
	}
	if rid <= 0 {
		t.Fatalf("Exists rid = %d, want > 0", rid)
	}

	_, ok = Exists(d, "0000000000000000000000000000000000000000")
	if ok {
		t.Fatal("Exists returned true for non-existent blob")
	}
}

func TestStorePhantom(t *testing.T) {
	d := setupTestDB(t)
	uuid := "da39a3ee5e6b4b0d3255bfef95601890afd80709"

	rid, err := StorePhantom(d, uuid)
	if err != nil {
		t.Fatalf("StorePhantom: %v", err)
	}
	if rid <= 0 {
		t.Fatalf("phantom rid = %d, want > 0", rid)
	}

	var count int
	d.QueryRow("SELECT count(*) FROM phantom WHERE rid=?", rid).Scan(&count)
	if count != 1 {
		t.Fatalf("phantom table count = %d, want 1", count)
	}

	var size int64
	d.QueryRow("SELECT size FROM blob WHERE rid=?", rid).Scan(&size)
	if size != -1 {
		t.Fatalf("phantom size = %d, want -1", size)
	}
}

func TestStoreDelta(t *testing.T) {
	d := setupTestDB(t)
	source := []byte("original content here")
	target := []byte("original content modified")

	srcRid, _, err := Store(d, source)
	if err != nil {
		t.Fatalf("Store source: %v", err)
	}

	tgtRid, _, err := StoreDelta(d, target, srcRid)
	if err != nil {
		t.Fatalf("StoreDelta: %v", err)
	}
	if tgtRid <= 0 {
		t.Fatalf("tgtRid = %d, want > 0", tgtRid)
	}

	var srcid int64
	err = d.QueryRow("SELECT srcid FROM delta WHERE rid=?", tgtRid).Scan(&srcid)
	if err != nil {
		t.Fatalf("delta row missing: %v", err)
	}
	if srcid != int64(srcRid) {
		t.Fatalf("delta.srcid = %d, want %d", srcid, srcRid)
	}
}

func TestStoreMarksUnclustered(t *testing.T) {
	d := setupTestDB(t)
	content := []byte("unclustered test blob")

	rid, _, err := Store(d, content)
	if err != nil {
		t.Fatalf("Store: %v", err)
	}

	var count int
	d.QueryRow("SELECT count(*) FROM unclustered WHERE rid=?", rid).Scan(&count)
	if count != 1 {
		t.Fatalf("unclustered count = %d, want 1", count)
	}
}

func TestStoreDeltaMarksUnclustered(t *testing.T) {
	d := setupTestDB(t)
	source := []byte("delta source content here")
	target := []byte("delta target content here")

	srcRid, _, _ := Store(d, source)
	tgtRid, _, err := StoreDelta(d, target, srcRid)
	if err != nil {
		t.Fatalf("StoreDelta: %v", err)
	}

	for _, rid := range []int64{int64(srcRid), int64(tgtRid)} {
		var count int
		d.QueryRow("SELECT count(*) FROM unclustered WHERE rid=?", rid).Scan(&count)
		if count != 1 {
			t.Fatalf("unclustered count for rid %d = %d, want 1", rid, count)
		}
	}
}

func TestStoreExistingBlobSkipsUnclustered(t *testing.T) {
	d := setupTestDB(t)
	content := []byte("idempotent blob test")

	rid, _, _ := Store(d, content)
	d.Exec("DELETE FROM unclustered WHERE rid=?", rid)

	rid2, _, err := Store(d, content)
	if err != nil {
		t.Fatalf("Store: %v", err)
	}
	if rid2 != rid {
		t.Fatalf("rid = %d, want %d (same blob)", rid2, rid)
	}

	var count int
	d.QueryRow("SELECT count(*) FROM unclustered WHERE rid=?", rid).Scan(&count)
	if count != 0 {
		t.Fatalf("unclustered count = %d, want 0 (already-existing blob)", count)
	}
}

// TestStoreVerifyCatchesBuggify enables BUGGIFY and runs Store in a loop.
// The Decompress BUGGIFY site (2% truncation) should cause at least one
// verify failure, confirming the round-trip check catches corruption.
func TestStoreVerifyCatchesBuggify(t *testing.T) {
	var caught int
	for seed := int64(0); seed < 50; seed++ {
		d := setupTestDB(t)
		simio.EnableBuggify(seed)

		content := []byte(fmt.Sprintf("buggify store test seed=%d with enough data to compress", seed))
		_, _, err := Store(d, content)

		simio.DisableBuggify()

		if err != nil && strings.Contains(err.Error(), "verify") {
			caught++
		}
	}
	if caught == 0 {
		t.Fatal("expected at least one verify failure across 50 BUGGIFY seeds")
	}
	t.Logf("caught %d verify failures out of 50 seeds", caught)
}

// TestStoreDeltaVerifyCatchesBuggify enables BUGGIFY and runs StoreDelta in a loop.
// The Decompress BUGGIFY site should cause verify failures on delta round-trip.
func TestStoreDeltaVerifyCatchesBuggify(t *testing.T) {
	var caught int
	// StoreDelta calls Decompress multiple times (source load + verify read-back),
	// so we need more seeds to hit the 2% BUGGIFY on the right call.
	for seed := int64(0); seed < 200; seed++ {
		d := setupTestDB(t)

		// Store source without BUGGIFY so we have a clean base.
		source := []byte("delta source content that is long enough to compress well in the test")
		srcRid, _, err := Store(d, source)
		if err != nil {
			t.Fatalf("seed %d: Store source: %v", seed, err)
		}

		simio.EnableBuggify(seed)

		target := []byte(fmt.Sprintf("delta target content modified for seed=%d with padding to ensure compression", seed))
		_, _, err = StoreDelta(d, target, srcRid)

		simio.DisableBuggify()

		if err != nil && strings.Contains(err.Error(), "verify") {
			caught++
		}
	}
	if caught == 0 {
		t.Fatal("expected at least one verify failure across 200 BUGGIFY seeds")
	}
	t.Logf("caught %d verify failures out of 200 seeds", caught)
}

func BenchmarkStore(b *testing.B) {
	d := func() *db.DB {
		path := filepath.Join(b.TempDir(), "bench.fossil")
		d, _ := db.Open(path)
		db.CreateRepoSchema(d)
		return d
	}()
	defer d.Close()

	data := bytes.Repeat([]byte("benchmark data"), 100)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		content := append(data, byte(i), byte(i>>8))
		Store(d, content)
	}
}