Skip to content
quarry Artifact [fef167cfb2]
Login

Artifact [fef167cfb2]

Artifact fef167cfb29656a02e6a0debcb5184aed6808698b65142373a6749f55e378991:


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)
	}
}