Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testutil: Export ParseTestCaseFile #336

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 102 additions & 13 deletions testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"runtime/debug"
Expand Down Expand Up @@ -62,8 +63,10 @@ type MarkdownTestCaseOptions struct {
Trim bool
}

const attributeSeparator = "//- - - - - - - - -//"
const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
const (
attributeSeparator = "//- - - - - - - - -//"
caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
)

var optionsRegexp *regexp.Regexp = regexp.MustCompile(`(?i)\s*options:(.*)`)

Expand All @@ -84,14 +87,61 @@ func ParseCliCaseArg() []int {
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 fp.Close()
type testCaseParseError struct {
Line int
Err error
}

func (e *testCaseParseError) Error() string {
return fmt.Sprintf("line %v: %v", e.Line, e.Err)
}

func (e *testCaseParseError) Unwrap() error {
return e.Err
}

// ParseTestCases parses test cases from a source in the following format:
//
// NUM[:DESC]
// [OPTIONS]
// //- - - - - - - - -//
// INPUT
// //- - - - - - - - -//
// OUTPUT
// //= = = = = = = = = = = = = = = = = = = = = = = =//
//
// Where,
//
// - NUM is a test case number
// - DESC is an optional description
// - OPTIONS, if present, is a JSON object
// - INPUT is the input Markdown
// - OUTPUT holds the expected result from the processor.
//
// Basic example:
//
// 3
// //- - - - - - - - -//
// Hello, **world**.
// //- - - - - - - - -//
// <p>Hello, <strong>world</strong></p>
// //= = = = = = = = = = = = = = = = = = = = = = = =//
//
// Example of a description:
//
// 3: supports bold text
// //- - - - - - - - -//
// Hello, **world**.
// [..]
//
// Example of options:
//
// 3: supports bold text
// OPTIONS: {"trim": true}
// //- - - - - - - - -//
// Hello, **world**.
// [..]
func ParseTestCases(fp io.Reader) ([]MarkdownTestCase, error) {
scanner := bufio.NewScanner(fp)
c := MarkdownTestCase{
No: -1,
Expand All @@ -102,6 +152,16 @@ func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int)
}
cases := []MarkdownTestCase{}
line := 0

// Builds a testCaseParseError for the curent line.
parseErrorf := func(msg string, args ...interface{}) error {
return &testCaseParseError{
Line: line,
Err: fmt.Errorf(msg, args...),
}
}

var err error
for scanner.Scan() {
line++
if util.IsBlank([]byte(scanner.Text())) {
Expand All @@ -117,23 +177,23 @@ func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int)
c.No, err = strconv.Atoi(scanner.Text())
}
if err != nil {
panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
return nil, parseErrorf("invalid case No: %w", err)
}
if !scanner.Scan() {
panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
return nil, parseErrorf("invalid case: expected content after case No")
}
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))
return nil, parseErrorf("invalid options: %w", err)
}
scanner.Scan()
line++
}
if scanner.Text() != attributeSeparator {
panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
return nil, parseErrorf("invalid separator %q", scanner.Text())
}
buf := []string{}
for scanner.Scan() {
Expand All @@ -158,6 +218,35 @@ func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int)
if len(c.Expected) != 0 {
c.Expected = c.Expected + "\n"
}

cases = append(cases, c)
}

return cases, nil
}

// ParseTestCaseFile reads test cases as described by [ParseTestCases]
// from an external file.
func ParseTestCaseFile(filename string) ([]MarkdownTestCase, error) {
fp, err := os.Open(filename)
if err != nil {
return nil, err
}
defer fp.Close()

return ParseTestCases(fp)
}

