1
2 package testutil
3
4 import (
5 "bufio"
6 "bytes"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "os"
11 "regexp"
12 "runtime/debug"
13 "strconv"
14 "strings"
15
16 "github.com/yuin/goldmark"
17 "github.com/yuin/goldmark/parser"
18 "github.com/yuin/goldmark/util"
19 )
20
21
22 type TestingT interface {
23 Logf(string, ...interface{})
24 Skipf(string, ...interface{})
25 Errorf(string, ...interface{})
26 FailNow()
27 }
28
29
30 type MarkdownTestCase struct {
31 No int
32 Description string
33 Options MarkdownTestCaseOptions
34 Markdown string
35 Expected string
36 }
37
38 func source(t *MarkdownTestCase) string {
39 ret := t.Markdown
40 if t.Options.Trim {
41 ret = strings.TrimSpace(ret)
42 }
43 if t.Options.EnableEscape {
44 return string(applyEscapeSequence([]byte(ret)))
45 }
46 return ret
47 }
48
49 func expected(t *MarkdownTestCase) string {
50 ret := t.Expected
51 if t.Options.Trim {
52 ret = strings.TrimSpace(ret)
53 }
54 if t.Options.EnableEscape {
55 return string(applyEscapeSequence([]byte(ret)))
56 }
57 return ret
58 }
59
60
61 type MarkdownTestCaseOptions struct {
62 EnableEscape bool
63 Trim bool
64 }
65
66 const attributeSeparator = "//- - - - - - - - -//"
67 const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
68
69 var optionsRegexp = regexp.MustCompile(`(?i)\s*options:(.*)`)
70
71
72 func ParseCliCaseArg() []int {
73 ret := []int{}
74 for _, a := range os.Args {
75 if strings.HasPrefix(a, "case=") {
76 parts := strings.Split(a, "=")
77 for _, cas := range strings.Split(parts[1], ",") {
78 value, err := strconv.Atoi(strings.TrimSpace(cas))
79 if err == nil {
80 ret = append(ret, value)
81 }
82 }
83 }
84 }
85 return ret
86 }
87
88
89 func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int) {
90 fp, err := os.Open(filename)
91 if err != nil {
92 panic(err)
93 }
94 defer func() {
95 _ = fp.Close()
96 }()
97
98 scanner := bufio.NewScanner(fp)
99 c := MarkdownTestCase{
100 No: -1,
101 Description: "",
102 Options: MarkdownTestCaseOptions{},
103 Markdown: "",
104 Expected: "",
105 }
106 cases := []MarkdownTestCase{}
107 line := 0
108 for scanner.Scan() {
109 line++
110 if util.IsBlank([]byte(scanner.Text())) {
111 continue
112 }
113 header := scanner.Text()
114 c.Description = ""
115 if strings.Contains(header, ":") {
116 parts := strings.Split(header, ":")
117 c.No, err = strconv.Atoi(strings.TrimSpace(parts[0]))
118 c.Description = strings.Join(parts[1:], ":")
119 } else {
120 c.No, err = strconv.Atoi(scanner.Text())
121 }
122 if err != nil {
123 panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
124 }
125 if !scanner.Scan() {
126 panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
127 }
128 line++
129 matches := optionsRegexp.FindAllStringSubmatch(scanner.Text(), -1)
130 if len(matches) != 0 {
131 err = json.Unmarshal([]byte(matches[0][1]), &c.Options)
132 if err != nil {
133 panic(fmt.Sprintf("%s: invalid options at line %d", filename, line))
134 }
135 scanner.Scan()
136 line++
137 }
138 if scanner.Text() != attributeSeparator {
139 panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
140 }
141 buf := []string{}
142 for scanner.Scan() {
143 line++
144 text := scanner.Text()
145 if text == attributeSeparator {
146 break
147 }
148 buf = append(buf, text)
149 }
150 c.Markdown = strings.Join(buf, "\n")
151 buf = []string{}
152 for scanner.Scan() {
153 line++
154 text := scanner.Text()
155 if text == caseSeparator {
156 break
157 }
158 buf = append(buf, text)
159 }
160 c.Expected = strings.Join(buf, "\n")
161 if len(c.Expected) != 0 {
162 c.Expected = c.Expected + "\n"
163 }
164 shouldAdd := len(no) == 0
165 if !shouldAdd {
166 for _, n := range no {
167 if n == c.No {
168 shouldAdd = true
169 break
170 }
171 }
172 }
173 if shouldAdd {
174 cases = append(cases, c)
175 }
176 }
177 DoTestCases(m, cases, t)
178 }
179
180
181 func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
182 for _, testCase := range cases {
183 DoTestCase(m, testCase, t, opts...)
184 }
185 }
186
187
188 func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
189 var ok bool
190 var out bytes.Buffer
191 defer func() {
192 description := ""
193 if len(testCase.Description) != 0 {
194 description = ": " + testCase.Description
195 }
196 if err := recover(); err != nil {
197 format := `============= case %d%s ================
198 Markdown:
199 -----------
200 %s
201
202 Expected:
203 ----------
204 %s
205
206 Actual
207 ---------
208 %v
209 %s
210 `
211 t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), err, debug.Stack())
212 } else if !ok {
213 format := `============= case %d%s ================
214 Markdown:
215 -----------
216 %s
217
218 Expected:
219 ----------
220 %s
221
222 Actual
223 ---------
224 %s
225
226 Diff
227 ---------
228 %s
229 `
230 t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), out.Bytes(),
231 DiffPretty([]byte(expected(&testCase)), out.Bytes()))
232 }
233 }()
234
235 if err := m.Convert([]byte(source(&testCase)), &out, opts...); err != nil {
236 panic(err)
237 }
238 ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(expected(&testCase))))
239 }
240
241 type diffType int
242
243 const (
244 diffRemoved diffType = iota
245 diffAdded
246 diffNone
247 )
248
249 type diff struct {
250 Type diffType
251 Lines [][]byte
252 }
253
254 func simpleDiff(v1, v2 []byte) []diff {
255 return simpleDiffAux(
256 bytes.Split(v1, []byte("\n")),
257 bytes.Split(v2, []byte("\n")))
258 }
259
260 func simpleDiffAux(v1lines, v2lines [][]byte) []diff {
261 v1index := map[string][]int{}
262 for i, line := range v1lines {
263 key := util.BytesToReadOnlyString(line)
264 if _, ok := v1index[key]; !ok {
265 v1index[key] = []int{}
266 }
267 v1index[key] = append(v1index[key], i)
268 }
269 overlap := map[int]int{}
270 v1start := 0
271 v2start := 0
272 length := 0
273 for v2pos, line := range v2lines {
274 newOverlap := map[int]int{}
275 key := util.BytesToReadOnlyString(line)
276 if _, ok := v1index[key]; !ok {
277 v1index[key] = []int{}
278 }
279 for _, v1pos := range v1index[key] {
280 value := 0
281 if v1pos != 0 {
282 if v, ok := overlap[v1pos-1]; ok {
283 value = v
284 }
285 }
286 newOverlap[v1pos] = value + 1
287 if newOverlap[v1pos] > length {
288 length = newOverlap[v1pos]
289 v1start = v1pos - length + 1
290 v2start = v2pos - length + 1
291 }
292 }
293 overlap = newOverlap
294 }
295 if length == 0 {
296 diffs := []diff{}
297 if len(v1lines) != 0 {
298 diffs = append(diffs, diff{diffRemoved, v1lines})
299 }
300 if len(v2lines) != 0 {
301 diffs = append(diffs, diff{diffAdded, v2lines})
302 }
303 return diffs
304 }
305 diffs := simpleDiffAux(v1lines[:v1start], v2lines[:v2start])
306 diffs = append(diffs, diff{diffNone, v2lines[v2start : v2start+length]})
307 diffs = append(diffs, simpleDiffAux(v1lines[v1start+length:],
308 v2lines[v2start+length:])...)
309 return diffs
310 }
311
312
313 func DiffPretty(v1, v2 []byte) []byte {
314 var b bytes.Buffer
315 diffs := simpleDiff(v1, v2)
316 for _, diff := range diffs {
317 c := " "
318 switch diff.Type {
319 case diffAdded:
320 c = "+"
321 case diffRemoved:
322 c = "-"
323 case diffNone:
324 c = " "
325 }
326 for _, line := range diff.Lines {
327 if c != " " {
328 b.WriteString(fmt.Sprintf("%s | %s\n", c, util.VisualizeSpaces(line)))
329 } else {
330 b.WriteString(fmt.Sprintf("%s | %s\n", c, line))
331 }
332 }
333 }
334 return b.Bytes()
335 }
336
337 func applyEscapeSequence(b []byte) []byte {
338 result := make([]byte, 0, len(b))
339 for i := 0; i < len(b); i++ {
340 if b[i] == '\\' && i != len(b)-1 {
341 switch b[i+1] {
342 case 'a':
343 result = append(result, '\a')
344 i++
345 continue
346 case 'b':
347 result = append(result, '\b')
348 i++
349 continue
350 case 'f':
351 result = append(result, '\f')
352 i++
353 continue
354 case 'n':
355 result = append(result, '\n')
356 i++
357 continue
358 case 'r':
359 result = append(result, '\r')
360 i++
361 continue
362 case 't':
363 result = append(result, '\t')
364 i++
365 continue
366 case 'v':
367 result = append(result, '\v')
368 i++
369 continue
370 case '\\':
371 result = append(result, '\\')
372 i++
373 continue
374 case 'x':
375 if len(b) >= i+3 && util.IsHexDecimal(b[i+2]) && util.IsHexDecimal(b[i+3]) {
376 v, _ := hex.DecodeString(string(b[i+2 : i+4]))
377 result = append(result, v[0])
378 i += 3
379 continue
380 }
381 case 'u', 'U':
382 if len(b) > i+2 {
383 num := []byte{}
384 for j := i + 2; j < len(b); j++ {
385 if util.IsHexDecimal(b[j]) {
386 num = append(num, b[j])
387 continue
388 }
389 break
390 }
391 if len(num) >= 4 && len(num) < 8 {
392 v, _ := strconv.ParseInt(string(num[:4]), 16, 32)
393 result = append(result, []byte(string(rune(v)))...)
394 i += 5
395 continue
396 }
397 if len(num) >= 8 {
398 v, _ := strconv.ParseInt(string(num[:8]), 16, 32)
399 result = append(result, []byte(string(rune(v)))...)
400 i += 9
401 continue
402 }
403 }
404 }
405 }
406 result = append(result, b[i])
407 }
408 return result
409 }
410
View as plain text