@ 261101.7
current
package main

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// fakeSyntax puts a helper on PATH that prints a fixed tree for .go files
// and refuses the rest, as the real one does without a grammar.
func fakeSyntax(t *testing.T) {
	dir := t.TempDir()
	script := "#!/bin/sh\ncase \"$1\" in *.go) ;; *) echo \"E95: no grammar for $1\" >&2; exit 95;; esac\ncat >/dev/null\n" +
		"printf '0\\tsource_file\\t\\t0\\t0\\t3\\t0\\n1\\tfunction_declaration\\t\\t0\\t0\\t2\\t1\\n2\\tidentifier\\tname\\t0\\t5\\t0\\t6\\n'\n"
	if err := os.WriteFile(filepath.Join(dir, progName+"-syntax"), []byte(script), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", dir)
}

// A tree page folds each node with children, opens the root, and gives
// each row its field, kind, text and lines.
func TestSyntaxPage(t *testing.T) {
	fakeSyntax(t)
	src := []byte("func f() {\n\treturn\n}\n")
	h, err := syntaxPage(context.Background(), "a.go", pageHref("/t/a.go"), src)
	if err != nil {
		t.Fatal(err)
	}
	for _, want := range []string{
		`<details open><summary><b>source_file</b> <code>func f() { …</code> <a href="a.go#L1-L3">1–3</a></summary>`,
		`<details><summary><b>function_declaration</b> <code>func f() { …</code> <a href="a.go#L1-L3">1–3</a></summary>`,
		`<div><i>name:</i> <b>identifier</b> <code>f</code> <a href="a.go#L1">1</a></div>`,
	} {
		if !strings.Contains(h, want) {
			t.Errorf("missing %s in\n%s", want, h)
		}
	}
	if strings.Count(h, "<details") != strings.Count(h, "</details>") {
		t.Errorf("folds not closed:\n%s", h)
	}
}

// Without the helper, or without a grammar, a tree page says so.
func TestSyntaxMissing(t *testing.T) {
	fakeSyntax(t)
	if _, err := syntaxNodes(context.Background(), "x.txt", nil); !errors.Is(err, errNoGrammar) {
		t.Errorf("no grammar: %v", err)
	}
	t.Setenv("PATH", t.TempDir())
	if _, err := syntaxNodes(context.Background(), "b.go", nil); !errors.Is(err, errNoHelper) {
		t.Errorf("no helper: %v", err)
	}
}

// Output the helper should never print fails the page with what was
// wrong, and so does a helper that dies after its first rows.
func TestSyntaxBadOutput(t *testing.T) {
	for _, tc := range []struct{ script, want string }{
		{`printf '0\tsource_file\t\t0\t0\n'`, "a line of 5 fields"},
		{`printf '0\tsource_file\t\t0\t-1\t3\t0\n'`, "a bad number: -1"},
		{`printf '0\tsource_file\t\t0\t0\t3\t0\n'; echo 'E5: broke' >&2; exit 5`, "E5: broke"},
		{`printf '0\tsource_file\t\t0'; echo 'E5: broke' >&2; exit 5`, "E5: broke"},
	} {
		dir := t.TempDir()
		script := "#!/bin/sh\ncat >/dev/null\n" + tc.script + "\n"
		if err := os.WriteFile(filepath.Join(dir, progName+"-syntax"), []byte(script), 0o755); err != nil {
			t.Fatal(err)
		}
		t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
		if _, err := syntaxPage(context.Background(), "a.go", "a.go", []byte("x\n")); err == nil || !strings.Contains(err.Error(), tc.want) {
			t.Errorf("%s: %v, want %q", tc.script, err, tc.want)
		}
	}
}

// An excerpt keeps a node's first row, its spaces folded, and marks text
// cut or continued.
func TestExcerpt(t *testing.T) {
	lines := [][]byte{[]byte("x := f(a,\t  b)"), []byte("y"), []byte(strings.Repeat("z", 80))}
	for _, tc := range []struct {
		n    synNode
		want string
	}{
		{synNode{sr: 0, sc: 5, er: 0, ec: 14}, "f(a, b)"},
		{synNode{sr: 0, sc: 0, er: 1, ec: 1}, "x := f(a, b) …"},
		{synNode{sr: 0, sc: 0, er: 1, ec: 0}, "x := f(a, b)"},
		{synNode{sr: 2, sc: 0, er: 2, ec: 80}, strings.Repeat("z", excerptLen) + " …"},
		{synNode{sr: 5}, ""},
	} {
		if got := excerpt(lines, tc.n); got != tc.want {
			t.Errorf("%+v: %q, want %q", tc.n, got, tc.want)
		}
	}
}

// writeExcerpt writes what excerpt gives, escaped, wherever a cut lands:
// on a word, on the space that joins two, inside a multibyte character,
// and in rows with escapes, a NUL, Unicode spaces or invalid UTF-8.
func TestWriteExcerpt(t *testing.T) {
	word := strings.Repeat("z", excerptLen-1)
	lines := [][]byte{
		[]byte(word + " a b"),
		[]byte(word + "  \t\u00a0b"),
		[]byte(word + "é" + " é"),
		[]byte(" \t<a href=\"x\">'&'</a>\x00 \u2003end "),
		[]byte(strings.Repeat("ü", excerptLen) + "\u0085"),
		[]byte(strings.Repeat("y", 70) + "\xff z"),
		[]byte("ok \xffbad"),
		[]byte("   "),
	}
	var b strings.Builder
	for sr, line := range lines {
		for sc := range len(line) + 1 {
			for _, n := range []synNode{
				{sr: sr, sc: sc, er: sr, ec: len(line)},
				{sr: sr, sc: sc, er: sr + 1, ec: 1},
				{sr: sr, sc: sc, er: sr + 1, ec: 0},
				{sr: sr, sc: sc, er: sr, ec: sc + 3},
			} {
				var want strings.Builder
				if s := excerpt(lines, n); s != "" {
					want.WriteString(" <code>")
					escapeHTML(&want, s)
					want.WriteString("</code>")
				}
				b.Reset()
				writeExcerpt(&b, lines, n)
				if b.String() != want.String() {
					t.Errorf("%+v: %q, want %q", n, b.String(), want.String())
				}
			}
		}
	}
}