Artifact ed212193dda741c2b0247b409b6c8bc539d520964d00ded29c75ef81a5d09dd9:
- File
repo_ticket_fossil_interop_test.go
— part of check-in
[68f2c3d930]
at
2026-10-26 03:14:14
on branch main
— feat: ticket writes, ticket reads and timeline-since
Repo.TicketAdd and Repo.TicketSet write the ticket-change artifact stock `fossil ticket add` / `fossil ticket set` write (D, J incl. +append, K, U, Z), byte for byte, and crosslink it in the same transaction so the ticket, ticketchng and event rows read back at once. TicketOpts.Comment is stored as the stock ticket forms store a remark: icomment with login, username and mimetype.
Repo.TicketList, Repo.TicketChanges and Repo.TimelineSince expose the reads the html-artifact-publisher engine needs, as JSON-ready values. manifest.Timeline gains Kinds and Since to back TimelineSince.
Tracked in #503 and #504. (user: [email protected], size: 4722) [annotate] [blame] [check-ins using] [more...]
package libfossil // Tickets written through TicketAdd/TicketSet against a stock fossil binary // (#503), in the topology the html-artifact-publisher engine runs: this // library's repository serves sync, and fossil clones, pulls and pushes. import ( "net/http/httptest" "net/url" "os/exec" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/testutil" ) type ticketInterop struct { bin, clone, url string repo *Repo } func newTicketInterop(t *testing.T) *ticketInterop { t.Helper() bin := testutil.RequireFossilBin(t) r := newTicketRepo(t) if err := r.CreateUser(UserOpts{Login: "bob", Password: "pw", Caps: "gio"}); err != nil { t.Fatal(err) } srv := httptest.NewServer(r.XferHandler()) t.Cleanup(srv.Close) u, err := url.Parse(srv.URL) if err != nil { t.Fatal(err) } u.User = url.UserPassword("bob", "pw") return &ticketInterop{ bin: bin, clone: filepath.Join(t.TempDir(), "clone.fossil"), url: u.String(), repo: r, } } func (x *ticketInterop) fossil(t *testing.T, args ...string) string { t.Helper() return ticketFossil(t, x.bin, filepath.Dir(x.clone), append(args, "-R", x.clone)...) } // ticketFossil runs the fossil binary in dir and fails the test on error. func ticketFossil(t *testing.T, bin, dir string, args ...string) string { t.Helper() cmd := exec.Command(bin, args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("fossil %s: %v\n%s", strings.Join(args, " "), err, out) } return string(out) } func requireLines(t *testing.T, what, out string, want ...string) { t.Helper() for _, w := range want { if !strings.Contains(out, w) { t.Fatalf("%s lacks %q:\n%s", what, w, out) } } } // A ticket written here and cloned by stock fossil shows in `fossil ticket // show` with the same fields, and in `fossil ticket history` with the same // author and remark. func TestTicketWrittenHereReadsInFossil(t *testing.T) { x := newTicketInterop(t) uuid, artifact, err := x.repo.TicketAdd(TicketOpts{ Fields: map[string]string{"title": "Broken link", "status": "Open", "type": "Code_Defect"}, User: "reader", Comment: "the second link 404s", Time: ticketT0, }) if err != nil { t.Fatal(err) } ticketFossil(t, x.bin, filepath.Dir(x.clone), "clone", x.url, x.clone) show := x.fossil(t, "ticket", "show", "0", "tkt_uuid='"+uuid+"'") requireLines(t, "fossil ticket show", show, uuid, "Broken link", "Open", "Code_Defect") history := x.fossil(t, "ticket", "history", uuid) requireLines(t, "fossil ticket history", history, "Ticket Change by reader on", "Change title: Broken link", "Change status: Open", "Change icomment: the second link 404s", "Change login: reader") if got := strings.TrimSpace(x.fossil(t, "sql", "SELECT count(*) FROM blob WHERE uuid='"+artifact+"'")); got != "1" { t.Fatalf("clone holds artifact %s %s times, want 1", artifact, got) } } // A ticket made by stock fossil and pushed here can be amended with // TicketSet, and the amendment pulled back shows in `fossil ticket history`. func TestFossilTicketAmendedHereShowsInHistory(t *testing.T) { x := newTicketInterop(t) dir := filepath.Dir(x.clone) ticketFossil(t, x.bin, dir, "clone", x.url, x.clone) x.fossil(t, "user", "default", "bob") out := x.fossil(t, "ticket", "add", "title", "From a terminal", "status", "Open") fields := strings.Fields(out) uuid := fields[len(fields)-1] ticketFossil(t, x.bin, dir, "push", x.url, "-R", x.clone) changes, err := x.repo.TicketChanges(uuid) if err != nil { t.Fatalf("pushed ticket is not readable here: %v", err) } if len(changes) != 1 || changes[0].User != "bob" { t.Fatalf("pushed ticket changes = %+v", changes) } artifact, err := x.repo.TicketSet(uuid, TicketOpts{ Fields: map[string]string{"status": "Closed", "resolution": "Fixed"}, User: "editor", Comment: "fixed in the next version", }) if err != nil { t.Fatal(err) } ticketFossil(t, x.bin, dir, "pull", x.url, "-R", x.clone) history := x.fossil(t, "ticket", "history", uuid) requireLines(t, "fossil ticket history", history, "Ticket Change by bob on", "Ticket Change by editor on", "Change status: Closed", "Change resolution: Fixed", "Change icomment: fixed in the next version") if strings.Index(history, "by bob") > strings.Index(history, "by editor") { t.Fatalf("fossil lists the amendment before the ticket:\n%s", history) } show := x.fossil(t, "ticket", "show", "0", "tkt_uuid='"+uuid+"'") requireLines(t, "fossil ticket show", show, "Closed", "Fixed") if got := strings.TrimSpace(x.fossil(t, "sql", "SELECT count(*) FROM blob WHERE uuid='"+artifact+"'")); got != "1" { t.Fatalf("clone holds amendment %s %s times, want 1", artifact, got) } }