|
|
@@ -712,6 +712,11 @@ func TestClearObjectOnlyClearsWhatItSnapshotted(t *testing.T) {
|
|
|
|
// from "the tally wasn't loaded", and the chat panel read that ambiguity the
|
|
|
|
// from "the tally wasn't loaded", and the chat panel read that ambiguity the
|
|
|
|
// wrong way: a successful undo reported "nothing left to undo", which is the
|
|
|
|
// wrong way: a successful undo reported "nothing left to undo", which is the
|
|
|
|
// worst possible thing to say about an action that just worked.
|
|
|
|
// worst possible thing to say about an action that just worked.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// It also pins the cross-layer contract that fix created. countRevisions tallies
|
|
|
|
|
|
|
|
// in Go what ListChangeSets tallies in SQL, and nothing but this test stops the
|
|
|
|
|
|
|
|
// two drifting — so it compares the FULL per-(entity, op) breakdown, not just
|
|
|
|
|
|
|
|
// the totals, which would agree even if the groupings had diverged.
|
|
|
|
func TestRevertResultCarriesItsCounts(t *testing.T) {
|
|
|
|
func TestRevertResultCarriesItsCounts(t *testing.T) {
|
|
|
|
s := newTestService(t, openConfig())
|
|
|
|
s := newTestService(t, openConfig())
|
|
|
|
owner := seedUser(t, s, "[email protected]")
|
|
|
|
owner := seedUser(t, s, "[email protected]")
|
|
|
@@ -723,35 +728,77 @@ func TestRevertResultCarriesItsCounts(t *testing.T) {
|
|
|
|
if _, err := s.FillNamedRegion(ctx, owner, bed.ID, "all", plant.ID, nil); err != nil {
|
|
|
|
if _, err := s.FillNamedRegion(ctx, owner, bed.ID, "all", plant.ID, nil); err != nil {
|
|
|
|
t.Fatalf("fill: %v", err)
|
|
|
|
t.Fatalf("fill: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fill := history(t, s, owner, g.ID)[0]
|
|
|
|
// A second, different kind of change, so the breakdown has more than one row
|
|
|
|
planted := totalOf(fill.Counts)
|
|
|
|
// to get wrong.
|
|
|
|
if planted == 0 {
|
|
|
|
if _, err := s.UpdateObject(ctx, owner, bed.ID, ObjectPatch{Name: strPtr("North Bed")}, bed.Version); err != nil {
|
|
|
|
t.Fatal("the fill recorded no counts")
|
|
|
|
t.Fatalf("rename: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sets := history(t, s, owner, g.ID)
|
|
|
|
|
|
|
|
rename, fill := sets[0], sets[1]
|
|
|
|
|
|
|
|
|
|
|
|
undo, conflicts, err := s.RevertChangeSet(ctx, owner, fill.ID, domain.SourceUI)
|
|
|
|
undoRename, conflicts, err := s.RevertChangeSet(ctx, owner, rename.ID, domain.SourceUI)
|
|
|
|
if err != nil || len(conflicts) != 0 {
|
|
|
|
if err != nil || len(conflicts) != 0 {
|
|
|
|
t.Fatalf("revert: err=%v conflicts=%+v", err, conflicts)
|
|
|
|
t.Fatalf("revert rename: err=%v conflicts=%+v", err, conflicts)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if undo == nil {
|
|
|
|
undoFill, conflicts, err := s.RevertChangeSet(ctx, owner, fill.ID, domain.SourceUI)
|
|
|
|
t.Fatal("a revert that did work returned no change set")
|
|
|
|
if err != nil || len(conflicts) != 0 {
|
|
|
|
}
|
|
|
|
t.Fatalf("revert fill: err=%v conflicts=%+v", err, conflicts)
|
|
|
|
if got := totalOf(undo.Counts); got != planted {
|
|
|
|
|
|
|
|
t.Errorf("revert reported %d changes, want %d — an empty tally reads as 'nothing happened'", got, planted)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// And the list read agrees with what the revert returned, so both halves of
|
|
|
|
for _, undo := range []*domain.ChangeSet{undoRename, undoFill} {
|
|
|
|
// the API describe the same change set the same way.
|
|
|
|
if undo == nil {
|
|
|
|
listed := history(t, s, owner, g.ID)[0]
|
|
|
|
t.Fatal("a revert that did work returned no change set")
|
|
|
|
if listed.ID != undo.ID {
|
|
|
|
}
|
|
|
|
t.Fatalf("newest change set is %d, want the revert %d", listed.ID, undo.ID)
|
|
|
|
if len(undo.Counts) == 0 {
|
|
|
|
|
|
|
|
t.Fatalf("change set %d came back with no tally — an empty tally reads as 'nothing happened'", undo.ID)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if totalOf(listed.Counts) != totalOf(undo.Counts) {
|
|
|
|
// Undoing a fill deletes every plop it created, so the tallies must match.
|
|
|
|
t.Errorf("list says %d changes, revert said %d", totalOf(listed.Counts), totalOf(undo.Counts))
|
|
|
|
if got, want := countsTotal(undoFill.Counts), countsTotal(fill.Counts); got != want {
|
|
|
|
|
|
|
|
t.Errorf("undo of the fill reports %d changes, want %d", got, want)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The SQL tally and the Go tally must agree, row for row.
|
|
|
|
|
|
|
|
listed := history(t, s, owner, g.ID)
|
|
|
|
|
|
|
|
byID := map[int64][]domain.ChangeCount{}
|
|
|
|
|
|
|
|
for _, cs := range listed {
|
|
|
|
|
|
|
|
byID[cs.ID] = cs.Counts
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, undo := range []*domain.ChangeSet{undoRename, undoFill} {
|
|
|
|
|
|
|
|
fromSQL, ok := byID[undo.ID]
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
|
|
t.Fatalf("revert %d is missing from the history list", undo.ID)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if !sameCounts(undo.Counts, fromSQL) {
|
|
|
|
|
|
|
|
t.Errorf("change set %d: revert returned %+v, the list read says %+v — countRevisions and the SQL grouping have drifted",
|
|
|
|
|
|
|
|
undo.ID, undo.Counts, fromSQL)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func totalOf(counts []domain.ChangeCount) int {
|
|
|
|
// sameCounts compares two tallies regardless of order.
|
|
|
|
|
|
|
|
func sameCounts(a, b []domain.ChangeCount) bool {
|
|
|
|
|
|
|
|
if len(a) != len(b) {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
index := func(cs []domain.ChangeCount) map[string]int {
|
|
|
|
|
|
|
|
m := map[string]int{}
|
|
|
|
|
|
|
|
for _, c := range cs {
|
|
|
|
|
|
|
|
m[c.EntityType+"/"+c.Op] = c.N
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return m
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
ia, ib := index(a), index(b)
|
|
|
|
|
|
|
|
for k, v := range ia {
|
|
|
|
|
|
|
|
if ib[k] != v {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// countsTotal sums a tally — the server-side twin of the client's totalChanges.
|
|
|
|
|
|
|
|
func countsTotal(counts []domain.ChangeCount) int {
|
|
|
|
n := 0
|
|
|
|
n := 0
|
|
|
|
for _, c := range counts {
|
|
|
|
for _, c := range counts {
|
|
|
|
n += c.N
|
|
|
|
n += c.N
|
|
|
|