package errors import ( "errors" "fmt" "io" "regexp" "strings" "testing" ) func TestFormatNew(t *testing.T) { tests := []struct { error format string want string }{{ New("error"), "%s", "error", }, { New("error"), "%v", "error", }, //{ // New("error"), // "%+v", // "error\n" + // "github.com/seacraft/errors.TestFormatNew\n" + // "\t.+/github.com/seacraft/errors/format_test.go:26", //}, { New("error"), "%q", `"error"`, }} for i, tt := range tests { testFormatRegexp(t, i, tt.error, tt.format, tt.want) } } func TestFormatErrorf(t *testing.T) { tests := []struct { error format string want string }{{ Errorf("%s", "error"), "%s", "error", }, { Errorf("%s", "error"), "%v", "error", }, //{ // Errorf("%s", "error"), // "%+v", // "error\n" + // "github.com/seacraft/errors.TestFormatErrorf\n" + // "\t.+/github.com/seacraft/errors/format_test.go:58", //} } for i, tt := range tests { testFormatRegexp(t, i, tt.error, tt.format, tt.want) } } func TestFormatWrap(t *testing.T) { tests := []struct { error format string want string }{{ Wrap(New("error"), "error2"), "%s", "error2", }, { Wrap(New("error"), "error2"), "%v", "error2", }, //{ // Wrap(New("error"), "error2"), // "%+v", // "error\n" + // "github.com/seacraft/errors.TestFormatWrap\n" + // "\t.+/github.com/seacraft/errors/format_test.go:82", //}, { Wrap(io.EOF, "error"), "%s", "error", }, { Wrap(io.EOF, "error"), "%v", "error", }, //{ // Wrap(io.EOF, "error"), // "%+v", // "EOF\n" + // "error\n" + // "github.com/seacraft/errors.TestFormatWrap\n" + // "\t.+/github.com/seacraft/errors/format_test.go:96", //}, { // Wrap(Wrap(io.EOF, "error1"), "error2"), // "%+v", // "EOF\n" + // "error1\n" + // "github.com/seacraft/errors.TestFormatWrap\n" + // "\t.+/github.com/seacraft/errors/format_test.go:103\n", //}, { Wrap(New("error with space"), "context"), "%q", `"context"`, }} for i, tt := range tests { testFormatRegexp(t, i, tt.error, tt.format, tt.want) } } func TestFormatWrapf(t *testing.T) { tests := []struct { error format string want string }{{ Wrapf(io.EOF, "error%d", 2), "%s", "error2", }, { Wrapf(io.EOF, "error%d", 2), "%v", "error2", }, //{ // Wrapf(io.EOF, "error%d", 2), // "%+v", // "EOF\n" + // "error2\n" + // "github.com/seacraft/errors.TestFormatWrapf\n" + // "\t.+/github.com/seacraft/errors/format_test.go:134", //}, { Wrapf(New("error"), "error%d", 2), "%s", "error2", }, { Wrapf(New("error"), "error%d", 2), "%v", "error2", }, //{ // Wrapf(New("error"), "error%d", 2), // "%+v", // "error\n" + // "github.com/seacraft/errors.TestFormatWrapf\n" + // "\t.+/github.com/seacraft/errors/format_test.go:149", //} } for i, tt := range tests { testFormatRegexp(t, i, tt.error, tt.format, tt.want) } } func TestFormatWithStack(t *testing.T) { tests := []struct { error format string want []string }{{ WithStack(io.EOF), "%s", []string{"EOF"}, }, { WithStack(io.EOF), "%v", []string{"EOF"}, }, //{ // WithStack(io.EOF), // "%+v", // []string{"EOF", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:175"}, //}, { WithStack(New("error")), "%s", []string{"error"}, }, { WithStack(New("error")), "%v", []string{"error"}, }, //{ // WithStack(New("error")), // "%+v", // []string{"error", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:189", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:189"}, //}, { // WithStack(WithStack(io.EOF)), // "%+v", // []string{"EOF", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:197", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:197"}, //}, { // WithStack(WithStack(Wrapf(io.EOF, "message"))), // "%+v", // []string{"EOF", // "message", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:205", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:205", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:205"}, //}, { // WithStack(Errorf("error%d", 1)), // "%+v", // []string{"error1", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:216", // "github.com/seacraft/errors.TestFormatWithStack\n" + // "\t.+/github.com/seacraft/errors/format_test.go:216"}, //} } for i, tt := range tests { testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) } } func TestFormatWithMessage(t *testing.T) { tests := []struct { error format string want []string }{{ WithMessage(New("error"), "error2"), "%s", []string{"error2"}, }, { WithMessage(New("error"), "error2"), "%v", []string{"error2"}, }, //{ // WithMessage(New("error"), "error2"), // "%+v", // []string{ // "error", // "github.com/seacraft/errors.TestFormatWithMessage\n" + // "\t.+/github.com/seacraft/errors/format_test.go:244", // "error2"}, //}, { WithMessage(io.EOF, "addition1"), "%s", []string{"addition1"}, }, { WithMessage(io.EOF, "addition1"), "%v", []string{"addition1"}, }, { WithMessage(io.EOF, "addition1"), "%+v", []string{"EOF", "addition1"}, }, { WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), "%v", []string{"addition2"}, }, { WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), "%+v", []string{"EOF", "addition1", "addition2"}, }, //{ // Wrap(WithMessage(io.EOF, "error1"), "error2"), // "%+v", // []string{"EOF", "error1", "error2", // "github.com/seacraft/errors.TestFormatWithMessage\n" + // "\t.+/github.com/seacraft/errors/format_test.go:272"}, //}, { // WithMessage(Errorf("error%d", 1), "error2"), // "%+v", // []string{"error1", // "github.com/seacraft/errors.TestFormatWithMessage\n" + // "\t.+/github.com/seacraft/errors/format_test.go:278", // "error2"}, //}, { // WithMessage(WithStack(io.EOF), "error"), // "%+v", // []string{ // "EOF", // "github.com/seacraft/errors.TestFormatWithMessage\n" + // "\t.+/github.com/seacraft/errors/format_test.go:285", // "error"}, //}, { // WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), // "%+v", // []string{ // "EOF", // "github.com/seacraft/errors.TestFormatWithMessage\n" + // "\t.+/github.com/seacraft/errors/format_test.go:293", // "inside-error", // "github.com/seacraft/errors.TestFormatWithMessage\n" + // "\t.+/github.com/seacraft/errors/format_test.go:293", // "outside-error"}, //} } for i, tt := range tests { testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) } } //func TestFormatGeneric(t *testing.T) { // starts := []struct { // err error // want []string // }{ // {New("new-error"), []string{ // "new-error", // "github.com/seacraft/errors.TestFormatGeneric\n" + // "\t.+/github.com/seacraft/errors/format_test.go:315"}, // }, {Errorf("errorf-error"), []string{ // "errorf-error", // "github.com/seacraft/errors.TestFormatGeneric\n" + // "\t.+/github.com/seacraft/errors/format_test.go:319"}, // }, {errors.New("errors-new-error"), []string{ // "errors-new-error"}, // }, // } // // wrappers := []wrapper{ // { // func(err error) error { return WithMessage(err, "with-message") }, // []string{"with-message"}, // }, { // func(err error) error { return WithStack(err) }, // []string{ // "github.com/seacraft/errors.(func·002|TestFormatGeneric.func2)\n\t" + // ".+/github.com/seacraft/errors/format_test.go:333", // }, // }, { // func(err error) error { return Wrap(err, "wrap-error") }, // []string{ // "wrap-error", // "github.com/seacraft/errors.(func·003|TestFormatGeneric.func3)\n\t" + // ".+/github.com/seacraft/errors/format_test.go:339", // }, // }, { // func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, // []string{ // "wrapf-error1", // "github.com/seacraft/errors.(func·004|TestFormatGeneric.func4)\n\t" + // ".+/github.com/seacraft/errors/format_test.go:346", // }, // }, // } // // for s := range starts { // err := starts[s].err // want := starts[s].want // testFormatCompleteCompare(t, s, err, "%+v", want, false) // testGenericRecursive(t, err, want, wrappers, 3) // } //} //func wrappedNew(message string) error { // This function will be mid-stack inlined in go 1.12+ // return New(message) //} //func TestFormatWrappedNew(t *testing.T) { // tests := []struct { // error // format string // want string // }{{ // wrappedNew("error"), // "%+v", // "error\n" + // "github.com/seacraft/errors.wrappedNew\n" + // "\t.+/github.com/seacraft/errors/format_test.go:373\n" + // "github.com/seacraft/errors.TestFormatWrappedNew\n" + // "\t.+/github.com/seacraft/errors/format_test.go:373", // }} // // for i, tt := range tests { // testFormatRegexp(t, i, tt.error, tt.format, tt.want) // } //} func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { t.Helper() got := fmt.Sprintf(format, arg) gotLines := strings.SplitN(got, "\n", -1) wantLines := strings.SplitN(want, "\n", -1) if len(wantLines) > len(gotLines) { t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) return } for i, w := range wantLines { match, err := regexp.MatchString(w, gotLines[i]) if err != nil { t.Fatal(err) } if !match { t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) } } } var stackLineR = regexp.MustCompile(`\.`) // parseBlocks parses input into a slice, where: // - incase entry contains a newline, its a stacktrace // - incase entry contains no newline, its a solo line. // // Detecting stack boundaries only works incase the WithStack-calls are // to be found on the same line, thats why it is optionally here. // // Example use: // // for _, e := range blocks { // if strings.ContainsAny(e, "\n") { // // Match as stack // } else { // // Match as line // } // } func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { var blocks []string stack := "" wasStack := false lines := map[string]bool{} // already found lines for _, l := range strings.Split(input, "\n") { isStackLine := stackLineR.MatchString(l) switch { case !isStackLine && wasStack: blocks = append(blocks, stack, l) stack = "" lines = map[string]bool{} case isStackLine: if wasStack { // Detecting two stacks after another, possible cause lines match in // our tests due to WithStack(WithStack(io.EOF)) on same line. if detectStackboundaries { if lines[l] { if len(stack) == 0 { return nil, errors.New("len of block must not be zero here") } blocks = append(blocks, stack) stack = l lines = map[string]bool{l: true} continue } } stack = stack + "\n" + l } else { stack = l } lines[l] = true case !isStackLine && !wasStack: blocks = append(blocks, l) default: return nil, errors.New("must not happen") } wasStack = isStackLine } // Use up stack if stack != "" { blocks = append(blocks, stack) } return blocks, nil } func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { gotStr := fmt.Sprintf(format, arg) got, err := parseBlocks(gotStr, detectStackBoundaries) if err != nil { t.Fatal(err) } if len(got) != len(want) { t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) } for i := range got { if strings.ContainsAny(want[i], "\n") { // Match as stack match, err := regexp.MatchString(want[i], got[i]) if err != nil { t.Fatal(err) } if !match { t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) } } else { // Match as message if got[i] != want[i] { t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) } } } } //type wrapper struct { // wrap func(err error) error // want []string //} func prettyBlocks(blocks []string) string { var out []string for _, b := range blocks { out = append(out, fmt.Sprintf("%v", b)) } return " " + strings.Join(out, "\n ") } //func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { // if len(beforeWant) == 0 { // panic("beforeWant must not be empty") // } // for _, w := range list { // if len(w.want) == 0 { // panic("want must not be empty") // } // // err := w.wrap(beforeErr) // // // Copy required cause append(beforeWant, ..) modified beforeWant subtly. // beforeCopy := make([]string, len(beforeWant)) // copy(beforeCopy, beforeWant) // // beforeWant := beforeCopy // last := len(beforeWant) - 1 // var want []string // // // Merge two stacks behind each other. // if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { // want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) // } else { // want = append(beforeWant, w.want...) // } // // testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) // if maxDepth > 0 { // testGenericRecursive(t, err, want, list, maxDepth-1) // } // } //} func TestFormatCode(t *testing.T) { tests := []struct { format string want string }{ {"%s", `ConfigurationNotValid error`}, {"%v", `ConfigurationNotValid error`}, //{"%-v", `^service configuration could not be loaded - #3 \[.*mocks_test.go:34 \(.*errors.loadConfig\)\] \(1000\) ConfigurationNotValid error$`}, //{"%+v", `^service configuration could not be loaded - #3 \[.*mocks_test.go:34 \(.*errors.loadConfig\)\] \(1000\) ConfigurationNotValid error; could not decode configuration data - #2 \[.*mocks_test.go:39 \(.*errors.decodeConfig\)\] \(1001\) Data is not valid JSON; could not read configuration file - #1 \[.*mocks_test.go:44 \(.*errors.readConfig\)\] \(1002\) End of input; read: end of input - #0 read: end of input`}, //{"%#-v", `[{\"caller\":\"#3 /home/lk/workspace/golang/src/github.com/seacraft/errors/mocks_test.go:34 (github.com/seacraft/errors.loadConfig)\",\"code\":1000,\"error\":\"service configuration could not be loaded\",\"message\":\"ConfigurationNotValid error\"}]`}, //{"%#+v", `[{\"caller\":\"#3 /home/lk/workspace/golang/src/github.com/seacraft/errors/mocks_test.go:34 (github.com/seacraft/errors.loadConfig)\",\"code\":1000,\"error\":\"service configuration could not be loaded\",\"message\":\"ConfigurationNotValid error\"},{\"caller\":\"#2 /home/lk/workspace/golang/src/github.com/seacraft/errors/mocks_test.go:39 (github.com/seacraft/errors.decodeConfig)\",\"code\":1001,\"error\":\"could not decode configuration data\",\"message\":\"Data is not valid JSON\"},{\"caller\":\"#1 /home/lk/workspace/golang/src/github.com/seacraft/errors/mocks_test.go:39 (github.com/seacraft/errors.readConfig)\",\"code\":1002,\"error\":\"could not read configuration file\",\"message\":\"End of input\"},{\"caller\":\"#0\",\"code\":1,\"error\":\"read: end of input\",\"message\":\"read: end of input\"}]`}, } for i, tt := range tests { got := fmt.Sprintf(tt.format, loadConfig()) if !regexp.MustCompile(tt.want).Match([]byte(got)) { t.Errorf("test %d: TestFormatCode:\n got %q\n want %q", i+1, got, tt.want) } } }