Skip to content
Merged
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
206 changes: 0 additions & 206 deletions api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,12 @@
package api

import (
"bufio"
"errors"
"fmt"
"os"
"strings"
"time"

"gopkg.in/yaml.v3"
)

var (
// ErrParse indicates a YAML definition is not valid
ErrParse = errors.New("parse error")
// ErrUnknownField indicates that there was an unknown field in the parsing
// of a spec or scenario.
ErrUnknownField = errors.New("unknown field")
Expand Down Expand Up @@ -104,217 +97,18 @@ func UnexpectedError(err error) error {
return fmt.Errorf("%w: %s", ErrUnexpectedError, err)
}

// ParseError is a custom error type that stores the location of an error that
// occurred while parsing a gdt test specification.
type ParseError struct {
// Path is the filepath to the parsed document.
Path string
// Line is the line number where the parse error occurred.
Line int
// Column is the column number where the parse error occurred.
Column int
// Message is the error message.
Message string
// Contents is the contents of the file read at Path.
Contents string
}

// Error implements the error interface for ParseError.
func (e *ParseError) Error() string {
contents := ""
if e.Contents != "" {
contents = fmt.Sprintf("\n%s\n", e.Contents)
}
return fmt.Sprintf(
"error parsing %q: at line %d, column %d:\n%s\n%s",
e.Path, e.Line, e.Column, e.Message, contents,
)
}

// SetContents adds the detail to the error message for surrounding contents if
// the Path, Line and Column is set.
func (e *ParseError) SetContents() {
if e.Path != "" {
f, err := os.Open(e.Path)
if err != nil {
// just ignore...
return
}
defer f.Close()

b := &strings.Builder{}
viewStartLine := max(0, e.Line-2)
viewEndLine := e.Line + 2

sc := bufio.NewScanner(f)
x := 0
for sc.Scan() {
x++
line := sc.Text()
if x > viewEndLine {
break
}
if x < viewStartLine {
continue
}
_, _ = fmt.Fprintf(b, "%03d: %s\n", x, line)
if x == e.Line {
_, _ = fmt.Fprintf(b, " %s^", strings.Repeat(" ", e.Column))
}
}
if err := sc.Err(); err != nil {
// just ignore...
return
}
e.Contents = b.String()
}
}

func (e *ParseError) Unwrap() error {
return ErrParse
}

var (
// ErrUnknownSourceType indicates that a From() function was called with an
// unknown source parameter type.
ErrUnknownSourceType = errors.New("unknown source argument type")
)

// UnknownSpecAt returns an ErrUnknownSpec with the line/column of the supplied
// YAML node.
func UnknownSpecAt(path string, node *yaml.Node) error {
return &ParseError{
Path: path,
Line: node.Line,
Column: node.Column,
Message: "no plugin could parse spec definition",
}
}

// UnknownFieldAt returns an ErrUnknownField for a supplied field annotated
// with the line/column of the supplied YAML node.
func UnknownFieldAt(field string, node *yaml.Node) error {
return fmt.Errorf(
"%w: %q at line %d, column %d",
ErrUnknownField, field, node.Line, node.Column,
)
}

// ExpectedMapAt returns an ErrExpectedMap error annotated with the
// line/column of the supplied YAML node.
func ExpectedMapAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected map field",
}
}

// ExpectedScalarAt returns an ErrExpectedScalar error annotated with
// the line/column of the supplied YAML node.
func ExpectedScalarAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected scalar field",
}
}

// ExpectedSequenceAt returns an ErrExpectedSequence error annotated
// with the line/column of the supplied YAML node.
func ExpectedSequenceAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected sequence field",
}
}

// ExpectedIntAt returns an ErrExpectedInt error annotated
// with the line/column of the supplied YAML node.
func ExpectedIntAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected int value",
}
}

// ExpectedScalarOrSequenceAt returns an ErrExpectedScalarOrSequence error
// annotated with the line/column of the supplied YAML node.
func ExpectedScalarOrSequenceAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected scalar or sequence of scalars field",
}
}

// ExpectedScalarOrMapAt returns an ErrExpectedScalarOrMap error annotated with
// the line/column of the supplied YAML node.
func ExpectedScalarOrMapAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected scalar or map field",
}
}

// ExpectedTimeoutAt returns an ErrExpectedTimeout error annotated
// with the line/column of the supplied YAML node.
func ExpectedTimeoutAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected timeout specification",
}
}

// ExpectedWaitAt returns an ErrExpectedWait error annotated with the
// line/column of the supplied YAML node.
func ExpectedWaitAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected wait specification",
}
}

// ExpectedRetryAt returns an ErrExpectedRetry error annotated with the
// line/column of the supplied YAML node.
func ExpectedRetryAt(node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: "expected retry specification",
}
}

// InvalidRetryAttempts returns an ErrInvalidRetryAttempts error annotated with
// the line/column of the supplied YAML node.
func InvalidRetryAttempts(node *yaml.Node, attempts int) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: fmt.Sprintf("invalid retry attempts: %d", attempts),
}
}

