Skip to content
quarry Artifact [932a61daf7]
Login

Artifact [932a61daf7]

Artifact 932a61daf758106a7be5e40581e74708db85c5eb82b3be0c7746f1af5efbc80f:


package web

import (
	"fmt"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/zitcli"
)

// lineServer serves one zit store whose trunk holds big.go, a thousand
// numbered lines, and answers with trunk's full hash.
func lineServer(t *testing.T) (*httptest.Server, string) {
	t.Helper()
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	dir := t.TempDir()
	run := func(args ...string) string {
		cmd := exec.Command("zit", args...)
		cmd.Dir = dir
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("zit %v: %v\n%s", args, err, out)
		}
		return string(out)
	}
	run("init", ".")
	var b strings.Builder
	for i := 1; i <= 1000; i++ {
		fmt.Fprintf(&b, "var line%d = %d\n", i, i)
	}
	if err := os.WriteFile(filepath.Join(dir, "big.go"), []byte(b.String()), 0o644); err != nil {
		t.Fatal(err)
	}
	run("add", "big.go")
	run("commit", "-m", "big", "--user", "u")
	hash := regexp.MustCompile(`[0-9a-f]{64}`).FindString(run("branch", "info", "trunk"))
	if hash == "" {
		t.Fatal("no trunk hash")
	}
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix, Zit: zitcli.New(dir)}})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
	t.Cleanup(srv.Close)
	return srv, hash
}

var (
	lineRowRe = regexp.MustCompile(`<span class="line( hl)?" id="L(\d+)">`)
	lnLinkRe  = regexp.MustCompile(`data-hook="line-link" href="([^"]*)"`)
)

// rows lists the drawn line numbers and which of them are highlighted.
func rows(body string) (drawn, hit []string) {
	for _, m := range lineRowRe.FindAllStringSubmatch(body, -1) {
		drawn = append(drawn, m[2])
		if m[1] != "" {
			hit = append(hit, m[2])
		}
	}
	return drawn, hit
}

// Every line is numbered, and the number is a link to the line, drawn
// from an attribute so a selection of the code never copies it.
func TestFileLinesAreNumberedOutsideTheText(t *testing.T) {
	srv, _ := lineServer(t)
	code, body := getPage(t, srv, "/r/alpha/file/trunk/big.go")
	if code != 200 {
		t.Fatalf("status %d", code)
	}
	drawn, hit := rows(body)
	if len(drawn) == 0 || drawn[0] != "1" || len(hit) != 0 {
		t.Fatalf("FAIL: drawn from %v, %d highlighted", drawn[:min(3, len(drawn))], len(hit))
	}
	if !strings.Contains(body, `<a class="ln" href="?ln=7#L7" data-n="7" aria-label="line 7"></a>`) {
		t.Error("FAIL: line 7's number is not an empty link carrying its number")
	}
	if !strings.Contains(body, `data-hook="line-bar"`) || !strings.Contains(body, `hidden>`) {
		t.Error("FAIL: with nothing picked the bar should be drawn hidden")
	}
}

// ln= opens the file a little above the range, with exactly the range
// highlighted, and the window reaches past it.
func TestFileLineRangeOpensOnTheRange(t *testing.T) {
	srv, _ := lineServer(t)
	for _, c := range []struct {
		ln       string
		first    string
		hitFirst string
		hits     int
	}{
		{"600", "590", "600", 1},
		{"600-620", "590", "600", 21},
		{"620-600", "590", "600", 21},
		{"600-602,610-611", "590", "600", 5},
		{"3", "1", "3", 1},
	} {
		_, body := getPage(t, srv, "/r/alpha/file/trunk/big.go?ln="+c.ln)
		drawn, hit := rows(body)
		if len(drawn) == 0 || drawn[0] != c.first {
			t.Errorf("FAIL: ln=%s: window starts at %v, want %s", c.ln, drawn[:min(1, len(drawn))], c.first)
			continue
		}
		if len(hit) != c.hits || hit[0] != c.hitFirst {
			t.Errorf("FAIL: ln=%s: highlighted %v", c.ln, hit)
		}
		if c.first != "1" && !strings.Contains(body, `data-hook="file-earlier"`) {
			t.Errorf("FAIL: ln=%s: no way to the lines above the window", c.ln)
		}
	}
}

// A range past the end shows the end of the file, highlights what
// exists, and says where the file stops. It is never an error page.
func TestFileLineRangePastTheEnd(t *testing.T) {
	srv, _ := lineServer(t)
	code, body := getPage(t, srv, "/r/alpha/file/trunk/big.go?ln=995-1200")
	drawn, hit := rows(body)
	if code != 200 || len(hit) != 6 || hit[0] != "995" || drawn[len(drawn)-1] != "1000" {
		t.Errorf("FAIL: %d, drawn to %v, highlighted %v", code, drawn[len(drawn)-1:], hit)
	}
	if !strings.Contains(body, `data-hook="line-past"`) {
		t.Error("FAIL: the bar does not say the file ends")
	}
	code, body = getPage(t, srv, "/r/alpha/file/trunk/big.go?ln=5000")
	if drawn, _ = rows(body); code != 200 || len(drawn) == 0 || drawn[len(drawn)-1] != "1000" {
		t.Errorf("FAIL: a range wholly past the end: %d, drawn %d lines", code, len(drawn))
	}
}

// The link the bar offers names the check-in by hash, so the lines it
// points at cannot move when the branch does.
func TestFileLineLinkNamesTheHash(t *testing.T) {
	srv, hash := lineServer(t)
	_, body := getPage(t, srv, "/r/alpha/file/trunk/big.go?ln=12-14")
	m := lnLinkRe.FindStringSubmatch(body)
	if m == nil || m[1] != "/r/alpha/file/"+hash+"/big.go?ln=12-14" {
		t.Errorf("FAIL: copy link is %v, want the trunk hash", m)
	}
}

// The next window keeps the range, so a long range highlights across
// the windows it spans.
func TestFileNextWindowKeepsTheRange(t *testing.T) {
	f := fileDetail{Hash: "abc", Path: "big.go", NextFrom: 400, Ranges: []lineRange{{390, 410}}}
	if got := f.MoreURL("alpha"); !strings.HasSuffix(got, "?from=400&ln=390-410") {
		t.Errorf("FAIL: next window %q drops the range", got)
	}
}

// On a diff with no comments to offer, a line number opens that line of
// the file: the new side at the check-in, the old side at the parent,
// under its old name when the file was renamed.
func TestDiffLineNumbersOpenTheFile(t *testing.T) {
	d := *noteDiff()
	d.From, d.To = "parent0", "child1"
	d.Structured[0].PrevPath = "was.go"
	for _, split := range []bool{false, true} {
		var buf strings.Builder
		if err := noHooks.template(diffPage).ExecuteTemplate(&buf, "diff-more",
			pageData{Repo: "r", Diff: d, DiffSplit: split}); err != nil {
			t.Fatal(err)
		}
		page := buf.String()
		for _, want := range []string{`href="/r/r/file/child1/a.go?ln=2#L2"`, `href="/r/r/file/parent0/was.go?ln=2#L2"`} {
			if !strings.Contains(page, want) || !strings.Contains(page, `data-hook="diff-line-file"`) {
				t.Errorf("FAIL: split=%v: no %s", split, want)
			}
		}
	}
}