Artifact ef35138099fe53954d650e171c69e2165ca524e4867d182221af353ba673f627:
- File
internal/verify/event_fossil_test.go
— part of check-in
[087e1c7ce3]
at
2026-10-18 21:49:37
on branch feat/submanifest-reader
— fix(manifest): derive the event rows fossil derives for tags and attachments
A control artifact's 'g' event comment now matches fossil byte for byte: each target introduced by an Edit link, changes sorted by target hash and full tag name, sym- tags named as tags, a branch move linked to its timeline with its own sym- changes left out, values HTML-escaped, no leading space. tag.InsertControlEvent owns the row, and AddTag writes it, as fossil's tag add does; it was missing until a rebuild or clone.
Attachment comments link the attached artifact and the ticket or tech note as fossil 2.28 does, and a deleted attachment's src is NULL. A check-in without a C card gets a NULL event comment. Hash prefixes follow hash-digits up to 64 digits (was capped at 40); the printf helpers move to fsltype so tag, ticket and attachment comments share them.
Fossil-binary oracles: a fossil-made history with tags, branch moves, wiki, tickets, tech notes, forum posts and attachments rebuilt by both, and a go-written history rebuilt by fossil, must give identical event and attachment tables. (user: [email protected], size: 8440) [annotate] [blame] [check-ins using] [more...]
package verify_test // The event rows fossil derives for every artifact type, not only check-ins: // a repository made by the fossil binary, with tags, branch moves, wiki, // tickets, tech notes, forum posts and attachments, is rebuilt once by // `fossil rebuild` and once by verify.Rebuild, and the event and attachment // tables must come back identical, comment text byte for byte. import ( "crypto/md5" "fmt" "os" "path/filepath" "reflect" "strings" "testing" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/branch" "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/tag" "github.com/danmestas/go-libfossil/internal/verify" "github.com/danmestas/go-libfossil/testutil" ) // fossilEventHistory runs the fossil commands that write a control, wiki, // technote, ticket or attachment artifact, each with the values whose // comment text fossil escapes or links. func fossilEventHistory(t *testing.T, bin, ck string) { t.Helper() path := filepath.Join(ck, "a.txt") steps := [][]string{ {"commit", "-m", "first"}, {"commit", "-m", "on feat", "--branch", "feat", "--bgcolor", "#abc"}, {"tag", "add", "rel-1", "current"}, {"tag", "add", "note", "current", `val "q" <b>`}, {"tag", "add", "--propagate", "prop", "current", "pv"}, {"tag", "cancel", "rel-1", "current"}, {"tag", "add", "--raw", "plain", "current"}, {"tag", "add", "--raw", "--propagate", "rawprop", "current"}, {"tag", "cancel", "--raw", "plain", "current"}, {"amend", "current", "-m", "edited comment"}, {"amend", "current", "--date", "2026-01-02 03:04:05"}, {"amend", "current", "--branchcolor", "#eee"}, {"amend", "current", "--hide"}, {"amend", "current", "--bgcolor", "#a1a", "--close", "--tag", "t'9", "--author", "carol"}, {"tag", "cancel", "--raw", "bgcolor", "current"}, {"update", "trunk"}, {"commit", "-m", "third"}, {"amend", "current", "--branch", "mo ved&/x"}, {"wiki", "create", "Page1", path}, {"wiki", "create", "Tech", path, "--technote", "2026-02-03 04:05:06", "--technote-tags", "t1 t2", "--technote-bgcolor", "#123"}, {"ticket", "add", "title", "tkt one", "status", "Open"}, {"attachment", "add", "Page1", path}, } for i, args := range steps { if args[0] == "commit" { // a new length, as the same second hides an edit if err := os.WriteFile(path, []byte(strings.Repeat("v", i+2)+"\n"), 0o644); err != nil { t.Fatal(err) } } fossilCmd(t, bin, ck, args...) } } // rawArtifact appends the Z card to cards (and a W card for body, when // given), building an artifact the fossil commands cannot write. func rawArtifact(body string, cards ...string) []byte { s := strings.Join(cards, "\n") + "\n" if body != "" { s += fmt.Sprintf("W %d\n%s\n", len(body), body) } return []byte(fmt.Sprintf("%sZ %x\n", s, md5.Sum([]byte(s)))) } // storeRawArtifacts adds what no fossil command writes: a control artifact // that edits two check-ins and moves one to a new branch, forum posts, and // attachments to a ticket, a wiki page, a tech note and an unknown target, // some of them deletions. func storeRawArtifacts(t *testing.T, path string) { t.Helper() r, err := repo.Open(path) if err != nil { t.Fatal(err) } defer func() { if err := r.Close(); err != nil { t.Error(err) } }() one := func(q string) string { var v string if err := r.DB().QueryRow(q).Scan(&v); err != nil { t.Fatalf("%s: %v", q, err) } return v } ciA := one("SELECT min(uuid) FROM blob JOIN event ON objid=rid WHERE type='ci'") ciB := one("SELECT max(uuid) FROM blob JOIN event ON objid=rid WHERE type='ci'") tkt := one("SELECT substr(tagname, 5) FROM tag WHERE tagname GLOB 'tkt-*'") note := one("SELECT substr(tagname, 7) FROM tag WHERE tagname GLOB 'event-*'") src := one("SELECT uuid FROM blob WHERE rid=(SELECT max(rid) FROM blob" + " WHERE rid NOT IN (SELECT objid FROM event))") put := func(art []byte) string { _, uuid, err := blob.Store(r.DB(), art) if err != nil { t.Fatal(err) } return uuid } root := put(rawArtifact("first post", `D 2026-03-01T10:00:00.000`, `H Hello\sforum`, "U alice")) put(rawArtifact("a reply", "D 2026-03-01T11:00:00.000", "G "+root, "I "+root, "U bob")) put(rawArtifact("edited", "D 2026-03-01T12:00:00.000", "G "+root, `H Hello\sagain`, "P "+root, "U alice")) put(rawArtifact("", "D 2026-03-02T10:00:00.000", `T *branch `+ciA+` x\sy&z/w`, `T *sym-x\sy&z/w `+ciA, "T +sym-one "+ciA, "T +sym-one "+ciB+" n<1>", "T -sym-trunk "+ciA, "T -zap "+ciB+" why", "U alice")) put(rawArtifact("", `A t\s<x>.txt `+tkt+" "+src, "D 2026-03-03T10:00:00.000", "U alice")) put(rawArtifact("", `A t\s<x>.txt `+tkt, "D 2026-03-03T11:00:00.000", "U alice")) put(rawArtifact("", "A a-very-long-attachment-name.txt Page1", "D 2026-03-03T12:00:00.000", "U alice")) put(rawArtifact("", "A a-very-long-attachment-name.txt "+note, "D 2026-03-03T13:00:00.000", "U alice")) put(rawArtifact("", "A early.txt "+strings.Repeat("ab", 32)+" "+src, "D 2026-03-03T14:00:00.000", "U alice")) } // eventRows reads every event and attachment column. Times follow textTime. func eventRows(t *testing.T, path string) map[string][]string { t.Helper() return queryRows(t, path, map[string]string{ "event": "SELECT objid, '|', type, '|', " + textTime("mtime") + ", '|', " + textTime("omtime") + ", '|', quote(tagid), '|', quote(uid), '|', quote(bgcolor), '|', quote(euser), '|', quote(user)" + ", '|', quote(ecomment), '|', quote(comment), '|', quote(brief) FROM event ORDER BY objid", "attachment": "SELECT attachid, '|', " + textTime("mtime") + ", '|', quote(src), '|', target" + ", '|', filename, '|', quote(comment), '|', user, '|', isLatest FROM attachment ORDER BY attachid", }) } func TestRebuildMatchesFossilEventRows(t *testing.T) { bin := testutil.RequireFossilBin(t) dir := t.TempDir() orig := filepath.Join(dir, "f.fossil") ck := filepath.Join(dir, "ck") if err := os.Mkdir(ck, 0o755); err != nil { t.Fatal(err) } fossilCmd(t, bin, dir, "init", "-A", "alice", orig) fossilCmd(t, bin, ck, "open", orig) fossilCmd(t, bin, ck, "user", "default", "alice") if err := os.WriteFile(filepath.Join(ck, "a.txt"), []byte("v\n"), 0o644); err != nil { t.Fatal(err) } fossilCmd(t, bin, ck, "add", "a.txt") fossilEventHistory(t, bin, ck) fossilCmd(t, bin, ck, "close", "--force") storeRawArtifacts(t, orig) if err := testutil.FossilRebuild(orig); err != nil { t.Fatal(err) } rebuilt := filepath.Join(dir, "rebuilt.fossil") copyFile(t, orig, rebuilt) r, err := repo.Open(rebuilt) if err != nil { t.Fatal(err) } if _, err := verify.Rebuild(r); err != nil { t.Fatalf("Rebuild: %v", err) } if err := r.Close(); err != nil { t.Fatal(err) } want, got := eventRows(t, orig), eventRows(t, rebuilt) if len(want["event"]) < 30 || len(want["attachment"]) < 6 { t.Fatalf("history too small: %d events, %d attachments", len(want["event"]), len(want["attachment"])) } for table := range want { if !reflect.DeepEqual(got[table], want[table]) { t.Errorf("%s after Rebuild:\n %s\nfossil rebuild wrote:\n %s", table, strings.Join(got[table], "\n "), strings.Join(want[table], "\n ")) } } } // TestFossilRebuildKeepsGoEventRows rebuilds a repository go-libfossil wrote, // tags and a closed branch included, with the fossil binary: every event row // must be the one go-libfossil stored. func TestFossilRebuildKeepsGoEventRows(t *testing.T) { testutil.RequireFossilBin(t) path := goTimesHistory(t) r, err := repo.Open(path) if err != nil { t.Fatal(err) } var tip fsltype.FslID if err := r.DB().QueryRow("SELECT max(objid) FROM event WHERE type='ci'").Scan(&tip); err != nil { t.Fatal(err) } if _, err := tag.AddTag(r, tag.TagOpts{TargetRID: tip, TagName: "note", TagType: tag.TagPropagating, Value: `a "b" <c>`, User: "u"}); err != nil { t.Fatal(err) } if err := branch.Close(r, "feat", "u"); err != nil { t.Fatal(err) } if err := r.Close(); err != nil { t.Fatal(err) } stored := eventRows(t, path) if err := testutil.FossilRebuild(path); err != nil { t.Fatal(err) } rebuilt := eventRows(t, path) if !reflect.DeepEqual(stored["event"], rebuilt["event"]) { t.Errorf("event as go-libfossil stored it:\n %s\nafter fossil rebuild:\n %s", strings.Join(stored["event"], "\n "), strings.Join(rebuilt["event"], "\n ")) } }