Skip to content
quarry Artifact [51a6ee9a6d]
Login

Artifact [51a6ee9a6d]

Artifact 51a6ee9a6de34a26196f7646b35d1fa36e6bb410812544d400e7efe303afa157:


package web

import (
	"bytes"
	"html/template"
	"strconv"
	"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, gaps int
	for _, l := range first.Base.Lines {
		switch {
		case l.Gap:
			gaps++
		case !l.Ctx:
			conf++
		}
	}
	if conf != 0 || gaps != 1 || 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)
	}
}

// A phone block whose context stops short says why: the file's start or
// end, or the next conflict, so a block that cannot grow reads as one.
func TestPhoneBlocksMarkWhereContextStops(t *testing.T) {
	lines := func(n int) []template.HTML {
		out := make([]template.HTML, n)
		for i := range out {
			out[i] = template.HTML(strconv.Itoa(i))
		}
		return out
	}
	// Two conflicts in a 40-line file: the first 30 lines in, the
	// second right at the end, four lines after the first.
	src := lines(40)
	regions := []meldRegion{
		{Kind: "same", Base: [2]int{0, 30}, Ours: [2]int{0, 30}, Theirs: [2]int{0, 30}},
		{Kind: "conflict", Base: [2]int{30, 32}, Ours: [2]int{30, 32}, Theirs: [2]int{30, 32}},
		{Kind: "same", Base: [2]int{32, 36}, Ours: [2]int{32, 36}, Theirs: [2]int{32, 36}},
		{Kind: "conflict", Base: [2]int{36, 40}, Ours: [2]int{36, 40}, Theirs: [2]int{36, 40}},
	}
	pcs := phoneConflicts(0, regions, src, src, src)
	if len(pcs) != 2 {
		t.Fatalf("FAIL: %d conflicts", len(pcs))
	}
	first, last := pcs[0].Ours, pcs[1].Ours
	if first.Top != nil {
		t.Error("FAIL: a block with more above claims an edge")
	}
	if first.Bottom == nil || first.Bottom.Kind != "conflict" || !first.Bottom.Extra {
		t.Errorf("FAIL: the first block does not say the next conflict follows: %+v", first.Bottom)
	}
	if last.Bottom == nil || last.Bottom.Kind != "end" {
		t.Errorf("FAIL: the last block does not say the file ends: %+v", last.Bottom)
	}
	if last.Top == nil || last.Top.Kind != "conflict" {
		t.Errorf("FAIL: the last block does not say a conflict is above: %+v", last.Top)
	}
	var b bytes.Buffer
	if err := noHooks.template(landingPage).ExecuteTemplate(&b, "phone-block", map[string]any{"Cls": "pours", "Label": "ours", "B": last}); err != nil {
		t.Fatal(err)
	}
	if strings.Count(b.String(), `data-hook="phone-edge"`) != 2 {
		t.Error("FAIL: the edges are not drawn")
	}
}