// UnknownSourceType returns an ErrUnknownSourceType error describing the
// supplied parameter type.
func UnknownSourceType(source interface{}) error {
return fmt.Errorf("%w: %T", ErrUnknownSourceType, source)
}

// FileNotFound returns ErrFileNotFound for a given file path
func FileNotFound(path string, node *yaml.Node) error {
return &ParseError{
Line: node.Line,
Column: node.Column,
Message: fmt.Sprintf("file not found: %q", path),
}
}

var (
// RuntimeError is the base error class for all errors occurring during
// runtime (and not during the parsing of a scenario or spec)
Expand Down
6 changes: 4 additions & 2 deletions api/flexstrings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package api

import (
"gopkg.in/yaml.v3"

"github.com/gdt-dev/core/parse"
)

// FlexStrings is a struct used to parse an interface{} that can be either a
Expand All @@ -23,7 +25,7 @@ func (f *FlexStrings) Values() []string {
// FlexStrings can be either a string or a slice of strings.
func (f *FlexStrings) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.ScalarNode && node.Kind != yaml.SequenceNode {
return ExpectedScalarOrSequenceAt(node)
return parse.ExpectedScalarOrSequenceAt(node)
}
var single string
if err := node.Decode(&single); err == nil {
Expand All @@ -35,5 +37,5 @@ func (f *FlexStrings) UnmarshalYAML(node *yaml.Node) error {
f.values = many
return nil
}
return ExpectedScalarOrSequenceAt(node)
return parse.ExpectedScalarOrSequenceAt(node)
}
3 changes: 2 additions & 1 deletion api/flexstrings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/gdt-dev/core/api"
"github.com/gdt-dev/core/parse"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
Expand All @@ -30,7 +31,7 @@ func TestFlexStringsError(t *testing.T) {
err := yaml.Unmarshal(contents, &f)

require.NotNil(err)
assert.ErrorIs(err, api.ErrParse)
assert.Error(err, &parse.Error{})
}

func TestFlexStrings(t *testing.T) {
Expand Down
24 changes: 13 additions & 11 deletions api/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"time"

"gopkg.in/yaml.v3"

"github.com/gdt-dev/core/parse"
)

var (
Expand Down Expand Up @@ -83,26 +85,26 @@ func slugify(s string) string {
// the associated struct field from that value node.
func (s *Spec) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return ExpectedMapAt(node)
return parse.ExpectedMapAt(node)
}
// maps/structs are stored in a top-level Node.Content field which is a
// concatenated slice of Node pointers in pairs of key/values.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
if keyNode.Kind != yaml.ScalarNode {
return ExpectedScalarAt(keyNode)
return parse.ExpectedScalarAt(keyNode)
}
key := keyNode.Value
valNode := node.Content[i+1]
switch key {
case "name":
if valNode.Kind != yaml.ScalarNode {
return ExpectedScalarAt(valNode)
return parse.ExpectedScalarAt(valNode)
}
s.Name = valNode.Value
case "description":
if valNode.Kind != yaml.ScalarNode {
return ExpectedScalarAt(valNode)
return parse.ExpectedScalarAt(valNode)
}
s.Description = valNode.Value
case "timeout":
Expand All @@ -111,15 +113,15 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode:
// We support the old-style timeout:after
if err := valNode.Decode(&to); err != nil {
return ExpectedTimeoutAt(valNode)
return parse.ExpectedTimeoutAt(valNode)
}
case yaml.ScalarNode:
// We also support a straight string duration
to = &Timeout{
After: valNode.Value,
}
default:
return ExpectedScalarOrMapAt(valNode)
return parse.ExpectedScalarOrMapAt(valNode)
}
_, err := time.ParseDuration(to.After)
if err != nil {
Expand All @@ -128,11 +130,11 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error {
s.Timeout = to
case "wait":
if valNode.Kind != yaml.MappingNode {
return ExpectedMapAt(valNode)
return parse.ExpectedMapAt(valNode)
}
var w *Wait
if err := valNode.Decode(&w); err != nil {
return ExpectedWaitAt(valNode)
return parse.ExpectedWaitAt(valNode)
}
if w.Before != "" {
_, err := time.ParseDuration(w.Before)
Expand All @@ -149,16 +151,16 @@ func (s *Spec) UnmarshalYAML(node *yaml.Node) error {
s.Wait = w
case "retry":
if valNode.Kind != yaml.MappingNode {
return ExpectedMapAt(valNode)
return parse.ExpectedMapAt(valNode)
}
var r *Retry
if err := valNode.Decode(&r); err != nil {
return ExpectedRetryAt(valNode)
return parse.ExpectedRetryAt(valNode)
}
if r.Attempts != nil {
attempts := *r.Attempts
if attempts < 1 {
return InvalidRetryAttempts(valNode, attempts)
return parse.InvalidRetryAttempts(valNode, attempts)
}
}
if r.Interval != "" {
Expand Down
Loading
Loading