Skip to content
go-libfossil Artifact [6ccfc5e3c5]
Login

Artifact [6ccfc5e3c5]

Artifact 6ccfc5e3c5a31ada80f725d4cc621f596b8c2c22765971932855a9d4f0723330:


package db_test

import (
	"errors"
	"path/filepath"
	"testing"
	"time"

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

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

func rcvfromCount(t *testing.T, q db.Querier) int {
	t.Helper()
	var n int
	if err := q.QueryRow("SELECT count(*) FROM rcvfrom").Scan(&n); err != nil {
		t.Fatalf("count rcvfrom: %v", err)
	}
	return n
}

// A new repository has received nothing, so it has no rcvfrom row: fossil
// adds one only when content arrives (content_rcvid_init).
func TestNewSchemaHasNoReceipt(t *testing.T) {
	d := newSchemaDB(t)
	if n := rcvfromCount(t, d); n != 0 {
		t.Fatalf("rcvfrom rows = %d, want 0", n)
	}
}

// A receipt writes its row the first time it is asked for its id, with
// fossil's column values: mtime the Julian day of the receipt, and an empty
// nonce or address stored as NULL. Asking again reuses the row.
func TestReceiptRecordsOneRowWhenFirstUsed(t *testing.T) {
	d := newSchemaDB(t)
	at := time.Date(2026, 10, 18, 12, 34, 56, 789_000_000, time.UTC)
	rc := &db.Receipt{UID: 7, Nonce: "abc", IPAddr: "10.0.0.1", Clock: simio.NewSimClockAt(at)}
	if n := rcvfromCount(t, d); n != 0 {
		t.Fatalf("rcvfrom rows before use = %d, want 0", n)
	}
	first, err := rc.ID(d)
	if err != nil {
		t.Fatalf("ID: %v", err)
	}
	again, err := rc.ID(d)
	if err != nil {
		t.Fatalf("ID again: %v", err)
	}
	if first <= 0 || again != first {
		t.Fatalf("ids = %d, %d; want one positive id", first, again)
	}
	if n := rcvfromCount(t, d); n != 1 {
		t.Fatalf("rcvfrom rows = %d, want 1", n)
	}
	var uid int64
	var mtime float64
	var nonce, ip string
	// mtime+0 reads the stored REAL as a number: the column is declared
	// DATETIME, which some drivers would otherwise scan as a time.
	if err := d.QueryRow("SELECT uid, mtime+0, nonce, ipaddr FROM rcvfrom"+
		" WHERE rcvid=? AND typeof(mtime)='real'", first).Scan(&uid, &mtime, &nonce, &ip); err != nil {
		t.Fatalf("read row: %v", err)
	}
	want := libfossil.JulianDayFromMillis(at.UnixMilli())
	if uid != 7 || mtime != want || nonce != "abc" || ip != "10.0.0.1" {
		t.Fatalf("row = (%d, %v, %q, %q), want (7, %v, \"abc\", \"10.0.0.1\")",
			uid, mtime, nonce, ip, want)
	}

	local := &db.Receipt{UID: 1}
	id, err := local.ID(d)
	if err != nil {
		t.Fatalf("local ID: %v", err)
	}
	var nulls int
	if err := d.QueryRow("SELECT count(*) FROM rcvfrom WHERE rcvid=? AND nonce IS NULL"+
		" AND ipaddr IS NULL AND typeof(mtime)='real'", id).Scan(&nulls); err != nil {
		t.Fatalf("read local row: %v", err)
	}
	if nulls != 1 {
		t.Fatalf("local receipt row does not have NULL nonce and ipaddr and a real mtime")
	}
}

// A receipt whose row was rolled back records a new one on its next use, so
// no blob is ever left naming a row that does not exist.
func TestReceiptRecordsAgainAfterRollback(t *testing.T) {
	d := newSchemaDB(t)
	rc := &db.Receipt{UID: 1}
	errUndo := errors.New("undo")
	err := d.WithTx(func(tx *db.Tx) error {
		if _, err := rc.ID(tx); err != nil {
			return err
		}
		return errUndo
	})
	if !errors.Is(err, errUndo) {
		t.Fatalf("WithTx = %v, want errUndo", err)
	}
	id, err := rc.ID(d)
	if err != nil {
		t.Fatalf("ID after rollback: %v", err)
	}
	var n int
	if err := d.QueryRow("SELECT count(*) FROM rcvfrom WHERE rcvid=?", id).Scan(&n); err != nil {
		t.Fatalf("count: %v", err)
	}
	if n != 1 {
		t.Fatalf("receipt %d has no row after its first was rolled back", id)
	}
}

// A transaction stores under the receipt it was given; one given none gets a
// receipt of its own, shared by everything stored in it.
func TestReceiptIDFollowsTransaction(t *testing.T) {
	d := newSchemaDB(t)
	rc := &db.Receipt{UID: 3}
	err := d.WithTx(func(tx *db.Tx) error {
		a, err := db.ReceiptID(tx)
		if err != nil {
			return err
		}
		b, err := db.ReceiptID(tx)
		if err != nil {
			return err
		}
		if a != b {
			t.Errorf("a transaction without a receipt used two: %d, %d", a, b)
		}
		prev := tx.UseReceipt(rc)
		got, err := db.ReceiptID(tx)
		if err != nil {
			return err
		}
		want, err := rc.ID(tx)
		if err != nil {
			return err
		}
		if got != want || got == a {
			t.Errorf("ReceiptID = %d, want the given receipt's %d", got, want)
		}
		tx.UseReceipt(prev)
		return nil
	})
	if err != nil {
		t.Fatalf("WithTx: %v", err)
	}
	if n := rcvfromCount(t, d); n != 2 {
		t.Fatalf("rcvfrom rows = %d, want 2", n)
	}
}

// A DB bound to a transaction stores under that transaction's receipt.
func TestReceiptIDOnBoundDB(t *testing.T) {
	d := newSchemaDB(t)
	rc := &db.Receipt{UID: 5}
	err := d.WithBoundTx(func(b *db.DB) error {
		prev := b.UseReceipt(rc)
		defer b.UseReceipt(prev)
		got, err := db.ReceiptID(b)
		if err != nil {
			return err
		}
		want, err := rc.ID(b)
		if err != nil {
			return err
		}
		if got != want {
			t.Errorf("ReceiptID(bound) = %d, want %d", got, want)
		}
		return nil
	})
	if err != nil {
		t.Fatalf("WithBoundTx: %v", err)
	}
}