Skip to content
go-libfossil Artifact [9bb25eb0cc]
Login

Artifact [9bb25eb0cc]

Artifact 9bb25eb0cc662e1b3ab011be6c252fa647ba96b56225e38a821e63ab5e7b320b:


package content

import (
	"bytes"
	"context"
	"errors"
	"testing"
	"time"

	"github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/blob"
	"github.com/danmestas/go-libfossil/internal/deck"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
)

func compactDeck(t *testing.T, d *db.DB, artifact *deck.Deck) libfossil.FslID {
	t.Helper()
	data, err := artifact.Marshal()
	if err != nil {
		t.Fatal(err)
	}
	rid, _, err := blob.Store(d, data)
	if err != nil {
		t.Fatal(err)
	}
	return rid
}

func compactName(t *testing.T, d *db.DB, rid libfossil.FslID) string {
	t.Helper()
	var uuid string
	if err := d.QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil {
		t.Fatal(err)
	}
	return uuid
}

func TestCompactStoreSharedHistoryAndControls(t *testing.T) {
	for _, kind := range []string{"shared", "history", "attachment", "tree-alias"} {
		t.Run(kind, func(t *testing.T) {
			d, out, _, _ := compactFixture(t)
			uuid := compactName(t, d, out)
			switch kind {
			case "shared", "history":
				compactDeck(t, d, &deck.Deck{Type: deck.Checkin, D: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), F: []deck.FileCard{{Name: "src/prior-name", UUID: uuid}}})
			case "attachment":
				compactDeck(t, d, &deck.Deck{Type: deck.Attachment, D: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), A: &deck.AttachmentCard{Filename: "a", Target: "wiki", Source: uuid}})
			case "tree-alias":
				node := compactDeck(t, d, &deck.Deck{Type: deck.Submanifest, F: []deck.FileCard{{Name: "src/leaf", UUID: uuid}}})
				compactDeck(t, d, &deck.Deck{Type: deck.Checkin, D: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), S: []deck.SubmanifestCard{{Name: "src/leaf", UUID: compactName(t, d, node)}}})
			}
			n, err := CompactStore(context.Background(), d)
			if err != nil || n != 0 {
				t.Fatalf("compact = %d, %v", n, err)
			}
			if _, err := Expand(d, out); err != nil {
				t.Fatal("shared reference lost", err)
			}
		})
	}
}

func TestCompactStoreDeltaClosure(t *testing.T) {
	for _, retainedBy := range []string{"in-cone", "private", "unsent"} {
		t.Run(retainedBy, func(t *testing.T) { compactDeltaClosure(t, retainedBy) })
	}
}

func compactDeltaClosure(t *testing.T, retainedBy string) {
	t.Helper()
	d := setupTestDB(t)
	baseData := bytes.Repeat([]byte("file content line\n"), 100)
	base, baseName, err := blob.Store(d, baseData)
	if err != nil {
		t.Fatal(err)
	}
	middleData := append(bytes.Clone(baseData), []byte("middle\n")...)
	middle, middleName, err := blob.StoreDelta(d, middleData, base)
	if err != nil {
		t.Fatal(err)
	}
	targetData := append(bytes.Clone(middleData), []byte("target\n")...)
	target, targetName, err := blob.StoreDelta(d, targetData, middle)
	if err != nil {
		t.Fatal(err)
	}
	drop, dropName, err := blob.StoreDelta(d, append(bytes.Clone(baseData), []byte("drop\n")...), base)
	if err != nil {
		t.Fatal(err)
	}
	targetPath := "src/target"
	if retainedBy != "in-cone" {
		targetPath = "docs/target"
		if _, err := d.Exec("INSERT INTO "+retainedBy+"(rid) VALUES(?)", target); err != nil {
			t.Fatal(err)
		}
	}
	compactDeck(t, d, &deck.Deck{Type: deck.Checkin, D: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), F: []deck.FileCard{{Name: "docs/base", UUID: baseName}, {Name: "docs/drop", UUID: dropName}, {Name: "docs/middle", UUID: middleName}, {Name: targetPath, UUID: targetName}}})
	if err := SetStoreCone(d, mustCone(t, []string{"src"}, nil)); err != nil {
		t.Fatal(err)
	}
	if n, err := CompactStore(context.Background(), d); err != nil || n != 1 {
		t.Fatalf("compact = %d, %v", n, err)
	}
	for _, rid := range []libfossil.FslID{base, middle, target} {
		if _, err := Expand(d, rid); err != nil {
			t.Fatal("delta closure lost", err)
		}
	}
	var count int
	if err := d.QueryRow("SELECT count(*) FROM delta WHERE rid=?", drop).Scan(&count); err != nil || count != 0 {
		t.Fatalf("elided delta link = %d, %v", count, err)
	}
}

