diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d9dee8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Stack trace implementation for Go \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fce4d29 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/acronis/go-stacktrace + +go 1.22.6 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/sprint.go b/sprint.go new file mode 100644 index 0000000..862013a --- /dev/null +++ b/sprint.go @@ -0,0 +1,123 @@ +package stacktrace + +import "fmt" + +const ( + DefaultMessageDelimiter = " * " + DefaultTraceDelimiter = "\n\n" + DefaultStackDelimiter = "\n |_ " + DefaultEnsureDuplicates = false +) + +type SprintOpt interface { + Apply(o *SprintOptions) +} + +type SprintOptions struct { + // MessageDelimiter is a delimiter between message and stack trace + MessageDelimiter string + // TraceDelimiter is a delimiter between stack traces + TraceDelimiter string + // StackDelimiter is a delimiter between stack trace elements + StackDelimiter string + // EnsureDuplicates ensures that duplicates are not printed + EnsureDuplicates bool + dups map[string]struct{} +} + +func NewSprintOptions() *SprintOptions { + opts := &SprintOptions{ + EnsureDuplicates: DefaultEnsureDuplicates, + dups: make(map[string]struct{}), + TraceDelimiter: DefaultTraceDelimiter, + MessageDelimiter: DefaultMessageDelimiter, + StackDelimiter: DefaultStackDelimiter, + } + return opts +} + +type messageDelimiterOpt string + +func (v messageDelimiterOpt) Apply(o *SprintOptions) { + o.MessageDelimiter = string(v) +} + +type traceDelimiterOpt string + +func (v traceDelimiterOpt) Apply(o *SprintOptions) { + o.TraceDelimiter = string(v) +} + +type stackDelimiterOpt string + +func (v stackDelimiterOpt) Apply(o *SprintOptions) { + o.StackDelimiter = string(v) +} + +type ensureDuplicatesOpt struct{} + +func (ensureDuplicatesOpt) Apply(o *SprintOptions) { + o.EnsureDuplicates = true +} + +func WithMessageDelimiter(delimiter string) SprintOpt { + return messageDelimiterOpt(delimiter) +} + +func WithTraceDelimiter(delimiter string) SprintOpt { + return traceDelimiterOpt(delimiter) +} + +func WithStackDelimiter(delimiter string) SprintOpt { + return stackDelimiterOpt(delimiter) +} + +func WithEnsureDuplicates() SprintOpt { + return &ensureDuplicatesOpt{} +} + +func (st *StackTrace) sprint(opts *SprintOptions) string { + trace := st.Header() + trace = fmt.Sprintf("%s%s%s", trace, opts.MessageDelimiter, st.FullMessageWithInfo()) + + listTraces := "" + for _, elem := range st.List { + elemStr := elem.sprint(opts) + if elemStr == "" { + continue + } + if listTraces != "" { + listTraces = fmt.Sprintf("%s%s%s", listTraces, opts.TraceDelimiter, elemStr) + } else { + listTraces = elemStr + } + } + + if _, ok := opts.dups[trace]; ok { + return listTraces + } + + if st.Wrapped != nil { + wrappedStr := st.Wrapped.sprint(opts) + if wrappedStr == "" { + return listTraces + } + trace = fmt.Sprintf("%s%s%s", trace, opts.StackDelimiter, wrappedStr) + } else if opts.EnsureDuplicates { + opts.dups[trace] = struct{}{} + } + + if listTraces != "" { + trace = fmt.Sprintf("%s%s%s", trace, opts.TraceDelimiter, listTraces) + } + return trace +} + +func (st *StackTrace) Sprint(opts ...SprintOpt) string { + o := NewSprintOptions() + for _, opt := range opts { + opt.Apply(o) + } + res := st.sprint(o) + return res +} diff --git a/sprint_test.go b/sprint_test.go new file mode 100644 index 0000000..38bbce1 --- /dev/null +++ b/sprint_test.go @@ -0,0 +1,70 @@ +package stacktrace + +import "testing" + +func TestError_Sprintf(t *testing.T) { + type fields struct { + Severity Severity + Type Type + Location string + Position *Position + Wrapped *StackTrace + Err error + Message string + WrappingMessage string + Info StructInfo + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test simple", + fields: fields{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/tmp/location.raml", + Message: "error message", + }, + want: "[error] validating: /tmp/location.raml:1\n\terror message", + }, + { + name: "Test with wrapped", + fields: fields{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/tmp/location.raml", + Position: &Position{1, 2}, + Message: "error message", + WrappingMessage: "wrapping message", + Wrapped: &StackTrace{ + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location2.raml", + Position: &Position{3, 4}, + Message: "error message 2", + }, + }, + want: "[error] validating: /tmp/location.raml:1:2\n\twrapping message: error message\n[critical] parsing: /tmp/location2.raml:3:4\n\terror message 2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &StackTrace{ + Severity: tt.fields.Severity, + Type: tt.fields.Type, + Location: tt.fields.Location, + Position: tt.fields.Position, + Wrapped: tt.fields.Wrapped, + Err: tt.fields.Err, + Message: tt.fields.Message, + WrappingMessage: tt.fields.WrappingMessage, + Info: tt.fields.Info, + } + if got := e.Sprint(); got != tt.want { + t.Errorf("Sprint() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/stacktrace.go b/stacktrace.go new file mode 100644 index 0000000..78a0996 --- /dev/null +++ b/stacktrace.go @@ -0,0 +1,465 @@ +package stacktrace + +import ( + "errors" + "fmt" + "sort" + "strings" +) + +// Type is the type of the error. +type Type string + +const ( + TypeUnknown Type = "unknown" + TypeParsing Type = "parsing" + TypeLoading Type = "loading" + TypeReading Type = "reading" + TypeResolving Type = "resolving" + TypeValidating Type = "validating" + TypeUnwrapping Type = "unwrapping" +) + +// Severity is the severity of the error. +type Severity string + +const ( + SeverityError Severity = "error" + SeverityWarning Severity = "warning" + SeverityCritical Severity = "critical" +) + +// stringer is a fmt.Stringer implementation. +type stringer struct { + msg string +} + +// String implements the fmt.Stringer interface. +func (s *stringer) String() string { + return s.msg +} + +// Stringer returns a fmt.Stringer for the given value. +func Stringer(v interface{}) fmt.Stringer { + switch w := v.(type) { + case fmt.Stringer: + return w + case string: + return &stringer{msg: w} + case error: + return &stringer{msg: w.Error()} + default: + return &stringer{msg: fmt.Sprintf("%v", w)} + } +} + +// StructInfo is a map of string keys to fmt.Stringer values. +// It is used to store additional information about an error. +// WARNING: Not thread-safe +type StructInfo struct { + info map[string]fmt.Stringer +} + +// String implements the fmt.Stringer interface. +// It returns a string representation of the struct info. +func (s *StructInfo) String() string { + var result string + keys := s.SortedKeys() + + for _, k := range keys { + v, ok := s.info[k] + if ok { + if result == "" { + result = fmt.Sprintf("%s: %s", k, v) + } else { + result = fmt.Sprintf("%s: %s: %s", result, k, v) + } + } + } + return result +} + +// ensureMap ensures that the map is initialized. +func (s *StructInfo) ensureMap() { + if s.info == nil { + s.info = make(map[string]fmt.Stringer) + } +} + +// Add adds a key-value pair to the struct info. +func (s *StructInfo) Add(key string, value fmt.Stringer) *StructInfo { + s.ensureMap() + s.info[key] = value + return s +} + +// Get returns the value of the given key. +func (s *StructInfo) Get(key string) fmt.Stringer { + s.ensureMap() + return s.info[key] +} + +// StringBy returns the string value of the given key. +func (s *StructInfo) StringBy(key string) string { + s.ensureMap() + return s.info[key].String() +} + +// Remove removes the given key from the struct info. +func (s *StructInfo) Remove(key string) *StructInfo { + s.ensureMap() + delete(s.info, key) + return s +} + +// Has checks if the given key exists in the struct info. +func (s *StructInfo) Has(key string) bool { + s.ensureMap() + _, ok := s.info[key] + return ok +} + +// Keys returns the keys of the struct info. +func (s *StructInfo) Keys() []string { + s.ensureMap() + result := make([]string, 0, len(s.info)) + for k := range s.info { + result = append(result, k) + } + return result +} + +// SortedKeys returns the sorted keys of the struct info. +func (s *StructInfo) SortedKeys() []string { + s.ensureMap() + keys := s.Keys() + sort.Strings(keys) + return keys +} + +// Update updates the struct info with the given struct info. +func (s *StructInfo) Update(u *StructInfo) *StructInfo { + s.ensureMap() + for k, v := range u.info { + s.info[k] = v + } + return s +} + +// NewStructInfo creates a new struct info. +func NewStructInfo() *StructInfo { + return &StructInfo{ + info: make(map[string]fmt.Stringer), + } +} + +// StackTrace contains information about a parser error. +type StackTrace struct { + // Severity is the severity of the error. + Severity Severity + // Type is the type of the error. + Type Type + // Location is the location file path of the error. + Location string + // Position is the position of the error in the file. + Position *Position + + // Wrapped is the error that wrapped by this error. + Wrapped *StackTrace + // Err is the underlying error. It is not used for the error message. + Err error + // Message is the error message. + Message string + // WrappingMessage is the error messages of the wrapped errors. + WrappingMessage string + // Info is the additional information about the error. + Info StructInfo + + List []*StackTrace + + typeIsSet bool +} + +// Header returns the header of the StackTrace. +func (st *StackTrace) Header() string { + result := fmt.Sprintf("[%s] %s: %s", + st.Severity, + st.Type, + st.Location, + ) + if st.Position != nil { + result = fmt.Sprintf("%s:%d:%d", result, st.Position.Line, st.Position.Column) + } else { + result = fmt.Sprintf("%s:1", result) + } + return result +} + +// FullMessage returns the full message of the error including the wrapped messages. +func (st *StackTrace) FullMessage() string { + if st.WrappingMessage != "" { + if st.Message != "" { + return fmt.Sprintf("%s: %s", st.WrappingMessage, st.Message) + } + return st.WrappingMessage + } else { + return st.Message + } +} + +// Option is an option for the StackTrace creation. +type Option interface { + Apply(*StackTrace) +} + +// OrigString returns the original error message without the wrapping messages. +func (st *StackTrace) OrigString() string { + result := st.Header() + if st.Message != "" { + result = fmt.Sprintf("%s: %s", result, st.Message) + } + if len(st.Info.info) > 0 { + result = fmt.Sprintf("%s: %s", result, st.Info.String()) + } + return result +} + +// OrigStringW returns the original error message with the wrapped error messages +func (st *StackTrace) OrigStringW() string { + result := st.OrigString() + if st.Wrapped != nil { + result = fmt.Sprintf("%s: %s", result, st.Wrapped.String()) + } + return result +} + +func (st *StackTrace) FullMessageWithInfo() string { + result := st.FullMessage() + if len(st.Info.info) > 0 { + result = fmt.Sprintf("%s: %s", result, st.Info.String()) + } + return result +} + +// String implements the fmt.Stringer interface. +// It returns the string representation of the StackTrace. +func (st *StackTrace) String() string { + result := st.Header() + msg := st.FullMessageWithInfo() + if msg != "" { + result = fmt.Sprintf("%s: %s", result, msg) + } + if st.Wrapped != nil { + result = fmt.Sprintf("%s: %s", result, st.Wrapped.String()) + } + if len(st.List) > 0 { + result = fmt.Sprintf("%s: and more (%d)...", result, len(st.List)) + } + return result +} + +// StackTrace implements the error interface. +// It returns the string representation of the StackTrace. +func (st *StackTrace) Error() string { + return st.String() +} + +// Unwrap checks if the given error is an StackTrace and returns it. +// It returns false if the error is not an StackTrace. +func Unwrap(err error) (*StackTrace, bool) { + if err == nil { + return nil, false + } + st, ok := err.(*StackTrace) + if !ok { + wrappedErr := errors.Unwrap(err) + if wrappedErr == nil { + return nil, false + } + st, ok = Unwrap(wrappedErr) + if ok { + msg := strings.ReplaceAll(err.Error(), st.OrigStringW(), "") + msg = strings.TrimSuffix(msg, ": ") + st.WrappingMessage = msg + st.Err = err + } + } + + // Clone the error to avoid modifying the original error. + return st.Clone(), ok +} + +// New creates a new StackTrace. +func New(message string, location string, opts ...Option) *StackTrace { + e := &StackTrace{ + Severity: SeverityError, + Type: TypeParsing, + Message: message, + Location: location, + } + for _, opt := range opts { + opt.Apply(e) + } + return e +} + +type optErrInfo struct { + Key string + Value fmt.Stringer +} + +func (o optErrInfo) Apply(e *StackTrace) { + e.Info.Add(o.Key, o.Value) +} + +type optErrPosition struct { + Pos *Position +} + +func (o optErrPosition) Apply(e *StackTrace) { + e.Position = o.Pos +} + +type optErrSeverity struct { + Severity Severity +} + +func (o optErrSeverity) Apply(e *StackTrace) { + e.Severity = o.Severity +} + +type optErrType struct { + ErrType Type +} + +func (o optErrType) Apply(e *StackTrace) { + _ = e.SetType(o.ErrType) +} + +func WithInfo(key string, value any) Option { + return optErrInfo{Key: key, Value: Stringer(value)} +} + +func WithPosition(pos *Position) Option { + return optErrPosition{Pos: pos} +} + +func WithSeverity(severity Severity) Option { + return optErrSeverity{Severity: severity} +} + +// WithType sets the type of the error with override. +func WithType(errType Type) Option { + return optErrType{ErrType: errType} +} + +// NewWrapped creates a new StackTrace from the given go error. +func NewWrapped(message string, err error, location string, opts ...Option) *StackTrace { + resultErr := &StackTrace{} + if st, ok := Unwrap(err); ok { + resultErr = New( + message, + location, + ).Wrap(st).SetErr(st.Err) + } else { + resultErr = New(fmt.Sprintf("%s: %s", message, err.Error()), location).SetErr(err) + } + for _, opt := range opts { + opt.Apply(resultErr) + } + return resultErr +} + +// SetSeverity sets the severity of the StackTrace and returns it +func (st *StackTrace) SetSeverity(severity Severity) *StackTrace { + st.Severity = severity + return st +} + +// SetType sets the type of the StackTrace and returns it, operation can be done only once. +func (st *StackTrace) SetType(errType Type) *StackTrace { + if !st.typeIsSet { + st.Type = errType + st.typeIsSet = true + } + if st.Wrapped != nil { + _ = st.Wrapped.SetType(errType) + } + return st +} + +// SetLocation sets the location of the StackTrace and returns it +func (st *StackTrace) SetLocation(location string) *StackTrace { + st.Location = location + return st +} + +// SetPosition sets the position of the StackTrace and returns it +func (st *StackTrace) SetPosition(pos *Position) *StackTrace { + st.Position = pos + return st +} + +// SetWrappingMessage sets a message which wraps the message of StackTrace and returns *StackTrace +func (st *StackTrace) SetWrappingMessage(msg string, a ...any) *StackTrace { + st.WrappingMessage = fmt.Sprintf(msg, a...) + return st +} + +// SetMessage sets the message of the StackTrace and returns it +func (st *StackTrace) SetMessage(message string, a ...any) *StackTrace { + st.Message = fmt.Sprintf(message, a...) + return st +} + +// SetErr sets the underlying error of the StackTrace and returns it +func (st *StackTrace) SetErr(err error) *StackTrace { + st.Err = err + return st +} + +// Wrap wraps the given StackTrace and returns it +func (st *StackTrace) Wrap(w *StackTrace) *StackTrace { + st.Wrapped = w + return st +} + +// Append adds the given StackTrace to the list of StackTraces and returns it +func (st *StackTrace) Append(e *StackTrace) *StackTrace { + if st.List == nil { + st.List = make([]*StackTrace, 0) + } + st.List = append(st.List, e) + return st +} + +// Clone returns a clone of the StackTrace. +func (st *StackTrace) Clone() *StackTrace { + if st == nil { + return nil + } + c := *st + return &c +} + +// Position contains the line and column where the error occurred. +type Position struct { + Line int + Column int +} + +// NewPosition creates a new position with the given line and column. +func NewPosition(line, column int) *Position { + return &Position{Line: line, Column: column} +} + +// optErrNodePosition is an option to set the position of the error to the position of the given node. +type optErrNodePosition struct { + pos *Position +} + +// Apply sets the position of the error to the given position. +// implements Option +func (o optErrNodePosition) Apply(e *StackTrace) { + e.Position = o.pos +} diff --git a/stacktrace_test.go b/stacktrace_test.go new file mode 100644 index 0000000..8fb700f --- /dev/null +++ b/stacktrace_test.go @@ -0,0 +1,1038 @@ +package stacktrace + +import ( + "fmt" + "reflect" + "testing" +) + +func TestUnwrapError(t *testing.T) { + type args struct { + err error + } + firstValidErr := New( + "first validation error", + "/usr/local/raml.raml", + ).SetPosition(&Position{Line: 10, Column: 1}) + simpleErr := fmt.Errorf("simple error") + wrappedSimpleErr := fmt.Errorf("wrapped simpleErr: %w", simpleErr) + wrappedFirstValidErr := fmt.Errorf("wrapped firstValidErr: %w", firstValidErr) + secondValidErr := NewWrapped( + "wrapped firstValidErr", + firstValidErr, + "/usr/local/raml2.raml", + ).SetPosition(&Position{Line: 20, Column: 2}) + wrappedSecondValidErr := fmt.Errorf("wrapped secondValidErr: %w", secondValidErr) + tests := []struct { + name string + args args + want *StackTrace + want1 bool + wantMsg string + }{ + { + name: "Check the same first validation error", + args: args{ + err: firstValidErr, + }, + want: firstValidErr, + want1: true, + wantMsg: "[error] validating: /usr/local/raml.raml:10:1: first validation error", + }, + { + name: "Check wrapped first validation error", + args: args{ + err: wrappedFirstValidErr, + }, + want: &StackTrace{ + Severity: SeverityError, + Type: TypeValidating, + Message: firstValidErr.Message, + Location: firstValidErr.Location, + Position: firstValidErr.Position, + WrappingMessage: "wrapped firstValidErr", + Err: wrappedFirstValidErr, + }, + want1: true, + wantMsg: "[error] validating: /usr/local/raml.raml:10:1: wrapped firstValidErr: first validation error", + }, + { + name: "Check not a validation error", + args: args{ + err: simpleErr, + }, + want: nil, + want1: false, + }, + { + name: "Check not a validation wrapped error", + args: args{ + err: wrappedSimpleErr, + }, + want: nil, + want1: false, + }, + { + name: "Check err is nil", + args: args{ + err: nil, + }, + want: nil, + want1: false, + }, + { + name: "Check validation error wrapped in another error", + args: args{ + err: NewWrapped("wrapped", wrappedSimpleErr, "/usr/local/raml3.raml").SetPosition(&Position{Line: 30, Column: 3}), + }, + want: &StackTrace{ + Severity: SeverityError, + Type: TypeValidating, + Message: fmt.Sprintf("wrapped: %s", wrappedSimpleErr.Error()), + Location: "/usr/local/raml3.raml", + Position: &Position{Line: 30, Column: 3}, + Err: wrappedSimpleErr, + WrappingMessage: "", + }, + want1: true, + wantMsg: fmt.Sprintf("[error] validating: /usr/local/raml3.raml:30:3: wrapped: %s", wrappedSimpleErr.Error()), + }, + { + name: "Check double wrapped error", + args: args{ + err: wrappedSecondValidErr, + }, + want: &StackTrace{ + Severity: SeverityError, + Type: TypeValidating, + Message: secondValidErr.Message, + Location: secondValidErr.Location, + Position: secondValidErr.Position, + Err: wrappedSecondValidErr, + WrappingMessage: "wrapped secondValidErr", + Wrapped: &StackTrace{ + Severity: SeverityError, + Type: TypeValidating, + Message: firstValidErr.Message, + Location: firstValidErr.Location, + Position: firstValidErr.Position, + }, + }, + want1: true, + wantMsg: "[error] validating: /usr/local/raml2.raml:20:2: wrapped secondValidErr: wrapped firstValidErr: [error] validating: /usr/local/raml.raml:10:1: first validation error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := Unwrap(tt.args.err) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Unwrap()\ngot:\n%v\nwant:\n%v\n", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Unwrap() got1 = %v, want %v", got1, tt.want1) + } + if got != nil { + if got.Error() != tt.wantMsg { + t.Errorf("Unwrap() Message\ngot:\n%s\nwant:\n%s\n", got.Error(), tt.wantMsg) + } + } + }) + } +} + +func TestStringer(t *testing.T) { + type args struct { + v interface{} + } + tests := []struct { + name string + args args + want string + }{ + { + name: "Check stringer with string", + args: args{ + v: "string", + }, + want: "string", + }, + { + name: "Check stringer with int", + args: args{ + v: 10, + }, + want: "10", + }, + { + name: "Check stringer with stringer", + args: args{ + v: Stringer("stringer"), + }, + want: "stringer", + }, + { + name: "Check stringer with nil", + args: args{ + v: nil, + }, + want: "", + }, + { + name: "Check stringer with error", + args: args{ + v: fmt.Errorf("error"), + }, + want: "error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Stringer(tt.args.v); !reflect.DeepEqual(got.String(), tt.want) { + t.Errorf("Stringer() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_String(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Check empty struct info", + fields: fields{ + info: map[string]fmt.Stringer{}, + }, + want: "", + }, + { + name: "Check struct info with one key", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + want: "key: value", + }, + { + name: "Check struct info with two keys", + fields: fields{ + info: map[string]fmt.Stringer{ + "key1": Stringer("value1"), + "key2": Stringer("value2"), + }, + }, + want: "key1: value1: key2: value2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_Add(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + type args struct { + key string + value fmt.Stringer + } + tests := []struct { + name string + fields fields + args args + want *StructInfo + }{ + { + name: "Check add key", + fields: fields{ + info: map[string]fmt.Stringer{}, + }, + args: args{ + key: "key", + value: Stringer("value"), + }, + want: &StructInfo{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.Add(tt.args.key, tt.args.value); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Add() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_Get(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + type args struct { + key string + } + tests := []struct { + name string + fields fields + args args + want fmt.Stringer + }{ + { + name: "Check get key", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + args: args{ + key: "key", + }, + want: Stringer("value"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.Get(tt.args.key); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Get() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_StringBy(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + type args struct { + key string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "Check string by key", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + args: args{ + key: "key", + }, + want: "value", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.StringBy(tt.args.key); got != tt.want { + t.Errorf("StringBy() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_Remove(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + type args struct { + key string + } + tests := []struct { + name string + fields fields + args args + want *StructInfo + }{ + { + name: "Check remove key", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + args: args{ + key: "key", + }, + want: &StructInfo{ + info: map[string]fmt.Stringer{}, + }, + }, + { + name: "Check remove key from empty struct info", + fields: fields{ + info: map[string]fmt.Stringer{}, + }, + args: args{ + key: "key", + }, + want: &StructInfo{ + info: map[string]fmt.Stringer{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.Remove(tt.args.key); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Remove() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_Has(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + type args struct { + key string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "Check has key", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + args: args{ + key: "key", + }, + want: true, + }, + { + name: "Check has key in empty struct info", + fields: fields{ + info: map[string]fmt.Stringer{}, + }, + args: args{ + key: "key", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.Has(tt.args.key); got != tt.want { + t.Errorf("Has() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStructInfo_Update(t *testing.T) { + type fields struct { + info map[string]fmt.Stringer + } + type args struct { + u *StructInfo + } + tests := []struct { + name string + fields fields + args args + want *StructInfo + }{ + { + name: "Check update struct info", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + args: args{ + u: &StructInfo{ + info: map[string]fmt.Stringer{ + "key2": Stringer("value2"), + }, + }, + }, + want: &StructInfo{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + "key2": Stringer("value2"), + }, + }, + }, + { + name: "Check update struct info with the same key", + fields: fields{ + info: map[string]fmt.Stringer{ + "key": Stringer("value"), + }, + }, + args: args{ + u: &StructInfo{ + info: map[string]fmt.Stringer{ + "key": Stringer("value2"), + }, + }, + }, + want: &StructInfo{ + info: map[string]fmt.Stringer{ + "key": Stringer("value2"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StructInfo{ + info: tt.fields.info, + } + if got := s.Update(tt.args.u); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Update() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_SetSeverity(t *testing.T) { + type fields struct { + Severity Severity + } + type args struct { + severity Severity + } + tests := []struct { + name string + fields fields + args args + want *StackTrace + }{ + { + name: "Check set severity", + fields: fields{ + Severity: SeverityError, + }, + args: args{ + severity: SeverityCritical, + }, + want: &StackTrace{ + Severity: SeverityCritical, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + Severity: tt.fields.Severity, + } + if got := v.SetSeverity(tt.args.severity); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetSeverity() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_SetType(t *testing.T) { + type fields struct { + ErrType Type + } + type args struct { + errType Type + } + tests := []struct { + name string + fields fields + args args + want *StackTrace + }{ + { + name: "Check set type", + fields: fields{ + ErrType: TypeValidating, + }, + args: args{ + errType: TypeParsing, + }, + want: &StackTrace{ + Type: TypeParsing, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + Type: tt.fields.ErrType, + } + if got := v.SetType(tt.args.errType); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_SetLocationAndPosition(t *testing.T) { + type fields struct { + Location string + Position Position + } + type args struct { + location string + pos Position + } + tests := []struct { + name string + fields fields + args args + want *StackTrace + }{ + { + name: "Check set location and position", + fields: fields{ + Location: "/usr/local/raml.raml", + Position: Position{ + Line: 10, + Column: 1, + }, + }, + args: args{ + location: "/usr/local/raml2.raml", + pos: Position{ + Line: 20, + Column: 2, + }, + }, + want: &StackTrace{ + Location: "/usr/local/raml2.raml", + Position: &Position{ + Line: 20, + Column: 2, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + Location: tt.fields.Location, + Position: &tt.fields.Position, + } + if got := v.SetLocation(tt.args.location).SetPosition(&tt.args.pos); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetLocation() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_SetMessage(t *testing.T) { + type fields struct { + Message string + } + type args struct { + message string + a []any + } + tests := []struct { + name string + fields fields + args args + want *StackTrace + }{ + { + name: "Check set message", + fields: fields{ + Message: "message", + }, + args: args{ + message: "new message", + a: []any{}, + }, + want: &StackTrace{ + Message: "new message", + }, + }, + { + name: "Check set message with arguments", + fields: fields{ + Message: "message", + }, + args: args{ + message: "new message with %s", + a: []any{"argument"}, + }, + want: &StackTrace{ + Message: "new message with argument", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + Message: tt.fields.Message, + } + if got := v.SetMessage(tt.args.message, tt.args.a...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetMessage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_SetWrappingMessage(t *testing.T) { + type fields struct { + Message string + } + type args struct { + message string + a []any + } + tests := []struct { + name string + fields fields + args args + want *StackTrace + }{ + { + name: "Check set message", + fields: fields{ + Message: "message", + }, + args: args{ + message: "new message", + a: []any{}, + }, + want: &StackTrace{ + WrappingMessage: "new message", + }, + }, + { + name: "Check set message with arguments", + fields: fields{ + Message: "message", + }, + args: args{ + message: "new message with %s", + a: []any{"argument"}, + }, + want: &StackTrace{ + WrappingMessage: "new message with argument", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + WrappingMessage: tt.fields.Message, + } + if got := v.SetWrappingMessage(tt.args.message, tt.args.a...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetWrappingMessage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_Error(t *testing.T) { + type fields struct { + Severity Severity + ErrType Type + Location string + Position *Position + Wrapped *StackTrace + Err error + Message string + WrappedMessages string + Info StructInfo + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Check error", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + Message: "message", + WrappedMessages: "wrapped message", + }, + want: "[error] validating: /usr/local/raml.raml:10:1: wrapped message: message", + }, + { + name: "Check error without position", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Message: "message", + }, + want: "[error] validating: /usr/local/raml.raml:1: message", + }, + { + name: "Check error with info", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + Message: "message", + Info: *NewStructInfo().Add("key", Stringer("value")), + }, + want: "[error] validating: /usr/local/raml.raml:10:1: message: key: value", + }, + { + name: "Check error with empty message", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + }, + want: "[error] validating: /usr/local/raml.raml:10:1", + }, + { + name: "Check error with only wrapped messages", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + WrappedMessages: "wrapped message", + }, + want: "[error] validating: /usr/local/raml.raml:10:1: wrapped message", + }, + { + name: "Check error with only wrapped messages and info", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + WrappedMessages: "wrapped message", + Info: *NewStructInfo().Add("key", Stringer("value")), + }, + want: "[error] validating: /usr/local/raml.raml:10:1: wrapped message: key: value", + }, + { + name: "Check error with only wrapped error", + fields: fields{ + Severity: SeverityError, + ErrType: TypeParsing, + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + Wrapped: &StackTrace{ + Severity: SeverityCritical, + Type: TypeValidating, + Location: "/usr/local/raml2.raml", + Position: &Position{Line: 20, Column: 2}, + Message: "message 1", + Wrapped: &StackTrace{ + Severity: SeverityError, + Type: TypeResolving, + Location: "/usr/local/raml3.raml", + Position: &Position{Line: 30, Column: 3}, + Message: "message 2", + }, + }, + Message: "message", + }, + want: "[error] parsing: /usr/local/raml.raml:10:1: message: [critical] validating: /usr/local/raml2.raml:20:2: message 1: [error] resolving: /usr/local/raml3.raml:30:3: message 2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + Severity: tt.fields.Severity, + Type: tt.fields.ErrType, + Location: tt.fields.Location, + Position: tt.fields.Position, + Wrapped: tt.fields.Wrapped, + Err: tt.fields.Err, + Message: tt.fields.Message, + WrappingMessage: tt.fields.WrappedMessages, + Info: tt.fields.Info, + } + if got := v.Error(); got != tt.want { + t.Errorf("StackTrace() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestError_OrigString(t *testing.T) { + type fields struct { + Severity Severity + ErrType Type + Location string + Position Position + Wrapped *StackTrace + Err error + Message string + WrappedMessages string + Info StructInfo + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Check original string", + fields: fields{ + Severity: SeverityError, + ErrType: TypeValidating, + Location: "/usr/local/raml.raml", + Position: Position{Line: 10, Column: 1}, + Message: "message", + Info: *NewStructInfo().Add("key", Stringer("value")), + Wrapped: &StackTrace{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/usr/local/raml2.raml", + Position: &Position{Line: 20, Column: 2}, + Message: "wrapped", + Info: *NewStructInfo().Add("key", Stringer("value")), + }, + }, + want: "[error] validating: /usr/local/raml.raml:10:1: message: key: value", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &StackTrace{ + Severity: tt.fields.Severity, + Type: tt.fields.ErrType, + Location: tt.fields.Location, + Position: &tt.fields.Position, + Wrapped: tt.fields.Wrapped, + Err: tt.fields.Err, + Message: tt.fields.Message, + WrappingMessage: tt.fields.WrappedMessages, + Info: tt.fields.Info, + } + if got := v.OrigString(); got != tt.want { + t.Errorf("OrigString() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewWrappedError(t *testing.T) { + type args struct { + message string + err error + location string + opts []Option + } + tests := []struct { + name string + args args + want *StackTrace + }{ + { + name: "Check wrapped error", + args: args{ + message: "message", + err: fmt.Errorf("error"), + location: "/usr/local/raml.raml", + opts: []Option{ + WithSeverity(SeverityCritical), + WithPosition(NewPosition(10, 1)), + WithInfo("key", Stringer("value")), + WithType(TypeParsing), + }, + }, + want: &StackTrace{ + Severity: SeverityCritical, + Type: TypeParsing, + Message: "message: error", + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + Err: fmt.Errorf("error"), + Info: *NewStructInfo().Add("key", Stringer("value")), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewWrapped(tt.args.message, tt.args.err, tt.args.location, tt.args.opts...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewWrapped() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewError(t *testing.T) { + type args struct { + message string + location string + opts []Option + } + tests := []struct { + name string + args args + want *StackTrace + }{ + { + name: "Check error", + args: args{ + message: "message", + location: "/usr/local/raml.raml", + opts: []Option{ + WithSeverity(SeverityCritical), + WithPosition(NewPosition(10, 1)), + WithInfo("key", Stringer("value")), + WithType(TypeParsing), + }, + }, + want: &StackTrace{ + Severity: SeverityCritical, + Type: TypeParsing, + Message: "message", + Location: "/usr/local/raml.raml", + Position: &Position{Line: 10, Column: 1}, + Info: *NewStructInfo().Add("key", Stringer("value")), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := New(tt.args.message, tt.args.location, tt.args.opts...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +}