Download
107 lines 3.6 KiBGoModified 2026-10-19 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
package render_test

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"strings"
	"testing"
)

func TestContentsJSON(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.seed()
	e.render()
	var data struct {
		Repo  string `json:"repo"`
		SHA   string `json:"sha"`
		Files []struct {
			P string   `json:"p"`
			L []string `json:"l"`
		} `json:"files"`
	}
	if err := json.Unmarshal([]byte(e.read("search/proj/corpus.json")), &data); err != nil {
		t.Fatalf("corpus: %v", err)
	}
	if data.Repo != "proj" || len(data.SHA) != 40 {
		t.Errorf("header: %+v", data)
	}
	paths := map[string][]string{}
	for _, f := range data.Files {
		paths[f.P] = f.L
	}
	if _, ok := paths["bin.dat"]; ok {
		t.Error("binary file included in search data")
	}
	if _, ok := paths["img/logo.png"]; ok {
		t.Error("image included in search data")
	}
	if _, ok := paths["vendor/lib.min.js"]; ok {
		t.Error("search_exclude glob not honoured")
	}
	if got := paths["src/main.c"]; len(got) != 5 || got[0] != "#include <stdio.h>" || got[4] != "}" {
		t.Errorf("line splitting wrong: %q", got)
	}
	if gz := gunzip(t, e.h("search/proj/corpus.json.gz")); gz != e.read("search/proj/corpus.json") {
		t.Error("search/proj/corpus.json.gz differs from search/proj/corpus.json")
	}
}

// TestContentsVersion: the search page fetches the corpus by its hash, so
// httpd can cache it for good, and a config change that rewrites it
// without a new commit gives it a new URL.
func TestContentsVersion(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.seed()
	e.render()
	url := func() string {
		sum := sha256.Sum256([]byte(e.read("search/proj/corpus.json")))
		return `data-data="/search/proj/corpus.json?v=` + hex.EncodeToString(sum[:])[:16] + `"`
	}
	before := url()
	mustContain(t, "search page", e.read("search/proj/index.html"), before)
	e.configure(`search_exclude: ["*.c"]` + "\n")
	e.render()
	after := url()
	if after == before {
		t.Fatal("excluding files did not change the corpus")
	}
	mustContain(t, "search page", e.read("search/proj/index.html"), after)
}

// TestHugeTextLines: a text file too big to serve or search still gets its
// line count in listings, streamed rather than read whole, and is not
// summarized as binary data.
func TestHugeTextLines(t *testing.T) {
	e := newEnv(t, "proj", "raw_max_bytes: 64\nsearch_max_bytes: 64\n")
	e.write("huge.log", strings.Repeat("a log line\n", 1000))
	e.write("huge.bin", "\x00"+strings.Repeat("z\n", 1000))
	e.commit("huge")
	e.push(false)
	e.render()
	root := e.read(e.page(""))
	mustContain(t, "root", root, `<span class="amount">1K lines</span>`, `<span class="amount">0 lines</span>`)
	mustContain(t, "binary pill", root, `title="2 KiB of binary data across 1 file"`)
}

// TestUnknownText: a text file no lexer knows counts as code, a dotfile as
// "Dotfile" and anything else by its extension, unless it is named like
// prose (.txt, or no extension), which is still docs; a dotfile with a
// lexer of its own keeps it.
func TestUnknownText(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.write(".clang-format", "BasedOnStyle: LLVM\nIndentWidth: 4\n")
	e.write(".bashrc", "alias ll='ls -l'\n")
	e.write("foo.qwerxxx", "a\nb\nc\n")
	e.write("notes.txt", "some words here\n")
	e.write("LICENSE", "do what you like\n")
	e.commit("unknown")
	e.push(false)
	e.render()
	root := e.read(e.page(""))
	code := between(t, root, `<div class="langs code"`, "</div>")
	mustContain(t, "code row", code, `>QWERXXX <b>3</b></a>`, `>Dotfile <b>2</b></a>`, `>Bash <b>1</b></a>`)
	mustNotContain(t, "code row", code, "Text")
	mustContain(t, "docs row", root, `>Text <b>7</b></a>`)
	mustContain(t, "lines ring", root, `<circle class="code"`)
}