package diff import ( "strings" "testing" "git.pablomurad.com/pablo/patchwork/internal/doc" ) // apply rebuilds b from a and an edit script. func apply[T comparable](a, b []T, edits []Edit) (gotA, gotB []T) { for _, e := range edits { switch e.Kind { case Eq: gotA = append(gotA, a[e.A0:e.A1]...) gotB = append(gotB, a[e.A0:e.A1]...) case Del: gotA = append(gotA, a[e.A0:e.A1]...) case Ins: gotB = append(gotB, b[e.B0:e.B1]...) } } return } func TestMyersMinimal(t *testing.T) { a := strings.Split("ABCABBA", "") b := strings.Split("CBABAC", "") edits := Myers(a, b) ga, gb := apply(a, b, edits) if strings.Join(ga, "") != "ABCABBA" || strings.Join(gb, "") != "CBABAC" { t.Fatalf("bad script %+v", edits) } d := 0 for _, e := range edits { if e.Kind != Eq { d += (e.A1 - e.A0) + (e.B1 - e.B0) } } if d != 5 { t.Fatalf("edit distance %d, want 5 (the classic Myers example)", d) } } func FuzzMyers(f *testing.F) { f.Add("the quick brown fox", "the slow brown dog") f.Add("", "abc") f.Add("abc", "") f.Fuzz(func(t *testing.T, x, y string) { // Compare runes, not the input strings: []rune turns invalid UTF-8 // into U+FFFD, which is a property of the conversion, not of Myers. a, b := []rune(x), []rune(y) ga, gb := apply(a, b, Myers(a, b)) if string(ga) != string(a) || string(gb) != string(b) { t.Fatalf("script does not rebuild inputs: %q %q", string(ga), string(gb)) } }) } func rebuild(segs []Segment) (old, new string) { var a, b strings.Builder for _, s := range segs { if s.T != SegIns { a.WriteString(s.S) } if s.T != SegDel { b.WriteString(s.S) } } return a.String(), b.String() } func FuzzWords(f *testing.F) { f.Add("E se cada protótipo fosse cuidado", "E se cada rascunho fosse regado") f.Add("one two three", "") f.Fuzz(func(t *testing.T, x, y string) { segs, _, _, _ := Words(x, y) a, b := rebuild(segs) if a != x || b != y { t.Fatalf("segments do not rebuild: %q / %q", a, b) } }) } func TestWordsHighlightsChangedWords(t *testing.T) { segs, replaced, added, removed := Words( "O modelo híbrido é o pior dos dois mundos para equipes pequenas.", "O modelo híbrido funciona para equipes pequenas.") if replaced { t.Fatal("should not be a whole replacement") } want := []Segment{ {SegEq, "O modelo híbrido "}, {SegDel, "é o pior dos dois mundos"}, {SegIns, "funciona"}, {SegEq, " para equipes pequenas."}, } if len(segs) != len(want) { t.Fatalf("got %q", segs) } for i := range want { if segs[i] != want[i] { t.Fatalf("segment %d = %q, want %q", i, segs[i], want[i]) } } if added != 1 || removed != 6 { t.Fatalf("added %d removed %d", added, removed) } } func TestWordsCleanupAbsorbsLonelySpaces(t *testing.T) { segs, _, _, _ := Words("the cat sat on the mat today", "a dog sat on the mat today") if segs[0].T != SegDel || segs[0].S != "the cat" || segs[1].S != "a dog" { t.Fatalf("got %q", segs) } } func TestWordsReplacedWhenRewritten(t *testing.T) { segs, replaced, _, _ := Words("Completely different sentence here.", "Nothing in common at all, really.") if !replaced || len(segs) != 2 { t.Fatalf("want replacement, got %v %q", replaced, segs) } } func blocks(src string) []doc.Block { return doc.SplitBlocks(src) } func opsString(ops []Op) string { var out []string for _, op := range ops { out = append(out, op.Op) } return strings.Join(out, ",") } func TestBlocks(t *testing.T) { cases := []struct{ name, old, new, want string }{ {"no change", "a\n\nb", "a\n\nb", "eq,eq"}, {"modify", "Intro.\n\nThe hybrid model is the worst of both worlds.\n\nEnd.", "Intro.\n\nThe hybrid model is the best of both worlds.\n\nEnd.", "eq,modify,eq"}, {"insert list item", "- one\n- two", "- one\n- one and a half\n- two", "eq,insert,eq"}, {"delete", "keep\n\ndrop this paragraph\n\nkeep too", "keep\n\nkeep too", "eq,delete,eq"}, {"move", "First paragraph here.\n\nSecond paragraph here.\n\nThird one.", "Third one.\n\nFirst paragraph here.\n\nSecond paragraph here.", "move,eq,eq"}, {"one for one replace is an edit", "Alpha beta gamma.", "Totally new words now.", "modify"}, {"unrelated two for two", "Alpha beta gamma.\n\nDelta epsilon.", "Totally new words.\n\nNothing shared.", "delete,delete,insert,insert"}, {"insert and unrelated delete", "keep\n\nold unrelated text\n\nend", "keep\n\nbrand new paragraph\n\nanother new one\n\nend", "eq,delete,insert,insert,eq"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { res := Blocks(blocks(c.old), blocks(c.new)) if got := opsString(res.Ops); got != c.want { t.Fatalf("got %s, want %s", got, c.want) } }) } } func TestBlocksMoveRecordsPositions(t *testing.T) { res := Blocks(blocks("A one.\n\nB two.\n\nC three."), blocks("C three.\n\nA one.\n\nB two.")) mv := res.Ops[0] if mv.Op != OpMove || *mv.From != 2 || *mv.To != 0 { t.Fatalf("got %+v", mv) } if res.Stats.BlocksChanged != 1 || res.Stats.WordsAdded != 0 { t.Fatalf("stats %+v", res.Stats) } } func TestBlocksPairsSimilarInOrder(t *testing.T) { old := "First idea about gardens at night.\n\nSecond idea about wind and grass." new := "First idea about gardens at dusk.\n\nSecond idea about wind and tall grass." res := Blocks(blocks(old), blocks(new)) if opsString(res.Ops) != "modify,modify" { t.Fatalf("got %s", opsString(res.Ops)) } if res.Stats.WordsAdded != 2 || res.Stats.WordsRemoved != 1 { t.Fatalf("stats %+v", res.Stats) } } func TestExcerpt(t *testing.T) { ops := []Op{{Op: OpEq}, {Op: OpEq}, {Op: OpEq}, {Op: OpModify}, {Op: OpEq}, {Op: OpEq}, {Op: OpEq}, {Op: OpInsert}, {Op: OpEq}} got, cut := Excerpt(ops, 1, 6) if cut || opsString(got) != "gap,eq,modify,eq,gap,eq,insert,eq" { t.Fatalf("got %s cut=%v", opsString(got), cut) } if got[0].Count != 2 || got[4].Count != 1 { t.Fatalf("gap counts %d %d", got[0].Count, got[4].Count) } got, cut = Excerpt(ops, 0, 1) if !cut || opsString(got) != "gap,modify" { t.Fatalf("got %s cut=%v", opsString(got), cut) } } func TestMeta(t *testing.T) { opt := doc.DefaultOptions() a := doc.Parse([]byte("---\ntitle: Old\ntags: [a, b]\nmaturity: seed\n---\nx"), opt) b := doc.Parse([]byte("---\ntitle: New\ntags: [a, b]\nmaturity: sprout\n---\nx"), opt) ch := Meta(a, b, opt.DiffKeys) if len(ch) != 2 || ch[0].Key != "title" || ch[1].Key != "maturity" || ch[1].New != "sprout" { t.Fatalf("got %+v", ch) } }