package diff
import (
"strings"
"testing"
"unsafe"
)
// The ticket's own motivating case, from pikchr a7f1c35b: one line
// gains "(including" at its end. The reader must see THAT — a marked
// range on each side — not two fully-coloured lines to diff by eye.
func TestIntraLineMarks(t *testing.T) {
u := `--- a/pikchr.y
+++ b/pikchr.y
@@ -5220,7 +5220,8 @@ static void x(void){
int i;
- /* Remove leading and trailing whitespace from each argument.
+ /* Remove leading and trailing whitespace from each argument (including
more;
`
files := Parse(u)
if len(files) != 1 || files[0].Path != "pikchr.y" || files[0].Change != Change {
t.Fatalf("files = %+v", files)
}
h := files[0].Hunks[0]
if h.OldStart != 5220 || h.NewStart != 5220 || h.Header != "static void x(void){" {
t.Fatalf("hunk = %+v", h)
}
// Numbers: context carries both, deletion old-only, addition new-only.
if h.Lines[0].OldNum != 5220 || h.Lines[0].NewNum != 5220 ||
h.Lines[1].OldNum != 5221 || h.Lines[1].NewNum != 0 ||
h.Lines[2].NewNum != 5221 || h.Lines[2].OldNum != 0 ||
h.Lines[3].OldNum != 5222 || h.Lines[3].NewNum != 5222 {
t.Fatalf("numbering wrong: %+v", h.Lines)
}
Enrich(files)
del, add := files[0].Hunks[0].Lines[1], files[0].Hunks[0].Lines[2]
marked := func(l Line) string {
var b strings.Builder
for _, tok := range l.Tokens {
if tok.Mark {
b.WriteString(tok.Text)
}
}
return b.String()
}
if got := marked(add); !strings.Contains(got, "(including") {
t.Fatalf("addition marks = %q, want the added words", got)
}
if got := marked(add); strings.Contains(got, "Remove leading") {
t.Fatalf("addition over-marked the unchanged text: %q", got)
}
if got := marked(del); strings.Contains(got, "Remove leading") {
t.Fatalf("deletion over-marked: %q", got)
}
// Syntax classes arrived positionally (pikchr.y matches a lexer via
// the .y suffix); reassembling all tokens must reproduce the text
// exactly — the splice must never lose or duplicate a byte.
var whole strings.Builder
for _, tok := range add.Tokens {
whole.WriteString(tok.Text)
}
if whole.String() != " /* Remove leading and trailing whitespace from each argument (including" {
t.Fatalf("splice corrupted the text: %q", whole.String())
}
}
// A new file and a deleted file are CHANGE KINDS, not diffs against
// /dev/null; and total rewrites mark nothing — a fully-marked pair
// says less than plain colour.
func TestChangeKindsAndRewriteGuard(t *testing.T) {
u := `--- /dev/null
+++ b/fresh.go
@@ -0,0 +1,1 @@
+package fresh
--- a/gone.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package gone
`
files := Parse(u)
if len(files) != 2 || files[0].Change != Added || files[0].Path != "fresh.go" ||
files[1].Change != Deleted || files[1].Path != "gone.go" {
t.Fatalf("kinds = %+v", files)
}
da, db := wordDiff("completely different text here", "nothing shared at all now")
if da != nil || db != nil {
t.Fatalf("a rewrite was word-marked: %v %v", da, db)
}
}
// Per-line tokenization meets multi-line constructs it cannot close —
// a raw string's backtick, a diff's @@ inside a string — and chroma
// answers with Error tokens, which the stylesheet paints as red
// warning boxes over healthy code. A lexer's shrug must render PLAIN.
func TestLexerErrorsRenderPlain(t *testing.T) {
u := "--- a/x.go\n+++ b/x.go\n@@ -1,1 +1,2 @@\n context\n+\tu := `--- embedded\n"
files := Parse(u)
Enrich(files)
for _, l := range files[0].Hunks[0].Lines {
for _, tok := range l.Tokens {
if tok.Class == "err" {
t.Fatalf("an Error token leaked a class: %+v", l.Tokens)
}
}
}
}
// Found by the corpus sweep on sqlite: a deleted line whose text
// begins "-- " (a SQL comment) prints as "---…" in unified output, and
// prefix-matching alone misreads it as the next file's header, losing
// the line. The @@ counts are the disambiguator: while a hunk's
// promised lines remain, everything is content.
func TestHeaderLookalikesInsideHunks(t *testing.T) {
u := "--- a/q.sql\n+++ b/q.sql\n@@ -1,3 +1,2 @@\n context\n--- drop this SQL comment\n+++ and add this one\n"
files := Parse(u)
if len(files) != 1 {
t.Fatalf("the lookalike split the file: %+v", files)
}
lines := files[0].Hunks[0].Lines
if len(lines) != 3 {
t.Fatalf("got %d lines, want 3: %+v", len(lines), lines)
}
if lines[1].Kind != "deletion" || lines[1].Tokens[0].Text != "-- drop this SQL comment" {
t.Fatalf("the SQL-comment deletion was misread: %+v", lines[1])
}
if lines[2].Kind != "addition" || lines[2].Tokens[0].Text != "++ and add this one" {
t.Fatalf("the ++ addition was misread: %+v", lines[2])
}
}
// The counts are the FILE's, not the window's: a reader paging through
// a long diff must see one total, not a total that shrinks per page.
func TestParseCountsChangedLinesPerFileAndSurvivesWindowing(t *testing.T) {
unified := "--- a/one.go\n+++ b/one.go\n" +
"@@ -1,2 +1,3 @@\n ctx\n-gone\n+new\n+also\n" +
"@@ -20,2 +21,2 @@\n ctx\n-old\n+fresh\n" +
"--- /dev/null\n+++ b/two.go\n@@ -0,0 +1,2 @@\n+a\n+b\n"
files := Parse(unified)
if len(files) != 2 {
t.Fatalf("want 2 files, got %d", len(files))
}
if files[0].Added != 3 || files[0].Deleted != 2 {
t.Errorf("one.go = +%d -%d, want +3 -2", files[0].Added, files[0].Deleted)
}
if files[1].Added != 2 || files[1].Deleted != 0 {
t.Errorf("two.go = +%d -%d, want +2 -0", files[1].Added, files[1].Deleted)
}
// Cut to a window small enough to drop hunks, then ask again.
Window(files, 0, 4)
if files[0].Added != 3 || files[0].Deleted != 2 {
t.Errorf("windowing changed the count to +%d -%d", files[0].Added, files[0].Deleted)
}
if len(files[0].Hunks) != 1 {
t.Fatalf("window kept %d hunks, expected the count to outlive a cut", len(files[0].Hunks))
}
}
// A patch that opens mid-hunk with no file header must not panic the
// tally.
func TestParseTalliesSurviveAHeaderlessHunk(t *testing.T) {
Parse("@@ -1,1 +1,1 @@\n-a\n+b\n")
}
// The lexer memo must not keep a diff alive. Its key is a file's base
// name, which is a substring of the diff text; stored as-is, it pinned
// the whole diff for as long as the process ran (10 GB on zeitforge).
func TestLexerMemoDoesNotPinTheDiff(t *testing.T) {
text := strings.Repeat("x", 1<<20) + "/pinned-name.go"
path := text[len(text)-len("x/pinned-name.go"):]
lexerFor(path)
lexerMu.Lock()
defer lexerMu.Unlock()
for k := range lexerCache {
if k != "pinned-name.go" {
continue
}
start := uintptr(unsafe.Pointer(unsafe.StringData(text)))
at := uintptr(unsafe.Pointer(unsafe.StringData(k)))
if at >= start && at < start+uintptr(len(text)) {
t.Fatal("FAIL: the memo's key points into the diff text, which keeps all of it alive")
}
t.Log("ok: the key is its own copy")
return
}
t.Fatal("FAIL: the name was not memoised")
}