func TestCompactStoreRejectsCorruptionAndIncompleteSpine(t *testing.T) {
	for _, kind := range []string{"corrupt", "spine", "lost-policy"} {
		t.Run(kind, func(t *testing.T) {
			d, out, _, _ := compactFixture(t)
			switch kind {
			case "corrupt":
				if _, err := d.Exec("UPDATE blob SET content=x'00' WHERE rid=?", out); err != nil {
					t.Fatal(err)
				}
			case "spine":
				compactDeck(t, d, &deck.Deck{Type: deck.Checkin, D: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), S: []deck.SubmanifestCard{{Name: "src/a", UUID: "1111111111111111111111111111111111111111"}}})
			case "lost-policy":
				if _, err := d.Exec("UPDATE storecone SET cone=x'0099'"); err != nil {
					t.Fatal(err)
				}
			}
			n, err := CompactStore(context.Background(), d)
			if kind == "lost-policy" {
				if err != nil || n != 0 {
					t.Fatalf("lost policy = %d, %v", n, err)
				}
			} else if err == nil || n != 0 {
				t.Fatalf("unsafe compact = %d, %v", n, err)
			}
			var size int
			if err := d.QueryRow("SELECT size FROM blob WHERE rid=?", out).Scan(&size); err != nil || size < 0 {
				t.Fatalf("failure dropped content: %d, %v", size, err)
			}
		})
	}
}

type compactCancelContext struct {
	context.Context
	cancel context.CancelFunc
	polls  int
}

func (c *compactCancelContext) Err() error {
	c.polls++
	if c.polls == 7 {
		c.cancel()
	}
	return c.Context.Err()
}

func TestCompactStoreCancellationRollsBackWrites(t *testing.T) {
	d, out, _, _ := compactFixture(t)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	polled := &compactCancelContext{Context: ctx, cancel: cancel}
	if n, err := CompactStore(polled, d); !errors.Is(err, context.Canceled) || n != 0 {
		t.Fatalf("cancel after writes = %d, %v", n, err)
	}
	if polled.polls != 7 {
		t.Fatalf("polls = %d, want 7", polled.polls)
	}
	if _, err := Expand(d, out); err != nil {
		t.Fatal("cancel lost content", err)
	}
}

func TestCompactStoreRetainsUnpublishedContent(t *testing.T) {
	for _, table := range []string{"unsent", "private"} {
		t.Run(table, func(t *testing.T) {
			d, out, _, _ := compactFixture(t)
			if _, err := d.Exec("INSERT INTO "+table+"(rid) VALUES(?)", out); err != nil {
				t.Fatal(err)
			}
			if n, err := CompactStore(context.Background(), d); err != nil || n != 0 {
				t.Fatalf("unpublished content compacted: %d, %v", n, err)
			}
			if _, err := Expand(d, out); err != nil {
				t.Fatal(err)
			}
		})
	}
}

// A reader holds its old WAL snapshot while compaction commits. It sees the
// demotion only after ending that snapshot, never half a blob/phantom rewrite.
func TestCompactStoreReaderSnapshot(t *testing.T) {
	d, out, _, _ := compactFixture(t)
	pool, err := db.OpenSQL(d.Path(), db.OpenConfig{}, nil)
	if err != nil {
		t.Fatal(err)
	}
	defer pool.Close()
	conn, err := pool.Conn(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()
	if _, err := conn.ExecContext(context.Background(), "BEGIN"); err != nil {
		t.Fatal(err)
	}
	defer conn.ExecContext(context.Background(), "ROLLBACK")
	read := func() (int, int) {
		t.Helper()
		var size, phantoms int
		err := conn.QueryRowContext(context.Background(), `SELECT size,
   (SELECT count(*) FROM phantom WHERE rid=?) FROM blob WHERE rid=?`, out, out).Scan(&size, &phantoms)
		if err != nil {
			t.Fatal(err)
		}
		return size, phantoms
	}
	before, phantoms := read()
	if before < 0 || phantoms != 0 {
		t.Fatal("fixture is not held")
	}
	if n, err := CompactStore(context.Background(), d); err != nil || n != 1 {
		t.Fatalf("compact = %d, %v", n, err)
	}
	if size, phantoms := read(); size != before || phantoms != 0 {
		t.Fatalf("snapshot changed: %d, %d", size, phantoms)
	}
	if _, err := conn.ExecContext(context.Background(), "COMMIT"); err != nil {
		t.Fatal(err)
	}
	if size, phantoms := read(); size != -1 || phantoms != 1 {
		t.Fatalf("committed demotion = %d, %d", size, phantoms)
	}
}

func TestCompactStoreWriterExclusion(t *testing.T) {
	d, out, _, _ := compactFixture(t)
	other, err := db.OpenWith(d.Path(), db.OpenConfig{Pragmas: map[string]string{"busy_timeout": "1"}})
	if err != nil {
		t.Fatal(err)
	}
	defer other.Close()
	if err := d.WithTx(func(tx *db.Tx) error {
		if n, err := CompactStore(context.Background(), other); err == nil || n != 0 {
			t.Fatalf("writer exclusion = %d, %v", n, err)
		}
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := Expand(d, out); err != nil {
		t.Fatal("lock failure lost content", err)
	}
}