...

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

Documentation: github.com/yuin/goldmark/testutil

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