package web
import (
"bytes"
"html/template"
"strings"
"testing"
)
// meldFixture is the mockup's shape: the target changes a line (merged),
// both sides add at one place (a conflict with no ancestor lines), the
// branch adds a block (merged), and the two rewrite one line apiece (a
// conflict over an ancestor line), with a long unchanged run between.
func meldFixture() conflictsReport {
base := []string{"type T struct {", "\tBehind int", "\tMerge *run", "}", "", "func page() {", "\t// no note", "}"}
var tail []string
for i := range 20 {
tail = append(tail, "func h"+string(rune('a'+i))+"() {}")
}
base = append(base, tail...)
ours := []string{"type T struct {", "\tBehind int", "\tHunks []file", "\tMerge *run", "}", "", "type file struct{}", "", "func page() {", "\tdet.Hunks = x()", "}"}
ours = append(ours, tail...)
theirs := []string{"type T struct {", "\tBehind int // seen", "\tNote string", "\tMerge *run", "}", "", "func page() {", "\tdet.Note = y()", "}"}
theirs = append(theirs, tail...)
var rep conflictsReport
rep.Branch, rep.Target, rep.BranchTip, rep.TargetTip, rep.Verdict = "feat", "trunk", "b1b1b1b1b1b1b1", "c2c2c2c2c2c2c2", "conflict"
f := struct {
Path string `json:"path"`
Kind string `json:"kind"`
Omitted string `json:"omitted"`
Ours struct {
Lines []string `json:"lines"`
} `json:"ours"`
Theirs struct {
Lines []string `json:"lines"`
} `json:"theirs"`
Base struct {
Lines []string `json:"lines"`
} `json:"base"`
Regions []struct {
Kind string `json:"kind"`
Base reportSpan `json:"base"`
Ours reportSpan `json:"ours"`
Theirs reportSpan `json:"theirs"`
} `json:"regions"`
}{Path: "web.go", Kind: "text"}
f.Ours.Lines, f.Theirs.Lines, f.Base.Lines = ours, theirs, base
add := func(kind string, b, o, t [2]int) {
f.Regions = append(f.Regions, struct {
Kind string `json:"kind"`
Base reportSpan `json:"base"`
Ours reportSpan `json:"ours"`
Theirs reportSpan `json:"theirs"`
}{kind, reportSpan{b[0], b[1]}, reportSpan{o[0], o[1]}, reportSpan{t[0], t[1]}})
}
add("same", [2]int{0, 1}, [2]int{0, 1}, [2]int{0, 1})
add("theirs", [2]int{1, 2}, [2]int{1, 2}, [2]int{1, 2})
add("conflict", [2]int{2, 2}, [2]int{2, 3}, [2]int{2, 3})
add("same", [2]int{2, 5}, [2]int{3, 6}, [2]int{3, 6})
add("ours", [2]int{5, 5}, [2]int{6, 8}, [2]int{6, 6})
add("same", [2]int{5, 6}, [2]int{8, 9}, [2]int{6, 7})
add("conflict", [2]int{6, 7}, [2]int{9, 10}, [2]int{7, 8})
add("same", [2]int{7, 28}, [2]int{10, 31}, [2]int{8, 29})
rep.Files = append(rep.Files, f)
return rep
}
// The result takes each one-sided change, shows the ancestor's lines at
// an open conflict, and marks a side wherever it differs from the
// result: red at a conflict, blue for a change, green where one of the
// two has no lines.
func TestMeldLaysOutTheMerge(t *testing.T) {
v := buildMeld(meldFixture(), "/r/alpha")
f := v.Files[0]
if f.Conflicts != 2 || f.FromOurs != 1 || f.FromTheirs != 1 {
t.Fatalf("FAIL: %d conflicts, %d from ours, %d from theirs", f.Conflicts, f.FromOurs, f.FromTheirs)
}
text := func(ls []meldLine) string {
var b strings.Builder
for _, l := range ls {
b.WriteString(string(l.HTML))
b.WriteString("|")
}
return b.String()
}
var kinds []string
for _, s := range f.Segs {
kinds = append(kinds, s.LCls+"/"+s.MCls+"/"+s.RCls)
}
want := []string{"//", "chg/chg/", "cf/cf/cf", "//", "/ins/ins gap", "//", "cf/cf/cf", "//", "//"}
if strings.Join(kinds, " ") != strings.Join(want, " ") {
t.Errorf("FAIL: classes %q, want %q", kinds, want)
}
// The target's change is in the result; the branch's added block too.
if !strings.Contains(text(f.Segs[1].M), "seen") || !strings.Contains(text(f.Segs[4].M), "file") {
t.Errorf("FAIL: a one-sided change is not merged: %q %q", text(f.Segs[1].M), text(f.Segs[4].M))
}
// Both added: no ancestor lines, but a label, so it is not a gap.
if c := f.Segs[2]; len(c.M) != 0 || c.Tag == "" || strings.Contains(c.MCls, "gap") {
t.Errorf("FAIL: an open conflict with no ancestor lines: %+v", c)
}
// The ancestor's line stands in the result at the second conflict.
if !strings.Contains(text(f.Segs[6].M), "no note") {
t.Errorf("FAIL: the open conflict does not show the ancestor: %q", text(f.Segs[6].M))
}
// The run to the end of the file keeps six lines after the last
// change and folds the rest; each pane numbers its own lines.
last := f.Segs[len(f.Segs)-2:]
if len(last[0].L) != meldContext || last[0].Fold != 0 || last[1].Fold != 21-meldContext {
t.Errorf("FAIL: the trailing run kept %d lines and folded %d", len(last[0].L), last[1].Fold)
}
if n := last[0].L[0].N; n != 11 {
t.Errorf("FAIL: ours numbers its own lines: %d, want 11", n)
}
if n := last[0].R[0].N; n != 9 {
t.Errorf("FAIL: theirs numbers its own lines: %d, want 9", n)
}
if n := last[0].M[0].N; n != 10 {
t.Errorf("FAIL: the result numbers as it goes: %d, want 10", n)
}
// Ours is the branch, linked at its tip; theirs the target.
if !strings.Contains(f.OursURL, "/file/b1b1b1b1b1b1b1/web.go") || !strings.Contains(f.TheirsURL, "/file/c2c2c2c2c2c2c2/web.go") {
t.Errorf("FAIL: links %s %s", f.OursURL, f.TheirsURL)
}
}
// A phone gets each conflict with its surroundings in each version,
// stopping at the neighbouring conflict, and More context only where
// there is more to show.
func TestMeldPhoneContextStopsAtTheNextConflict(t *testing.T) {
f := buildMeld(meldFixture(), "/r/alpha").Files[0]
if len(f.Phone) != 2 {
t.Fatalf("FAIL: %d phone conflicts", len(f.Phone))
}
first := f.Phone[0]
for _, l := range first.Ours.Lines {
if l.N > 9 {
t.Errorf("FAIL: the first conflict's context runs into the second: line %d", l.N)
}
}
if first.OursLine != 3 || first.TheirsLine != 3 {
t.Errorf("FAIL: the conflict is at ours %d, theirs %d", first.OursLine, first.TheirsLine)
}
if !f.Phone[1].More {
t.Error("FAIL: the second conflict has more context below it but no More context")
}
var conf int
for _, l := range first.Base.Lines {
if !l.Ctx {
conf++
}
}
if conf != 0 || first.Base.Empty == "" {
t.Errorf("FAIL: both added: the ancestor should have no lines of its own and say so")
}
}
// The panes and the phone view render with their hooks, the script's
// handles, and each pane's header naming its side.
func TestMeldTemplateRenders(t *testing.T) {
v := buildMeld(meldFixture(), "/r/alpha")
var b bytes.Buffer
if err := template.Must(base.Clone()).ExecuteTemplate(&b, "meld", v); err != nil {
t.Fatal(err)
}
h := b.String()
for _, want := range []string{`data-hook="meld"`, `data-hook="meld-file"`, `data-meld`, `data-pane="L"`, `data-pane="M"`,
`data-pane="R"`, `data-gut="L"`, `class="ovr"`, `data-hook="meld-phone"`, `data-hook="meld-count" data-count="2"`,
`data-hook="meld-tag"`, `class="seg cf"`, `class="l fold"`} {
if !strings.Contains(h, want) {
t.Errorf("FAIL: rendered view lacks %s", want)
}
}
if strings.Count(h, `data-hook="phone-conflict"`) != 2 {
t.Error("FAIL: the phone view does not list both conflicts")
}
}
// The controls are there only for a reader who may resolve: a commit
// form, take-all, both/neither/edit on each open conflict, the phone's
// buttons, and the draft and tips the script needs. A changed region
// carries its index; an unchanged one does not.
func TestMeldResolveControlsOnlyForAResolver(t *testing.T) {
render := func(v *meldView) string {
var b bytes.Buffer
if err := template.Must(base.Clone()).ExecuteTemplate(&b, "meld", v); err != nil {
t.Fatal(err)
}
return b.String()
}
v := buildMeld(meldFixture(), "/r/alpha")
if h := render(v); strings.Contains(h, "data-resolve") || strings.Contains(h, `data-hook="meld-commit"`) || strings.Contains(h, `data-act=`) {
t.Error("FAIL: a reader who may not resolve is offered the controls")
}
v.CanResolve, v.ResolveURL, v.DraftKey = true, "/r/alpha/landing/aa/resolve", "meld:alpha/aa/b/t"
h := render(v)
for _, want := range []string{`data-resolve`, `data-draft="meld:alpha/aa/b/t"`, `data-branch-tip="b1b1b1b1b1b1b1"`,
`data-hook="meld-commit"`, `action="/r/alpha/landing/aa/resolve"`, `name="answers"`, `data-take-all="L"`,
`data-act="LR"`, `data-act="N"`, `data-act="E"`, `data-acts`, `data-region="2"`} {
if !strings.Contains(h, want) {
t.Errorf("FAIL: a resolver's view lacks %s", want)
}
}
if strings.Count(h, `data-region="0"`) != 0 {
t.Error("FAIL: an unchanged stretch carries a region index")
}
v.CanResolve, v.Unresolvable = false, "big.bin is too large"
if h := render(v); !strings.Contains(h, `data-hook="meld-unresolvable"`) {
t.Error("FAIL: a file that cannot be answered here does not say so")
}
}
// A region both sides changed alike is merged and marks nothing: the
// three panes agree there.
func TestMeldBothRegionMarksNothing(t *testing.T) {
rep := meldFixture()
rep.Files[0].Regions[1].Kind = "both"
rep.Files[0].Theirs.Lines[1] = rep.Files[0].Ours.Lines[1]
f := buildMeld(rep, "/r/alpha").Files[0]
s := f.Segs[1]
if s.Region != 1 || s.LCls != "" || s.MCls != "" || s.RCls != "" || len(s.M) != 1 {
t.Errorf("FAIL: a both region laid out as %+v", s)
}
}