Skip to content
go-libfossil Artifact [83bb7fe89a]
Login

Artifact [83bb7fe89a]

Artifact 83bb7fe89a801f7d76a80df819b9ab86b6f97b2f6cf9efe3135661dfc12ae5c4:


package manifest

import (
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/testutil"
)

// ticketSchemas are ticket table definitions fossil accepts as a
// repository's ticket-table setting, each lacking columns the default
// schema has: fossil probes for tkt_ctime, tkt_rid, tkt_user and
// "baseline for" columns and copies a field both tables share from the
// ticket row when an artifact leaves it unset (src/tkt.c, ticket_insert).
// The first is shaped like fossil's own repository, whose ticketchng has
// neither tkt_rid nor tkt_user.
var ticketSchemas = map[string]string{
	"no rid, user or ctime; shared field; baseline": `
		CREATE TABLE repository.ticket(tkt_id INTEGER PRIMARY KEY, tkt_uuid TEXT UNIQUE,
		  tkt_mtime DATE, type TEXT, status TEXT, title TEXT, comment TEXT,
		  "baseline for title" INTEGER);
		CREATE TABLE repository.ticketchng(tkt_id INTEGER REFERENCES ticket,
		  tkt_mtime DATE, status TEXT, icomment TEXT);`,
	"rid only, no fields": `
		CREATE TABLE repository.ticket(tkt_id INTEGER PRIMARY KEY, tkt_uuid TEXT UNIQUE,
		  tkt_mtime DATE, tkt_ctime DATE, type TEXT, status TEXT, title TEXT, comment TEXT);
		CREATE TABLE repository.ticketchng(tkt_id INTEGER REFERENCES ticket,
		  tkt_rid INTEGER, tkt_mtime DATE);`,
	"user only, no rid or ctime": `
		CREATE TABLE repository.ticket(tkt_id INTEGER PRIMARY KEY, tkt_uuid TEXT UNIQUE,
		  tkt_mtime DATE, type TEXT, status TEXT, title TEXT, comment TEXT);
		CREATE TABLE repository.ticketchng(tkt_id INTEGER REFERENCES ticket, tkt_mtime DATE,
		  tkt_user TEXT, icomment TEXT);`,
}

// TestFossilBinaryTicketSchemaParity derives the ticket tables of a
// repository whose ticket schema lacks optional columns, and requires the
// rows fossil rebuild derives.
func TestFossilBinaryTicketSchemaParity(t *testing.T) {
	bin := testutil.RequireFossilBin(t)
	for name, schema := range ticketSchemas {
		t.Run(name, func(t *testing.T) {
			orig := fossilTicketHistory(t, bin, schema)
			mine := filepath.Join(t.TempDir(), "mine.fossil")
			copyRepoFile(t, orig, mine)
			if out, err := exec.Command(bin, "rebuild", orig).CombinedOutput(); err != nil {
				t.Fatalf("fossil rebuild: %v\n%s", err, out)
			}
			clearTicketDerived(t, mine)
			r, err := repo.Open(mine)
			if err != nil {
				t.Fatal(err)
			}
			if _, err := Crosslink(r); err != nil {
				t.Fatalf("Crosslink: %v", err)
			}
			if err := r.Close(); err != nil {
				t.Fatal(err)
			}
			for _, table := range []string{"ticket", "ticketchng"} {
				want, got := tableRows(t, orig, table), tableRows(t, mine, table)
				if !reflect.DeepEqual(got, want) {
					t.Errorf("%s:\n got  %q\n want %q", table, got, want)
				}
			}
		})
	}
}

// fossilTicketHistory makes a repository with the given ticket schema and
// a ticket created, amended without its shared field, and amended again.
func fossilTicketHistory(t *testing.T, bin, schema string) string {
	t.Helper()
	dir := t.TempDir()
	path := filepath.Join(dir, "t.fossil")
	work := filepath.Join(dir, "w")
	if err := os.Mkdir(work, 0o755); err != nil {
		t.Fatal(err)
	}
	run := func(wd string, args ...string) string {
		t.Helper()
		cmd := exec.Command(bin, args...)
		cmd.Dir = wd
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("fossil %s: %v\n%s", strings.Join(args, " "), err, out)
		}
		return string(out)
	}
	run(dir, "init", path, "-A", "alice")
	run(dir, "sql", "-R", path, "REPLACE INTO config(name, value, mtime) VALUES('ticket-table', '"+
		strings.ReplaceAll(schema, "'", "''")+"', 0)")
	run(dir, "rebuild", path)
	run(work, "open", path, "--user", "alice")
	run(work, "user", "new", "bob", "Bob", "secret", "--user", "alice")
	tkt := lastField(t, run(work, "ticket", "add", "title", "one", "status", "Open", "type", "Bug", "--user", "alice"))
	if strings.Contains(schema, "icomment") {
		run(work, "ticket", "change", tkt, "icomment", "a note", "--user", "bob")
	}
	run(work, "ticket", "change", tkt, "status", "Closed", "title", "two", "--user", "bob")
	run(work, "close", "--force")
	return path
}

func copyRepoFile(t *testing.T, from, to string) {
	t.Helper()
	src, err := os.Open(from)
	if err != nil {
		t.Fatal(err)
	}
	defer src.Close()
	dst, err := os.Create(to)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := io.Copy(dst, src); err != nil {
		t.Fatal(err)
	}
	if err := dst.Close(); err != nil {
		t.Fatal(err)
	}
}

func clearTicketDerived(t *testing.T, path string) {
	t.Helper()
	d, err := db.Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer d.Close()
	for _, tbl := range ticketDerivedTables {
		if _, err := d.Exec("DELETE FROM " + tbl); err != nil && !strings.Contains(err.Error(), "no such table") {
			t.Fatalf("clear %s: %v", tbl, err)
		}
	}
}

// tableRows renders every row of table, every column quoted, times as
// fossil's julian day to 17 significant digits, ordered by every column.
func tableRows(t *testing.T, path, table string) []string {
	t.Helper()
	d, err := db.Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer d.Close()
	rows, err := d.Query("SELECT name FROM pragma_table_info('" + table + "') ORDER BY cid")
	if err != nil {
		t.Fatal(err)
	}
	var exprs []string
	for rows.Next() {
		var col string
		if err := rows.Scan(&col); err != nil {
			t.Fatal(err)
		}
		q := `"` + col + `"`
		if strings.HasSuffix(col, "time") {
			q = "printf('%!.17g', " + q + ")"
		}
		exprs = append(exprs, "quote("+q+")")
	}
	if err := rows.Close(); err != nil {
		t.Fatal(err)
	}
	sel := strings.Join(exprs, " || '|' || ")
	var out []string
	rs, err := d.Query("SELECT " + sel + " FROM " + table + " ORDER BY 1")
	if err != nil {
		t.Fatal(err)
	}
	defer rs.Close()
	for rs.Next() {
		var s string
		if err := rs.Scan(&s); err != nil {
			t.Fatal(err)
		}
		out = append(out, s)
	}
	if err := rs.Err(); err != nil {
		t.Fatal(err)
	}
	if len(out) == 0 && table == "ticket" {
		t.Fatalf("%s: no rows; the comparison would be vacuous", table)
	}
	return out
}