| @@ -0,0 +1,120 @@ |
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + | |
| 3 | + | package domain_test |
| 4 | + | |
| 5 | + | import ( |
| 6 | + | "fmt" |
| 7 | + | "testing" |
| 8 | + | |
| 9 | + | "bnema.dev/ermite/internal/domain" |
| 10 | + | "github.com/stretchr/testify/assert" |
| 11 | + | "github.com/stretchr/testify/require" |
| 12 | + | ) |
| 13 | + | |
| 14 | + | func TestWorkflowChangeOf(t *testing.T) { |
| 15 | + | id := func(n int) domain.ObjectID { |
| 16 | + | v, err := domain.ParseObjectID(fmt.Sprintf("%040x", n)) |
| 17 | + | require.NoError(t, err) |
| 18 | + | return v |
| 19 | + | } |
| 20 | + | a, b, c, d := id(1), id(2), id(3), id(4) |
| 21 | + | x, y, z := id(11), id(12), id(13) |
| 22 | + | dir := func(v domain.ObjectID) domain.Subtree { return domain.Subtree{Kind: domain.EntryDirectory, ID: v} } |
| 23 | + | up := func(old, new domain.ObjectID) domain.RefUpdate { |
| 24 | + | o, n := old.String(), new.String() |
| 25 | + | if old.IsZero() { |
| 26 | + | o = domain.ZeroOID |
| 27 | + | } |
| 28 | + | if new.IsZero() { |
| 29 | + | n = domain.ZeroOID |
| 30 | + | } |
| 31 | + | return domain.RefUpdate{Old: o, New: n} |
| 32 | + | } |
| 33 | + | zero := domain.ObjectID{} |
| 34 | + | commit := func(v domain.ObjectID, parents ...domain.ObjectID) domain.PushedCommit { |
| 35 | + | return domain.PushedCommit{ID: v, Parents: parents} |
| 36 | + | } |
| 37 | + | type row struct { |
| 38 | + | name string |
| 39 | + | ups []domain.RefUpdate |
| 40 | + | commits []domain.PushedCommit |
| 41 | + | trees map[string]domain.Subtree |
| 42 | + | treeEnds []domain.ObjectID |
| 43 | + | blobs []domain.ObjectID |
| 44 | + | want domain.WorkflowTreeChange |
| 45 | + | found bool |
| 46 | + | missing bool |
| 47 | + | } |
| 48 | + | change := func(old, new domain.ObjectID) domain.WorkflowTreeChange { |
| 49 | + | return domain.WorkflowTreeChange{Old: old, New: new} |
| 50 | + | } |
| 51 | + | rows := []row{ |
| 52 | + | {name: "no updates"}, |
| 53 | + | {name: "delete", ups: []domain.RefUpdate{up(a, zero)}}, |
| 54 | + | {name: "new blob ignores even a misleading subtree", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y)}, blobs: []domain.ObjectID{b}}, |
| 55 | + | {name: "old blob is not a directory", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y)}, blobs: []domain.ObjectID{a}, want: change(zero, y), found: true}, |
| 56 | + | {name: "new ref at HEAD", ups: []domain.RefUpdate{up(zero, a)}, trees: map[string]domain.Subtree{domain.HeadKey: dir(x), a.String(): dir(x)}}, |
| 57 | + | {name: "new ref same directory as HEAD", ups: []domain.RefUpdate{up(zero, b)}, trees: map[string]domain.Subtree{domain.HeadKey: dir(x), b.String(): dir(x)}}, |
| 58 | + | {name: "new ref old workflows", ups: []domain.RefUpdate{up(zero, b)}, trees: map[string]domain.Subtree{domain.HeadKey: dir(x), b.String(): dir(y)}, want: change(x, y), found: true}, |
| 59 | + | {name: "unborn HEAD", ups: []domain.RefUpdate{up(zero, a)}, trees: map[string]domain.Subtree{a.String(): dir(x)}, want: change(zero, x), found: true}, |
| 60 | + | {name: "HEAD has no workflows", ups: []domain.RefUpdate{up(zero, a)}}, |
| 61 | + | {name: "same workflow directory", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(x)}}, |
| 62 | + | {name: "move back or remove one file from surviving directory", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y)}, want: change(x, y), found: true}, |
| 63 | + | {name: "directory disappears", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x)}}, |
| 64 | + | {name: "lookalike path absent", ups: []domain.RefUpdate{up(a, b)}}, |
| 65 | + | {name: "root with workflows", commits: []domain.PushedCommit{commit(a)}, trees: map[string]domain.Subtree{a.String(): dir(x)}, want: change(zero, x), found: true}, |
| 66 | + | {name: "root without workflows", commits: []domain.PushedCommit{commit(a)}}, |
| 67 | + | {name: "plain pushed commit", commits: []domain.PushedCommit{commit(b, a)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(x)}}, |
| 68 | + | {name: "pushed directory disappears", commits: []domain.PushedCommit{commit(b, a)}, trees: map[string]domain.Subtree{a.String(): dir(x)}}, |
| 69 | + | {name: "merge keeps second parent", commits: []domain.PushedCommit{commit(c, a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(y)}}, |
| 70 | + | {name: "merge differs from all parents", commits: []domain.PushedCommit{commit(c, a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(z)}, want: change(x, z), found: true}, |
| 71 | + | {name: "merge new side changes workflows", commits: []domain.PushedCommit{commit(d, a, c), commit(c, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(z), d.String(): dir(x)}, want: change(y, z), found: true}, |
| 72 | + | {name: "merge adopts other parent refused by ref rule", ups: []domain.RefUpdate{up(a, c)}, commits: []domain.PushedCommit{commit(c, a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(y)}, want: change(x, y), found: true}, |
| 73 | + | {name: "change then revert", ups: []domain.RefUpdate{up(a, c)}, commits: []domain.PushedCommit{commit(c, b), commit(b, a)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(x)}, want: change(y, x), found: true}, |
| 74 | + | {name: "change below plain tip", commits: []domain.PushedCommit{commit(d, c), commit(c, b)}, trees: map[string]domain.Subtree{b.String(): dir(x), c.String(): dir(y), d.String(): dir(y)}, want: change(x, y), found: true}, |
| 75 | + | {name: "all new commits before any ref", ups: []domain.RefUpdate{up(a, b)}, commits: []domain.PushedCommit{commit(d, c)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(y), d.String(): dir(z)}, want: change(y, z), found: true}, |
| 76 | + | {name: "first ref wins", ups: []domain.RefUpdate{up(a, b), up(c, d)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(y), c.String(): dir(y), d.String(): dir(z)}, want: change(x, y), found: true}, |
| 77 | + | {name: "plain ref then changed ref", ups: []domain.RefUpdate{up(a, b), up(c, d)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): dir(x), c.String(): dir(y), d.String(): dir(z)}, want: change(y, z), found: true}, |
| 78 | + | {name: "missing ends fail closed", ups: []domain.RefUpdate{up(a, b)}, missing: true}, |
| 79 | + | } |
| 80 | + | rows = append(rows, |
| 81 | + | row{name: "new tree end with workflows", treeEnds: []domain.ObjectID{a}, ups: []domain.RefUpdate{up(zero, a)}, trees: map[string]domain.Subtree{a.String(): dir(x)}, want: change(zero, x), found: true}, |
| 82 | + | row{name: "new tree end without workflows", treeEnds: []domain.ObjectID{a}, ups: []domain.RefUpdate{up(zero, a)}}, |
| 83 | + | row{name: "new blob ref", ups: []domain.RefUpdate{up(zero, a)}, blobs: []domain.ObjectID{a}}, |
| 84 | + | ) |
| 85 | + | |
| 86 | + | for _, kind := range []domain.EntryKind{domain.EntrySubmodule, domain.EntrySymlink, domain.EntryFile, domain.EntryExecutable} { |
| 87 | + | rows = append(rows, |
| 88 | + | row{name: string(kind) + " to directory same id", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): {Kind: kind, ID: x}, b.String(): dir(x)}, want: change(zero, x), found: true}, |
| 89 | + | row{name: "directory to " + string(kind), ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): dir(x), b.String(): {Kind: kind, ID: x}}}, |
| 90 | + | row{name: "same " + string(kind), ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): {Kind: kind, ID: x}, b.String(): {Kind: kind, ID: x}}}, |
| 91 | + | ) |
| 92 | + | } |
| 93 | + | rows = append(rows, row{name: "gitlink to symlink", ups: []domain.RefUpdate{up(a, b)}, trees: map[string]domain.Subtree{a.String(): {Kind: domain.EntrySubmodule, ID: x}, b.String(): {Kind: domain.EntrySymlink, ID: y}}}) |
| 94 | + | for _, r := range rows { |
| 95 | + | t.Run(r.name, func(t *testing.T) { |
| 96 | + | data := domain.PushSubtrees{Ends: map[string]domain.PushObjectType{}, Commits: r.commits, Subtrees: r.trees} |
| 97 | + | if !r.missing { |
| 98 | + | for _, u := range r.ups { |
| 99 | + | for _, oid := range []string{u.Old, u.New} { |
| 100 | + | data.Ends[oid] = domain.PushObjectCommit |
| 101 | + | } |
| 102 | + | } |
| 103 | + | } |
| 104 | + | for _, tree := range r.treeEnds { |
| 105 | + | data.Ends[tree.String()] = domain.PushObjectTree |
| 106 | + | } |
| 107 | + | for _, b := range r.blobs { |
| 108 | + | data.Ends[b.String()] = domain.PushObjectBlob |
| 109 | + | } |
| 110 | + | got, found, err := domain.WorkflowChangeOf(r.ups, data) |
| 111 | + | if r.missing { |
| 112 | + | require.ErrorIs(t, err, domain.ErrInvalid) |
| 113 | + | return |
| 114 | + | } |
| 115 | + | require.NoError(t, err) |
| 116 | + | assert.Equal(t, r.found, found) |
| 117 | + | assert.Equal(t, r.want, got) |
| 118 | + | }) |
| 119 | + | } |
| 120 | + | } |