...

Source file src/github.com/yuin/goldmark/testutil/testutil.go

Documentation: github.com/yuin/goldmark/testutil

     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  // TestingT is a subset of the functionality provided by testing.T.
    21  type TestingT interface {
    22  	Logf(string, ...interface{})
    23  	Skipf(string, ...interface{})
    24  	Errorf(string, ...interface{})
    25  	FailNow()
    26  }
    27  
    28  // MarkdownTestCase represents a test case.
    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  // MarkdownTestCaseOptions represents options for each test case.
    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  // ParseCliCaseArg parses -case command line args.
    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  // DoTestCaseFile runs test cases in a given file.
    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  // DoTestCases runs a set of test cases.
   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  // DoTestCase runs a test case.
   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  // DiffPretty returns pretty formatted diff between given bytes.
   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