Artifact 83bb7fe89a801f7d76a80df819b9ab86b6f97b2f6cf9efe3135661dfc12ae5c4:
- File
internal/manifest/ticket_schema_fossilbin_test.go
— part of check-in
[eb234f62ad]
at
2026-10-18 21:54:08
on branch feat/submanifest-reader
— fix(manifest): link tickets under a schema without the optional columns
ticketInsert assumed ticket.tkt_ctime and ticketchng.tkt_rid/tkt_user, so crosslinking fossil's own repository failed with "table ticketchng has no column named tkt_user". It now probes the schema as fossil's getAllTicketFields does and writes as ticket_insert does: optional columns only where present, a ticketchng row only when the artifact sets one of its fields or the table records the artifact or its user, shared fields the artifact leaves unset copied from the ticket row, and the "baseline for NAME" column set to the artifact that assigns NAME.
A fossil-binary oracle derives three such schemas and requires the ticket and ticketchng rows fossil rebuild writes. (user: [email protected], size: 6064) [annotate] [blame] [check-ins using] [more...]
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 }