Artifact 27180ab5600388221ae5ac19c4ad3b560ec992c6f03351ab78a5803d06041376:
- File
internal/tag/tag_test.go
— part of check-in
[7043d6e3a9]
at
2026-07-28 00:18:35
on branch trunk
— fix: stop tag propagation from crossing branch boundaries (#198) (#206)
Branch tag propagation left 1,155 of 20,859 check-ins on a clone of the fossil repository carrying a different branch value than canonical, and tagxref at 68,883 rows against canonical's 51,764. Two independent defects in the port, both confirmed against fossil's own source and a canonical rebuild of the same blobs.
tag_insert is mtime-guarded and our tag application was not. Canonical opens it with
SELECT 1 FROM tagxref WHERE tagid=%d AND rid=%d AND mtime>=:mtime
and returns without touching tagxref -- and without calling tag_propagate -- when it matches (src/tag.c:173-186). AddTag, ApplyTag and ApplyTagWithTx all wrote unconditionally, so a check-in's own inline T-cards clobbered a later control artifact that had retagged the same check-in, then re-propagated the stale value to every descendant. On the real corpus one branch rename did this 19,115 times: check-in ab0a0d76 declares branch dual-license, three control artifacts later rename it to clear-title, and we reinstated dual-license and flooded the repository with it. The guard makes the outcome independent of the order the sweep visits the two artifacts in.
repairTagPropagation seeded from the wrong node. tag_propagate's per-child test is strict --
coalesce(srcid=0 AND tagxref.mtime<:mtime, 1) AS doit
-- so a descendant that already carries the tag holds it at exactly the origin's mtime, doit is 0, and the walk neither retags that child nor queues it. Replaying from tag origins therefore stopped dead at the first already-tagged descendant, stranding any check-in linked by a later sweep. Canonical seeds from the primary parent instead, right after inserting a new check-in's plink edges (src/manifest.c:2300-2302 and 2467-2469), which only ever tests that parent's own children. Seed from every primary parent, which is the sweep-order-independent equivalent. Worth 693 missing propagated branch rows.
Measured on a clone of the fossil repository against a canonical rebuild of the same blobs:
divergent branch values 1,155 -> 1 (of 20,858 compared) tagxref rows 68,883 -> 51,132 (canonical 51,764) leaf rows 1,463 -> 1,425 (canonical 1,426)
The residual 632-row tagxref gap is 613 missing `closed` singletons plus small change; it measures identically before and after this change and is a separate defect. The one remaining divergent branch value is a NULL-vs-empty-string difference in cancel-tag values, also pre-existing.
repairTagPropagation still runs before repairLeafTable, as #197 requires. (user: [email protected], size: 21193) [annotate] [blame] [check-ins using]
package tag_test import ( "path/filepath" "testing" "time" "github.com/danmestas/go-libfossil/db" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/tag" _ "github.com/danmestas/go-libfossil/internal/testdriver" "github.com/danmestas/go-libfossil/simio" ) func setupTestRepo(t *testing.T) *repo.Repo { t.Helper() path := filepath.Join(t.TempDir(), "test.fossil") r, err := repo.Create(path, "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("repo.Create: %v", err) } t.Cleanup(func() { r.Close() }) return r } func TestAddTag(t *testing.T) { r := setupTestRepo(t) // Create a checkin to tag rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "hello.txt", Content: []byte("hello")}}, Comment: "initial commit", User: "testuser", Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("Checkin: %v", err) } // Add a singleton tag tagRid, err := tag.AddTag(r, tag.TagOpts{ TargetRID: rid, TagName: "testlabel", TagType: tag.TagSingleton, Value: "myvalue", User: "testuser", Time: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("tag.AddTag: %v", err) } if tagRid <= 0 { t.Fatalf("tagRid = %d, want > 0", tagRid) } // Verify tagxref has the correct entry var tagtype int var value string err = r.DB().QueryRow( `SELECT tagtype, value FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = ? AND tagxref.rid = ?`, "testlabel", rid, ).Scan(&tagtype, &value) if err != nil { t.Fatalf("tagxref query: %v", err) } if tagtype != tag.TagSingleton { t.Fatalf("tagtype = %d, want %d (singleton)", tagtype, tag.TagSingleton) } if value != "myvalue" { t.Fatalf("value = %q, want %q", value, "myvalue") } } func TestCancelTag(t *testing.T) { r := setupTestRepo(t) // Create a checkin (auto-gets sym-trunk tag via propagation in manifest) rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "hello.txt", Content: []byte("hello")}}, Comment: "initial commit", User: "testuser", Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("Checkin: %v", err) } // Cancel the sym-trunk tag cancelRid, err := tag.AddTag(r, tag.TagOpts{ TargetRID: rid, TagName: "sym-trunk", TagType: tag.TagCancel, User: "testuser", Time: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("tag.AddTag cancel: %v", err) } if cancelRid <= 0 { t.Fatalf("cancelRid = %d, want > 0", cancelRid) } // Verify tagxref has tagtype=0 (cancel) var tagtype int err = r.DB().QueryRow( `SELECT tagtype FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = ? AND tagxref.rid = ?`, "sym-trunk", rid, ).Scan(&tagtype) if err != nil { t.Fatalf("tagxref query: %v", err) } if tagtype != tag.TagCancel { t.Fatalf("tagtype = %d, want %d (cancel)", tagtype, tag.TagCancel) } } // makeCheckin is a test helper that creates a checkin with one file. func makeCheckin(t *testing.T, r *repo.Repo, parent int64, name, content, comment string) int64 { t.Helper() return makeCheckinAt(t, r, parent, name, content, comment, time.Now().UTC()) } // makeCheckinAt is makeCheckin with an explicit check-in time. A check-in // declaring a branch writes its own branch/sym-trunk tagxref rows stamped with // that time, and tag application is mtime-guarded (see superseded), so a test // that later applies one of those tags by hand has to place the check-in // before the tag rather than leaving it at time.Now(). func makeCheckinAt(t *testing.T, r *repo.Repo, parent int64, name, content, comment string, when time.Time) int64 { t.Helper() rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: name, Content: []byte(content)}}, Comment: comment, User: "testuser", Parent: libfossil.FslID(parent), Time: when, }) if err != nil { t.Fatalf("Checkin: %v", err) } return int64(rid) } func TestPropagateChain(t *testing.T) { r := setupTestRepo(t) // Create chain A→B→C ridA := makeCheckin(t, r, 0, "a.txt", "content A", "commit A") ridB := makeCheckin(t, r, ridA, "b.txt", "content B", "commit B") ridC := makeCheckin(t, r, ridB, "c.txt", "content C", "commit C") // Add propagating "branch" tag to A with value "feature" _, err := tag.AddTag(r, tag.TagOpts{ TargetRID: libfossil.FslID(ridA), TagName: "branch", TagType: tag.TagPropagating, Value: "feature", User: "testuser", Time: time.Now().UTC(), }) if err != nil { t.Fatalf("tag.AddTag: %v", err) } // Verify B has the propagated tag (srcid=0, correct value) var srcidb, tagtypeB int var valueB string err = r.DB().QueryRow(` SELECT srcid, tagtype, value FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? `, ridB).Scan(&srcidb, &tagtypeB, &valueB) if err != nil { t.Fatalf("tagxref query for B: %v", err) } if srcidb != 0 { t.Errorf("B srcid = %d, want 0 (propagated)", srcidb) } if tagtypeB != tag.TagPropagating { t.Errorf("B tagtype = %d, want %d", tagtypeB, tag.TagPropagating) } if valueB != "feature" { t.Errorf("B value = %q, want %q", valueB, "feature") } // Verify C has the propagated tag var srcidC, tagtypeC int var valueC string err = r.DB().QueryRow(` SELECT srcid, tagtype, value FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? `, ridC).Scan(&srcidC, &tagtypeC, &valueC) if err != nil { t.Fatalf("tagxref query for C: %v", err) } if srcidC != 0 { t.Errorf("C srcid = %d, want 0 (propagated)", srcidC) } if tagtypeC != tag.TagPropagating { t.Errorf("C tagtype = %d, want %d", tagtypeC, tag.TagPropagating) } if valueC != "feature" { t.Errorf("C value = %q, want %q", valueC, "feature") } } func TestCancelPropagation(t *testing.T) { r := setupTestRepo(t) // Create chain A→B→C ridA := makeCheckin(t, r, 0, "a.txt", "content A", "commit A") ridB := makeCheckin(t, r, ridA, "b.txt", "content B", "commit B") ridC := makeCheckin(t, r, ridB, "c.txt", "content C", "commit C") // Add propagating tag to A _, err := tag.AddTag(r, tag.TagOpts{ TargetRID: libfossil.FslID(ridA), TagName: "testprop", TagType: tag.TagPropagating, Value: "propvalue", User: "testuser", Time: time.Now().UTC(), }) if err != nil { t.Fatalf("tag.AddTag propagating: %v", err) } // Cancel at B _, err = tag.AddTag(r, tag.TagOpts{ TargetRID: libfossil.FslID(ridB), TagName: "testprop", TagType: tag.TagCancel, User: "testuser", Time: time.Now().UTC(), }) if err != nil { t.Fatalf("tag.AddTag cancel: %v", err) } // Verify B has no active tags (count of tagtype>0 should be 0) var countB int err = r.DB().QueryRow(` SELECT COUNT(*) FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'testprop' AND tagxref.rid = ? AND tagxref.tagtype > 0 `, ridB).Scan(&countB) if err != nil { t.Fatalf("count query for B: %v", err) } if countB != 0 { t.Errorf("B has %d active tags, want 0", countB) } // Verify C has no tagxref row for this tag at all var countC int err = r.DB().QueryRow(` SELECT COUNT(*) FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'testprop' AND tagxref.rid = ? `, ridC).Scan(&countC) if err != nil { t.Fatalf("count query for C: %v", err) } if countC != 0 { t.Errorf("C has %d tagxref rows, want 0", countC) } } func TestPropagateBgcolor(t *testing.T) { r := setupTestRepo(t) // Create A→B ridA := makeCheckin(t, r, 0, "a.txt", "content A", "commit A") ridB := makeCheckin(t, r, ridA, "b.txt", "content B", "commit B") // Run crosslink to populate event table _, err := manifest.Crosslink(r) if err != nil { t.Fatalf("Crosslink: %v", err) } // Add propagating "bgcolor" tag to A _, err = tag.AddTag(r, tag.TagOpts{ TargetRID: libfossil.FslID(ridA), TagName: "bgcolor", TagType: tag.TagPropagating, Value: "#ff0000", User: "testuser", Time: time.Now().UTC(), }) if err != nil { t.Fatalf("tag.AddTag bgcolor: %v", err) } // Verify event.bgcolor updated at B var bgcolor string err = r.DB().QueryRow("SELECT bgcolor FROM event WHERE objid=?", ridB).Scan(&bgcolor) if err != nil { t.Fatalf("event query for B: %v", err) } if bgcolor != "#ff0000" { t.Errorf("B bgcolor = %q, want %q", bgcolor, "#ff0000") } } func TestApplyTag(t *testing.T) { r := setupTestRepo(t) // The check-ins predate the tag applied below, so its mtime is the newest // one standing for (sym-trunk, A) and the application is not superseded. checkedInAt := time.Date(2024, 1, 15, 9, 0, 0, 0, time.UTC) ridA := makeCheckinAt(t, r, 0, "a.txt", "aaa", "commit A", checkedInAt) ridB := makeCheckinAt(t, r, ridA, "a.txt", "bbb", "commit B", checkedInAt.Add(time.Minute)) err := tag.ApplyTag(r, tag.ApplyOpts{ TargetRID: libfossil.FslID(ridA), SrcRID: 999, TagName: "sym-trunk", TagType: tag.TagPropagating, Value: "", MTime: libfossil.TimeToJulian(time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)), }) if err != nil { t.Fatalf("tag.ApplyTag: %v", err) } // Verify tagxref at A has srcid=999. var srcid int64 r.DB().QueryRow( "SELECT srcid FROM tagxref JOIN tag USING(tagid) WHERE tagname='sym-trunk' AND rid=?", ridA, ).Scan(&srcid) if srcid != 999 { t.Errorf("A srcid=%d, want 999", srcid) } // Verify propagated to B with srcid=0. r.DB().QueryRow( "SELECT srcid FROM tagxref JOIN tag USING(tagid) WHERE tagname='sym-trunk' AND rid=?", ridB, ).Scan(&srcid) if srcid != 0 { t.Errorf("B srcid=%d, want 0 (propagated)", srcid) } } // TestPropagateAllOrderingAdjacentMillisecond exercises the `tagxref.mtime < ?` // ordering comparison in propagate() — the one confirmed ScanJulianDay consumer // that makes an ordering decision — driven through PropagateAll, where the // bound `?` value is produced by db.ScanJulianDay from the origin row's mtime. // // Goal: prove the "overwrite a child's propagated tag only if it is strictly // older" decision survives a scanned mtime that is only one millisecond away // from the child's stored mtime. Under the ncruces driver the origin mtime is // scanned from a time.Time carrying sub-millisecond conversion noise; if that // noise collapsed the one-millisecond gap (as a prior truncation bug did), the // strict `<` would flip and the overwrite would silently not happen. // // Methodology: A is the ancestor of two children, B and C. B carries an older // propagated tag (base), C a newer one (base+2ms), and A's own tag sits between // them at base+1ms. Re-propagating from A must overwrite B (base < base+1ms) and // leave C untouched (base+2ms is not < base+1ms). Adjacent-millisecond deltas // are the point: a one-millisecond truncation is exactly what flips the B // decision, and only sub-10ms intervals can express that — timestamps hours // apart pass regardless of the bug. This test runs under whichever driver the // testdriver import selects, so `-tags test_ncruces` runs it where the noise // lives. func TestPropagateAllOrderingAdjacentMillisecond(t *testing.T) { r := setupTestRepo(t) // A is the common ancestor; B and C are both primary children of A. They // predate the tag mtimes below so the origin application at A is not // superseded by the branch row the check-in itself wrote. checkedInAt := time.Date(2024, 1, 15, 9, 0, 0, 0, time.UTC) ridA := makeCheckinAt(t, r, 0, "a.txt", "content A", "commit A", checkedInAt) ridB := makeCheckinAt(t, r, ridA, "b.txt", "content B", "commit B", checkedInAt.Add(time.Minute)) ridC := makeCheckinAt(t, r, ridA, "c.txt", "content C", "commit C", checkedInAt.Add(2*time.Minute)) base := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) mtimeChildOld := libfossil.TimeToJulian(base) // B: older than A mtimeOrigin := libfossil.TimeToJulian(base.Add(1 * time.Millisecond)) // A: one ms newer than B mtimeChildNew := libfossil.TimeToJulian(base.Add(2 * time.Millisecond)) // C: newer than A // Apply the propagating tag at A. This creates the tag and, via propagate, // seeds srcid=0 tagxref rows on B and C that we overwrite below to set exact // mtimes for the comparison. if err := tag.ApplyTag(r, tag.ApplyOpts{ TargetRID: libfossil.FslID(ridA), SrcRID: libfossil.FslID(ridA), // non-zero: an origin, not a propagated row TagName: "branch", TagType: tag.TagPropagating, Value: "new", MTime: mtimeOrigin, }); err != nil { t.Fatalf("tag.ApplyTag: %v", err) } // Pin the children's existing propagated tags to exact mtimes a single // millisecond on either side of the origin. setChildTag := func(rid int64, value string, mtime float64) { t.Helper() if _, err := r.DB().Exec( `UPDATE tagxref SET value=?, mtime=?, srcid=0 WHERE tagid=(SELECT tagid FROM tag WHERE tagname='branch') AND rid=?`, value, mtime, rid, ); err != nil { t.Fatalf("pin child tag for rid=%d: %v", rid, err) } } setChildTag(ridB, "old", mtimeChildOld) setChildTag(ridC, "newer", mtimeChildNew) // Re-propagate from A. The `?` in `tagxref.mtime < ?` now comes from // ScanJulianDay(A.mtime). if err := tag.PropagateAll(r.DB(), libfossil.FslID(ridA)); err != nil { t.Fatalf("tag.PropagateAll: %v", err) } readChildValue := func(rid int64) string { t.Helper() var value string if err := r.DB().QueryRow( `SELECT value FROM tagxref WHERE tagid=(SELECT tagid FROM tag WHERE tagname='branch') AND rid=?`, rid, ).Scan(&value); err != nil { t.Fatalf("read child tag for rid=%d: %v", rid, err) } return value } // B was strictly older than the origin (base < base+1ms) → overwritten. if got := readChildValue(ridB); got != "new" { t.Fatalf("B tag value = %q, want %q: base < origin (one ms) did not overwrite — a lost millisecond collapsed the strict ordering", got, "new") } // C was newer than the origin (base+2ms not < base+1ms) → preserved. This // negative control confirms the comparison actually gates rather than always // overwriting. if got := readChildValue(ridC); got != "newer" { t.Fatalf("C tag value = %q, want %q: newer child was overwritten — the `<` guard did not hold", got, "newer") } } func TestPropagateAll(t *testing.T) { r := setupTestRepo(t) // Create linear chain A→B→C ridA := makeCheckin(t, r, 0, "a.txt", "content A", "commit A") ridB := makeCheckin(t, r, ridA, "b.txt", "content B", "commit B") ridC := makeCheckin(t, r, ridB, "c.txt", "content C", "commit C") // Add propagating "branch" tag to A with value "feature" _, err := tag.AddTag(r, tag.TagOpts{ TargetRID: libfossil.FslID(ridA), TagName: "branch", TagType: tag.TagPropagating, Value: "feature", User: "testuser", Time: time.Now().UTC(), }) if err != nil { t.Fatalf("tag.AddTag: %v", err) } // Verify B and C have propagated tags initially var countB, countC int r.DB().QueryRow(` SELECT COUNT(*) FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? AND srcid = 0 `, ridB).Scan(&countB) r.DB().QueryRow(` SELECT COUNT(*) FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? AND srcid = 0 `, ridC).Scan(&countC) if countB != 1 || countC != 1 { t.Fatalf("initial setup check: B has %d tags, C has %d tags, want both = 1", countB, countC) } // Clear propagated tags from B and C to simulate incomplete propagation if _, err := r.DB().Exec("DELETE FROM tagxref WHERE rid=? AND srcid=0", ridB); err != nil { t.Fatalf("clear B tags: %v", err) } if _, err := r.DB().Exec("DELETE FROM tagxref WHERE rid=? AND srcid=0", ridC); err != nil { t.Fatalf("clear C tags: %v", err) } // Verify cleared r.DB().QueryRow(` SELECT COUNT(*) FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? AND srcid = 0 `, ridB).Scan(&countB) r.DB().QueryRow(` SELECT COUNT(*) FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? AND srcid = 0 `, ridC).Scan(&countC) if countB != 0 || countC != 0 { t.Fatalf("after clear: B has %d tags, C has %d tags, want both = 0", countB, countC) } // Call PropagateAll on A to re-propagate if err := tag.PropagateAll(r.DB(), libfossil.FslID(ridA)); err != nil { t.Fatalf("tag.PropagateAll: %v", err) } // Verify B has propagated branch=feature tag (srcid=0) var srcidB, tagtypeB int var valueB string err = r.DB().QueryRow(` SELECT srcid, tagtype, value FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? `, ridB).Scan(&srcidB, &tagtypeB, &valueB) if err != nil { t.Fatalf("tagxref query for B: %v", err) } if srcidB != 0 { t.Errorf("B srcid = %d, want 0 (propagated)", srcidB) } if tagtypeB != tag.TagPropagating { t.Errorf("B tagtype = %d, want %d", tagtypeB, tag.TagPropagating) } if valueB != "feature" { t.Errorf("B value = %q, want %q", valueB, "feature") } // Verify C has propagated branch=feature tag (srcid=0) var srcidC, tagtypeC int var valueC string err = r.DB().QueryRow(` SELECT srcid, tagtype, value FROM tagxref JOIN tag ON tag.tagid = tagxref.tagid WHERE tag.tagname = 'branch' AND tagxref.rid = ? `, ridC).Scan(&srcidC, &tagtypeC, &valueC) if err != nil { t.Fatalf("tagxref query for C: %v", err) } if srcidC != 0 { t.Errorf("C srcid = %d, want 0 (propagated)", srcidC) } if tagtypeC != tag.TagPropagating { t.Errorf("C tagtype = %d, want %d", tagtypeC, tag.TagPropagating) } if valueC != "feature" { t.Errorf("C value = %q, want %q", valueC, "feature") } } // TestApplyTagIgnoresOlderTag pins the guard canonical fossil opens tag_insert // with (src/tag.c:173-186): if tagxref already holds a row for (tagid, rid) // whose mtime is >= the incoming one, the application is dropped whole -- no // tagxref write and no propagation. // // SELECT 1 FROM tagxref WHERE tagid=%d AND rid=%d AND mtime>=:mtime // ... // if( rc==SQLITE_ROW ){ // /* Another entry that is more recent already exists. Do nothing */ // return tagid; // } // // It is what makes the result independent of the order artifacts are applied // in, which crosslink relies on: a check-in's inline T-cards and a control // artifact that later retagged the same check-in can be crosslinked in either // order, and the newer one has to win both times. Without the guard the older // application overwrites the newer row and then propagates the stale value to // every descendant (issue #198). func TestApplyTagIgnoresOlderTag(t *testing.T) { r := setupTestRepo(t) base := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) ridA, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "f.txt", Content: []byte("a")}}, Comment: "A", User: "testuser", Time: base, }) if err != nil { t.Fatalf("Checkin A: %v", err) } ridB, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "f.txt", Content: []byte("b")}}, Comment: "B", User: "testuser", Time: base.Add(time.Minute), Parent: ridA, }) if err != nil { t.Fatalf("Checkin B: %v", err) } newer := libfossil.TimeToJulian(base.Add(48 * time.Hour)) older := libfossil.TimeToJulian(base.Add(24 * time.Hour)) // The newer application lands and propagates to B. if err := tag.ApplyTag(r, tag.ApplyOpts{ TargetRID: ridA, SrcRID: ridA, TagName: "branch", TagType: tag.TagPropagating, Value: "clear-title", MTime: newer, }); err != nil { t.Fatalf("ApplyTag newer: %v", err) } // The older one must be dropped whole. if err := tag.ApplyTag(r, tag.ApplyOpts{ TargetRID: ridA, SrcRID: ridA, TagName: "branch", TagType: tag.TagPropagating, Value: "dual-license", MTime: older, }); err != nil { t.Fatalf("ApplyTag older: %v", err) } for _, tc := range []struct { name string rid libfossil.FslID }{{"A", ridA}, {"B", ridB}} { var value string // mtime comes back with a driver-dependent dynamic type, so it goes // through ScanJulianDay the way every other consumer does. var mtimeRaw any if err := r.DB().QueryRow(` SELECT value, mtime FROM tagxref JOIN tag USING(tagid) WHERE tag.tagname = 'branch' AND tagxref.rid = ? `, tc.rid).Scan(&value, &mtimeRaw); err != nil { t.Fatalf("tagxref query for %s: %v", tc.name, err) } mtime, ok := db.ScanJulianDay(mtimeRaw) if !ok { t.Fatalf("ScanJulianDay for %s: cannot read %T", tc.name, mtimeRaw) } if value != "clear-title" { t.Errorf("%s value = %q, want %q (older application must not overwrite)", tc.name, value, "clear-title") } if mtime != newer { t.Errorf("%s mtime = %v, want %v", tc.name, mtime, newer) } } }