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