diff --git a/testutil/testutil.go b/testutil/testutil.go index d343639..4e529f3 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -86,6 +86,19 @@ func ParseCliCaseArg() []int { return ret } +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 +} + // ParseTestCaseFile parses the contents of the given test case file // and reurns the test cases found inside. // @@ -133,7 +146,7 @@ func ParseCliCaseArg() []int { func ParseTestCaseFile(filename string) ([]MarkdownTestCase, error) { fp, err := os.Open(filename) if err != nil { - panic(err) + return nil, err } defer fp.Close() @@ -147,6 +160,15 @@ func ParseTestCaseFile(filename string) ([]MarkdownTestCase, error) { } 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...), + } + } + for scanner.Scan() { line++ if util.IsBlank([]byte(scanner.Text())) { @@ -162,23 +184,23 @@ func ParseTestCaseFile(filename string) ([]MarkdownTestCase, error) { 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() { diff --git a/testutil/testutil_test.go b/testutil/testutil_test.go index 87e0975..541a993 100644 --- a/testutil/testutil_test.go +++ b/testutil/testutil_test.go @@ -1,6 +1,7 @@ package testutil import ( + "errors" "os" "path/filepath" "reflect" @@ -102,3 +103,103 @@ func TestParseTestCaseFile(t *testing.T) { }) } } + +func TestParseTestCaseFile_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", + "//- - - - - - - - -//", + "
world
", + "//= = = = = = = = = = = = = = = = = = = = = = = =//", + }, "\n"), + errMsg: "line 1: invalid case No", + }, + { + desc: "bad number/description", + give: strings.Join([]string{ + "1 not a number:description", + "//- - - - - - - - -//", + "world", + "//- - - - - - - - -//", + "world
", + "//= = = = = = = = = = = = = = = = = = = = = = = =//", + }, "\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", + "//- - - - - - - - -//", + "world
", + "//= = = = = = = = = = = = = = = = = = = = = = = =//", + }, "\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) { + filename := filepath.Join(t.TempDir(), "give.txt") + if err := os.WriteFile(filename, []byte(tt.give), 0o644); err != nil { + t.Fatal(err) + } + + cases, err := ParseTestCaseFile(filename) + 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 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) + } + }) +}