Merge commit 'ad273dbe5dba7bd0e901270464e25fc1f030a5b5' as 'backend/goldmark'
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
// Package testutil provides utilities for unit tests.
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// TestingT is a subset of the functionality provided by testing.T.
|
||||
type TestingT interface {
|
||||
Logf(string, ...any)
|
||||
Skipf(string, ...any)
|
||||
Errorf(string, ...any)
|
||||
FailNow()
|
||||
}
|
||||
|
||||
// MarkdownTestCase represents a test case.
|
||||
type MarkdownTestCase struct {
|
||||
No int
|
||||
Description string
|
||||
Options MarkdownTestCaseOptions
|
||||
Markdown string
|
||||
Expected string
|
||||
}
|
||||
|
||||
func source(t *MarkdownTestCase) string {
|
||||
ret := t.Markdown
|
||||
if t.Options.Trim {
|
||||
ret = strings.TrimSpace(ret)
|
||||
}
|
||||
if t.Options.EnableEscape {
|
||||
return string(applyEscapeSequence([]byte(ret)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func expected(t *MarkdownTestCase) string {
|
||||
ret := t.Expected
|
||||
if t.Options.Trim {
|
||||
ret = strings.TrimSpace(ret)
|
||||
}
|
||||
if t.Options.EnableEscape {
|
||||
return string(applyEscapeSequence([]byte(ret)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// MarkdownTestCaseOptions represents options for each test case.
|
||||
type MarkdownTestCaseOptions struct {
|
||||
EnableEscape bool
|
||||
Trim bool
|
||||
}
|
||||
|
||||
const attributeSeparator = "//- - - - - - - - -//"
|
||||
const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
|
||||
|
||||
var optionsRegexp = regexp.MustCompile(`(?i)\s*options:(.*)`)
|
||||
|
||||
// ParseCliCaseArg parses -case command line args.
|
||||
func ParseCliCaseArg() []int {
|
||||
ret := []int{}
|
||||
for _, a := range os.Args {
|
||||
if strings.HasPrefix(a, "case=") {
|
||||
parts := strings.Split(a, "=")
|
||||
for _, cas := range strings.Split(parts[1], ",") {
|
||||
value, err := strconv.Atoi(strings.TrimSpace(cas))
|
||||
if err == nil {
|
||||
ret = append(ret, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// DoTestCaseFile runs test cases in a given file.
|
||||
func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int) {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(fp)
|
||||
c := MarkdownTestCase{
|
||||
No: -1,
|
||||
Description: "",
|
||||
Options: MarkdownTestCaseOptions{},
|
||||
Markdown: "",
|
||||
Expected: "",
|
||||
}
|
||||
cases := []MarkdownTestCase{}
|
||||
line := 0
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
if util.IsBlank([]byte(scanner.Text())) {
|
||||
continue
|
||||
}
|
||||
header := scanner.Text()
|
||||
c.Description = ""
|
||||
if strings.Contains(header, ":") {
|
||||
parts := strings.Split(header, ":")
|
||||
c.No, err = strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
c.Description = strings.Join(parts[1:], ":")
|
||||
} else {
|
||||
c.No, err = strconv.Atoi(scanner.Text())
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
|
||||
}
|
||||
if !scanner.Scan() {
|
||||
panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
|
||||
}
|
||||
line++
|
||||
matches := optionsRegexp.FindAllStringSubmatch(scanner.Text(), -1)
|
||||
if len(matches) != 0 {
|
||||
err = json.Unmarshal([]byte(matches[0][1]), &c.Options)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: invalid options at line %d", filename, line))
|
||||
}
|
||||
scanner.Scan()
|
||||
line++
|
||||
}
|
||||
if scanner.Text() != attributeSeparator {
|
||||
panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
|
||||
}
|
||||
buf := []string{}
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
text := scanner.Text()
|
||||
if text == attributeSeparator {
|
||||
break
|
||||
}
|
||||
buf = append(buf, text)
|
||||
}
|
||||
c.Markdown = strings.Join(buf, "\n")
|
||||
buf = []string{}
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
text := scanner.Text()
|
||||
if text == caseSeparator {
|
||||
break
|
||||
}
|
||||
buf = append(buf, text)
|
||||
}
|
||||
c.Expected = strings.Join(buf, "\n")
|
||||
if len(c.Expected) != 0 {
|
||||
c.Expected = c.Expected + "\n"
|
||||
}
|
||||
shouldAdd := len(no) == 0
|
||||
if !shouldAdd {
|
||||
shouldAdd = slices.Contains(no, c.No)
|
||||
}
|
||||
if shouldAdd {
|
||||
cases = append(cases, c)
|
||||
}
|
||||
}
|
||||
DoTestCases(m, cases, t)
|
||||
}
|
||||
|
||||
// DoTestCases runs a set of test cases.
|
||||
func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
|
||||
for _, testCase := range cases {
|
||||
DoTestCase(m, testCase, t, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// DoTestCase runs a test case.
|
||||
func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
|
||||
var ok bool
|
||||
var out bytes.Buffer
|
||||
defer func() {
|
||||
description := ""
|
||||
if len(testCase.Description) != 0 {
|
||||
description = ": " + testCase.Description
|
||||
}
|
||||
if err := recover(); err != nil {
|
||||
format := `============= case %d%s ================
|
||||
Markdown:
|
||||
-----------
|
||||
%s
|
||||
|
||||
Expected:
|
||||
----------
|
||||
%s
|
||||
|
||||
Actual
|
||||
---------
|
||||
%v
|
||||
%s
|
||||
`
|
||||
t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), err, debug.Stack())
|
||||
} else if !ok {
|
||||
format := `============= case %d%s ================
|
||||
Markdown:
|
||||
-----------
|
||||
%s
|
||||
|
||||
Expected:
|
||||
----------
|
||||
%s
|
||||
|
||||
Actual
|
||||
---------
|
||||
%s
|
||||
|
||||
Diff
|
||||
---------
|
||||
%s
|
||||
`
|
||||
t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), out.Bytes(),
|
||||
DiffPretty([]byte(expected(&testCase)), out.Bytes()))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := m.Convert([]byte(source(&testCase)), &out, opts...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(expected(&testCase))))
|
||||
}
|
||||
|
||||
type diffType int
|
||||
|
||||
const (
|
||||
diffRemoved diffType = iota
|
||||
diffAdded
|
||||
diffNone
|
||||
)
|
||||
|
||||
type diff struct {
|
||||
Type diffType
|
||||
Lines [][]byte
|
||||
}
|
||||
|
||||
func simpleDiff(v1, v2 []byte) []diff {
|
||||
return simpleDiffAux(
|
||||
bytes.Split(v1, []byte("\n")),
|
||||
bytes.Split(v2, []byte("\n")))
|
||||
}
|
||||
|
||||
func simpleDiffAux(v1lines, v2lines [][]byte) []diff {
|
||||
v1index := map[string][]int{}
|
||||
for i, line := range v1lines {
|
||||
key := util.BytesToReadOnlyString(line)
|
||||
if _, ok := v1index[key]; !ok {
|
||||
v1index[key] = []int{}
|
||||
}
|
||||
v1index[key] = append(v1index[key], i)
|
||||
}
|
||||
overlap := map[int]int{}
|
||||
v1start := 0
|
||||
v2start := 0
|
||||
length := 0
|
||||
for v2pos, line := range v2lines {
|
||||
newOverlap := map[int]int{}
|
||||
key := util.BytesToReadOnlyString(line)
|
||||
if _, ok := v1index[key]; !ok {
|
||||
v1index[key] = []int{}
|
||||
}
|
||||
for _, v1pos := range v1index[key] {
|
||||
value := 0
|
||||
if v1pos != 0 {
|
||||
if v, ok := overlap[v1pos-1]; ok {
|
||||
value = v
|
||||
}
|
||||
}
|
||||
newOverlap[v1pos] = value + 1
|
||||
if newOverlap[v1pos] > length {
|
||||
length = newOverlap[v1pos]
|
||||
v1start = v1pos - length + 1
|
||||
v2start = v2pos - length + 1
|
||||
}
|
||||
}
|
||||
overlap = newOverlap
|
||||
}
|
||||
if length == 0 {
|
||||
diffs := []diff{}
|
||||
if len(v1lines) != 0 {
|
||||
diffs = append(diffs, diff{diffRemoved, v1lines})
|
||||
}
|
||||
if len(v2lines) != 0 {
|
||||
diffs = append(diffs, diff{diffAdded, v2lines})
|
||||
}
|
||||
return diffs
|
||||
}
|
||||
diffs := simpleDiffAux(v1lines[:v1start], v2lines[:v2start])
|
||||
diffs = append(diffs, diff{diffNone, v2lines[v2start : v2start+length]})
|
||||
diffs = append(diffs, simpleDiffAux(v1lines[v1start+length:],
|
||||
v2lines[v2start+length:])...)
|
||||
return diffs
|
||||
}
|
||||
|
||||
// DiffPretty returns pretty formatted diff between given bytes.
|
||||
func DiffPretty(v1, v2 []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
diffs := simpleDiff(v1, v2)
|
||||
for _, diff := range diffs {
|
||||
c := " "
|
||||
switch diff.Type {
|
||||
case diffAdded:
|
||||
c = "+"
|
||||
case diffRemoved:
|
||||
c = "-"
|
||||
case diffNone:
|
||||
c = " "
|
||||
}
|
||||
for _, line := range diff.Lines {
|
||||
if c != " " {
|
||||
fmt.Fprintf(&b, "%s | %s\n", c, util.VisualizeSpaces(line))
|
||||
} else {
|
||||
fmt.Fprintf(&b, "%s | %s\n", c, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func applyEscapeSequence(b []byte) []byte {
|
||||
result := make([]byte, 0, len(b))
|
||||
for i := 0; i < len(b); i++ {
|
||||
if b[i] == '\\' && i != len(b)-1 {
|
||||
switch b[i+1] {
|
||||
case 'a':
|
||||
result = append(result, '\a')
|
||||
i++
|
||||
continue
|
||||
case 'b':
|
||||
result = append(result, '\b')
|
||||
i++
|
||||
continue
|
||||
case 'f':
|
||||
result = append(result, '\f')
|
||||
i++
|
||||
continue
|
||||
case 'n':
|
||||
result = append(result, '\n')
|
||||
i++
|
||||
continue
|
||||
case 'r':
|
||||
result = append(result, '\r')
|
||||
i++
|
||||
continue
|
||||
case 't':
|
||||
result = append(result, '\t')
|
||||
i++
|
||||
continue
|
||||
case 'v':
|
||||
result = append(result, '\v')
|
||||
i++
|
||||
continue
|
||||
case '\\':
|
||||
result = append(result, '\\')
|
||||
i++
|
||||
continue
|
||||
case 'x':
|
||||
if len(b) >= i+3 && util.IsHexDecimal(b[i+2]) && util.IsHexDecimal(b[i+3]) {
|
||||
v, _ := hex.DecodeString(string(b[i+2 : i+4]))
|
||||
result = append(result, v[0])
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
case 'u', 'U':
|
||||
if len(b) > i+2 {
|
||||
num := []byte{}
|
||||
for j := i + 2; j < len(b); j++ {
|
||||
if util.IsHexDecimal(b[j]) {
|
||||
num = append(num, b[j])
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(num) >= 4 && len(num) < 8 {
|
||||
v, _ := strconv.ParseInt(string(num[:4]), 16, 32)
|
||||
result = append(result, []byte(string(rune(v)))...)
|
||||
i += 5
|
||||
continue
|
||||
}
|
||||
if len(num) >= 8 {
|
||||
v, _ := strconv.ParseInt(string(num[:8]), 16, 32)
|
||||
result = append(result, []byte(string(rune(v)))...)
|
||||
i += 9
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, b[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user