Skip to content
quarry Artifact [57c6b62f1d]
Login

Artifact [57c6b62f1d]

Artifact 57c6b62f1d20abb831aeb9e4b0072d48831fbc1de0f0b4579347ade7dcc634f8:


package diff

import (
	"strings"
	"testing"
)

// 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")
}