1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "strings"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 pb "bnema.dev/ermite-api/gen/ermite"
14)
15
16// Trojan Source (ADR 0017): direction controls, zero-width and control
17// characters are shown as markers; ordinary text, tabs and non-Latin letters
18// are not touched.
19func TestSegmentsShowInvisibleCharacters(t *testing.T) {
20 tests := []struct {
21 name, in string
22 want []Seg
23 }{
24 {"plain", "return x", []Seg{{Text: "return x"}}},
25 {"tab and unicode", "\tπ := “é”", []Seg{{Text: "\tπ := “é”"}}},
26 {"empty", "", []Seg{{Text: ""}}},
27 {"crlf", "a\r", []Seg{{Text: "a"}}},
28 {"rlo", "if x { // \u202eadmin", []Seg{{Text: "if x { // "}, {Text: "<U+202E>", Mark: true}, {Text: "admin"}}},
29 {"isolates", "\u2066a\u2069", []Seg{{Text: "<U+2066>", Mark: true}, {Text: "a"}, {Text: "<U+2069>", Mark: true}}},
30 {"zero width", "a\u200bb\ufeff", []Seg{{Text: "a"}, {Text: "<U+200B>", Mark: true}, {Text: "b"}, {Text: "<U+FEFF>", Mark: true}}},
31 {"control", "a\x00\x1bb", []Seg{{Text: "a"}, {Text: "<U+0000>", Mark: true}, {Text: "<U+001B>", Mark: true}, {Text: "b"}}},
32 {"tag character", "a\U000E0041", []Seg{{Text: "a"}, {Text: "<U+E0041>", Mark: true}}},
33 {"hangul filler", "\u3164", []Seg{{Text: "<U+3164>", Mark: true}}},
34 {"replacement is visible already", "a\ufffdb", []Seg{{Text: "a\ufffdb"}}},
35 }
36 for _, tt := range tests {
37 t.Run(tt.name, func(t *testing.T) {
38 assert.Equal(t, tt.want, segments(tt.in))
39 })
40 }
41 assert.Equal(t, "a<U+202E>b.go", visibleText("a\u202eb.go"))
42 assert.Equal(t, "plain", visibleText("plain"))
43 assert.Equal(t, "l1\nl<U+200B>2", messageText("l1\r\nl\u200b2\n\n"))
44}
45
46func line(kind pb.LineKind, oldN, newN int32, text string) *pb.Line {
47 return &pb.Line{Kind: kind, OldNumber: oldN, NewNumber: newN, Text: text}
48}
49
50// The split view lines a run of deletions up with the additions after it;
51// context fills both sides; the extra lines of the longer run have an empty
52// opposite side.
53func TestPairLines(t *testing.T) {
54 ctx, add, del := pb.LineKind_LINE_KIND_CONTEXT, pb.LineKind_LINE_KIND_ADDITION, pb.LineKind_LINE_KIND_DELETION
55 var ls []DiffLine
56 for _, l := range []*pb.Line{
57 line(ctx, 1, 1, "c"),
58 line(del, 2, 0, "d1"), line(del, 3, 0, "d2"), line(del, 4, 0, "d3"),
59 line(add, 0, 2, "a1"),
60 line(ctx, 5, 3, "c2"),
61 line(add, 0, 4, "a2"), line(add, 0, 5, "a3"),
62 } {
63 ls = append(ls, diffLine(l))
64 }
65 rows := pairLines(ls)
66 type pair struct{ l, r string }
67 var got []pair
68 for _, r := range rows {
69 p := pair{}
70 if r.L != nil {
71 p.l = r.L.Mark + r.L.Segs[0].Text
72 }
73 if r.R != nil {
74 p.r = r.R.Mark + r.R.Segs[0].Text
75 }
76 got = append(got, p)
77 }
78 assert.Equal(t, []pair{
79 {" c", " c"}, {"-d1", "+a1"}, {"-d2", ""}, {"-d3", ""}, {" c2", " c2"}, {"", "+a2"}, {"", "+a3"},
80 }, got)
81 assert.Equal(t, DiffLine{Kind: "del", Mark: "-", Old: "2", Segs: []Seg{{Text: "d1"}}, oldN: 2}, ls[1])
82 assert.Equal(t, DiffLine{Kind: "add", Mark: "+", New: "2", Segs: []Seg{{Text: "a1"}}, newN: 2}, ls[4])
83}
84
85func TestChangeBar(t *testing.T) {
86 assert.Equal(t, Bar{Rest: "··········"}, changeBar(0, 0, 0))
87 assert.Equal(t, Bar{Add: "++++++++++"}, changeBar(100, 0, 100))
88 assert.Equal(t, Bar{Add: "+++++", Del: "-----", Rest: ""}, changeBar(50, 50, 100))
89 assert.Equal(t, Bar{Add: "+", Rest: "·········"}, changeBar(1, 0, 1000), "a change is never rounded away")
90 assert.Equal(t, 10, len(changeBar(7, 7, 8).Add+changeBar(7, 7, 8).Del+changeBar(7, 7, 8).Rest), "never wider than the bar")
91}
92
93func TestFileNotes(t *testing.T) {
94 assert.Equal(t, "binary file: no text diff", fileNote(&pb.FileDiff{Binary: true, Status: pb.FileStatus_FILE_STATUS_MODIFIED}))
95 assert.Equal(t, "mode 100644 → 100755", fileNote(&pb.FileDiff{Status: pb.FileStatus_FILE_STATUS_MODIFIED, OldMode: 0o100644, NewMode: 0o100755}))
96 assert.Empty(t, fileNote(&pb.FileDiff{Status: pb.FileStatus_FILE_STATUS_ADDED, NewMode: 0o100644}), "an added file has no mode change")
97 assert.Equal(t, "renamed, 90% similar", fileNote(&pb.FileDiff{Status: pb.FileStatus_FILE_STATUS_RENAMED, Similarity: 90, OldMode: 0o100644, NewMode: 0o100644}))
98 assert.Contains(t, fileNote(&pb.FileDiff{Truncated: true, Additions: 2500}), "too large")
99}
100
101func TestDiffFilesPaths(t *testing.T) {
102 fs := diffFiles([]*pb.FileDiff{
103 {Status: pb.FileStatus_FILE_STATUS_ADDED, NewPath: "a.go", Additions: 1, Hunks: []*pb.Hunk{{Lines: []*pb.Line{line(pb.LineKind_LINE_KIND_ADDITION, 0, 1, "x")}}}},
104 {Status: pb.FileStatus_FILE_STATUS_DELETED, OldPath: "b.go", Deletions: 1},
105 {Status: pb.FileStatus_FILE_STATUS_RENAMED, OldPath: "c\u202e.go", NewPath: "d.go"},
106 {Status: pb.FileStatus_FILE_STATUS_MODIFIED, NewPath: "big.go", Truncated: true, Additions: 3000},
107 })
108 assert.Equal(t, "a.go", fs[0].Path)
109 assert.True(t, fs[0].Open)
110 assert.Equal(t, "b.go", fs[1].Path)
111 assert.False(t, fs[1].Open, "nothing to read: stays closed")
112 assert.Equal(t, "c<U+202E>.go → d.go", fs[2].Path)
113 assert.False(t, fs[3].Open, "a truncated file is collapsed")
114 assert.Equal(t, []string{"f1", "f2", "f3", "f4"}, []string{fs[0].ID, fs[1].ID, fs[2].ID, fs[3].ID})
115}
116
117// A code cell holds exactly the text: escaped, with markers, and nothing
118// added between the pieces (the cell keeps its white space).
119func TestCodeLine(t *testing.T) {
120 var b strings.Builder
121 ln := diffLine(&pb.Line{Kind: pb.LineKind_LINE_KIND_ADDITION, NewNumber: 1, Text: "a <b> \u202e c d", NoNewlineAtEof: true})
122 require.NoError(t, codeLine(ln).Render(context.Background(), &b))
123 assert.Equal(t, `a <b> <span class="inv" title="invisible character"><U+202E></span> c d`+
124 `<span class="eol" title="no newline at end of file">`+noEOL+`</span>`, b.String())
125}