// DoTestCaseFile runs test cases in a given file.
func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int) {
allCases, err := ParseTestCaseFile(filename)
if err != nil {
t.Errorf("%v: %v", filename, err)
t.FailNow()
}

cases := allCases[:0]
for _, c := range allCases {
shouldAdd := len(no) == 0
if !shouldAdd {
for _, n := range no {
Expand Down
201 changes: 200 additions & 1 deletion testutil/testutil_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,206 @@
package testutil

import "testing"
import (
"errors"
"os"
"reflect"
"strings"
"testing"
)

// This will fail to compile if the TestingT interface is changed in a way
// that doesn't conform to testing.T.
var _ TestingT = (*testing.T)(nil)

func TestParseTestCases(t *testing.T) {
tests := []struct {
desc string
give string // contents of the test file
want []MarkdownTestCase
}{
{
desc: "empty",
give: "",
want: []MarkdownTestCase{},
},
{
desc: "simple",
give: strings.Join([]string{
"1",
"//- - - - - - - - -//",
"input",
"//- - - - - - - - -//",
"output",
"//= = = = = = = = = = = = = = = = = = = = = = = =//",
}, "\n"),
want: []MarkdownTestCase{
{
No: 1,
Markdown: "input",
Expected: "output\n",
},
},
},
{
desc: "description",
give: strings.Join([]string{
"2:check something",
"//- - - - - - - - -//",
"hello",
"//- - - - - - - - -//",
"<p>hello</p>",
"//= = = = = = = = = = = = = = = = = = = = = = = =//",
}, "\n"),
want: []MarkdownTestCase{
{
No: 2,
Description: "check something",
Markdown: "hello",
Expected: "<p>hello</p>\n",
},
},
},
{
desc: "options",
give: strings.Join([]string{
"3",
`OPTIONS: {"trim": true}`,
"//- - - - - - - - -//",
"world",
"//- - - - - - - - -//",
"<p>world</p>",
"//= = = = = = = = = = = = = = = = = = = = = = = =//",
}, "\n"),
want: []MarkdownTestCase{
{
No: 3,
Options: MarkdownTestCaseOptions{Trim: true},
Markdown: "world",
Expected: "<p>world</p>\n",
},
},
},
}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got, err := ParseTestCases(strings.NewReader(tt.give))
if err != nil {
t.Fatalf("could not parse: %v", err)
}

if !reflect.DeepEqual(tt.want, got) {
t.Errorf("output did not match:")
t.Errorf(" got = %#v", got)
t.Errorf("want = %#v", tt.want)
}
})
}
}

func TestParseTestCases_Errors(t *testing.T) {
tests := []struct {
desc string
give string // contents of the test file
errMsg string
}{
{
desc: "bad number/no description",
give: strings.Join([]string{
"1 not a number",
"//- - - - - - - - -//",
"world",
"//- - - - - - - - -//",
"<p>world</p>",
"//= = = = = = = = = = = = = = = = = = = = = = = =//",
}, "\n"),
errMsg: "line 1: invalid case No",
},
{
desc: "bad number/description",
give: strings.Join([]string{
"1 not a number:description",
"//- - - - - - - - -//",
"world",
"//- - - - - - - - -//",
"<p>world</p>",
"//= = = = = = = = = = = = = = = = = = = = = = = =//",
}, "\n"),
errMsg: "line 1: invalid case No",
},
{
desc: "eof after number",
give: strings.Join([]string{
"1",
}, "\n"),
errMsg: "line 1: invalid case: expected content after",
},
{
desc: "bad options",
give: strings.Join([]string{
"3",
`OPTIONS: {not valid JSON}`,
"//- - - - - - - - -//",
"world",
"//- - - - - - - - -//",
"<p>world</p>",
"//= = = = = = = = = = = = = = = = = = = = = = = =//",
}, "\n"),
errMsg: "line 2: invalid options:",
},
{
desc: "bad separator",
give: strings.Join([]string{
"3",
"// not the right separator //",
}, "\n"),
errMsg: `line 2: invalid separator "// not the right separator //"`,
},
}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cases, err := ParseTestCases(strings.NewReader(tt.give))
if err == nil {
t.Fatalf("expected error, got:\n%#v", cases)
}

if got := err.Error(); !strings.Contains(got, tt.errMsg) {
t.Errorf("unexpected error message:")
t.Errorf(" got = %v", got)
t.Errorf("does not contain = %v", tt.errMsg)
}
})
}
}

func TestParseTestCaseFile_Error(t *testing.T) {
cases, err := ParseTestCaseFile("does_not_exist.txt")
if err == nil {
t.Fatalf("expected error, got:\n%#v", cases)
}

if !errors.Is(err, os.ErrNotExist) {
t.Errorf(" unexpected error = %v", err)
t.Errorf("expected unwrap to = %v", os.ErrNotExist)
}
}

func TestTestCaseParseError(t *testing.T) {
wrapped := errors.New("great sadness")
err := &testCaseParseError{Line: 42, Err: wrapped}

t.Run("Error", func(t *testing.T) {
want := "line 42: great sadness"
got := err.Error()
if want != got {
t.Errorf("Error() = %q, want %q", got, want)
}
})

t.Run("Unwrap", func(t *testing.T) {
if !errors.Is(err, wrapped) {
t.Errorf("error %#v should unwrap to %#v", err, wrapped)
}
})
}