From d40069c18e240d6ed1f6ae661f55321abace47ed Mon Sep 17 00:00:00 2001 From: Nedyalko Andreev Date: Mon, 7 Jun 2021 09:56:41 +0300 Subject: [PATCH] Add an errext package with error types, helpers and exitcodes --- errext/errext_test.go | 77 +++++++++++++++++++++++++++++++++++++++ errext/exception.go | 29 +++++++++++++++ errext/exit_code.go | 66 +++++++++++++++++++++++++++++++++ errext/exitcodes/codes.go | 39 ++++++++++++++++++++ errext/hint.go | 63 ++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 errext/errext_test.go create mode 100644 errext/exception.go create mode 100644 errext/exit_code.go create mode 100644 errext/exitcodes/codes.go create mode 100644 errext/hint.go diff --git a/errext/errext_test.go b/errext/errext_test.go new file mode 100644 index 00000000000..71659e5bd07 --- /dev/null +++ b/errext/errext_test.go @@ -0,0 +1,77 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2021 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package errext + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func assertHasHint(t *testing.T, err error, hint string) { + var typederr HasHint + require.ErrorAs(t, err, &typederr) + assert.Equal(t, typederr.Hint(), hint) + assert.Contains(t, err.Error(), typederr.Error()) +} + +func assertHasExitCode(t *testing.T, err error, exitcode ExitCode) { + var typederr HasExitCode + require.ErrorAs(t, err, &typederr) + assert.Equal(t, typederr.ExitCode(), exitcode) + assert.Contains(t, err.Error(), typederr.Error()) +} + +func TestErrextHelpers(t *testing.T) { + t.Parallel() + + const testExitCode ExitCode = 13 + assert.Nil(t, WithHint(nil, "test hint")) + assert.Nil(t, WithExitCode(nil, testExitCode)) + + errBase := errors.New("base error") + errBaseWithHint := WithHint(errBase, "test hint") + assertHasHint(t, errBaseWithHint, "test hint") + errBaseWithTwoHints := WithHint(errBaseWithHint, "better hint") + assertHasHint(t, errBaseWithTwoHints, "better hint (test hint)") + + errWrapperWithHints := fmt.Errorf("wrapper error: %w", errBaseWithTwoHints) + assertHasHint(t, errWrapperWithHints, "better hint (test hint)") + + errWithExitCode := WithExitCode(errWrapperWithHints, testExitCode) + assertHasHint(t, errWithExitCode, "better hint (test hint)") + assertHasExitCode(t, errWithExitCode, testExitCode) + + errWithExitCodeAgain := WithExitCode(errWithExitCode, ExitCode(27)) + assertHasHint(t, errWithExitCodeAgain, "better hint (test hint)") + assertHasExitCode(t, errWithExitCodeAgain, testExitCode) + + errBaseWithThreeHints := WithHint(errWithExitCodeAgain, "best hint") + assertHasHint(t, errBaseWithThreeHints, "best hint (better hint (test hint))") + + finalErrorMess := fmt.Errorf("woot: %w", errBaseWithThreeHints) + assert.Equal(t, finalErrorMess.Error(), "woot: wrapper error: base error") + assertHasHint(t, finalErrorMess, "best hint (better hint (test hint))") + assertHasExitCode(t, finalErrorMess, testExitCode) +} diff --git a/errext/exception.go b/errext/exception.go new file mode 100644 index 00000000000..06daa138651 --- /dev/null +++ b/errext/exception.go @@ -0,0 +1,29 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2021 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +// Package errext contains extensions for normal Go errors that are used in k6. +package errext + +// Exception represents errors that resulted from a script exception and contain +// a stack trace that lead to them. +type Exception interface { + error + StackTrace() string +} diff --git a/errext/exit_code.go b/errext/exit_code.go new file mode 100644 index 00000000000..03688b1ead5 --- /dev/null +++ b/errext/exit_code.go @@ -0,0 +1,66 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2021 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package errext + +import "errors" + +// ExitCode is the code with which the application should exit if this error +// bubbles up to the top of the scope. Values should be between 0 and 125: +// https://unix.stackexchange.com/questions/418784/what-is-the-min-and-max-values-of-exit-codes-in-linux +type ExitCode uint8 + +// HasExitCode is a wrapper around an error with an attached exit code. +type HasExitCode interface { + error + ExitCode() ExitCode +} + +// WithExitCode can attach an exit code to the given error, if it doesn't have +// one already. It won't do anything if the error already had an exit code +// attached. Similarly, if there is no error (i.e. the given error is nil), it +// also won't do anything. +func WithExitCode(err error, exitCode ExitCode) error { + if err == nil { + // No error, do nothing + return nil + } + var ecerr HasExitCode + if errors.As(err, &ecerr) { + // The given error already has an exit code, do nothing + return err + } + return withExitCode{err, exitCode} +} + +type withExitCode struct { + error + exitCode ExitCode +} + +func (wh withExitCode) Unwrap() error { + return wh.error +} + +func (wh withExitCode) ExitCode() ExitCode { + return wh.exitCode +} + +var _ HasExitCode = withExitCode{} diff --git a/errext/exitcodes/codes.go b/errext/exitcodes/codes.go new file mode 100644 index 00000000000..d98aaf4ab72 --- /dev/null +++ b/errext/exitcodes/codes.go @@ -0,0 +1,39 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2021 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +// Package exitcodes contains the constants representing possible k6 exit error codes. +//nolint: golint +package exitcodes + +import "go.k6.io/k6/errext" + +const ( + CloudTestRunFailed errext.ExitCode = 97 // This used to be 99 before k6 v0.33.0 + CloudFailedToGetProgress errext.ExitCode = 98 + ThresholdsHaveFailed errext.ExitCode = 99 + SetupTimeout errext.ExitCode = 100 + TeardownTimeout errext.ExitCode = 101 + GenericTimeout errext.ExitCode = 102 // TODO: remove? + GenericEngine errext.ExitCode = 103 + InvalidConfig errext.ExitCode = 104 + ExternalAbort errext.ExitCode = 105 + CannotStartRESTAPI errext.ExitCode = 106 + ScriptException errext.ExitCode = 107 +) diff --git a/errext/hint.go b/errext/hint.go new file mode 100644 index 00000000000..c1f6988dfbd --- /dev/null +++ b/errext/hint.go @@ -0,0 +1,63 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2021 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package errext + +import "errors" + +// HasHint is a wrapper around an error with an attached user hint. These hints +// can be used to give extra human-readable information about the error, +// including suggestions on how the error can be fixed. +type HasHint interface { + error + Hint() string +} + +// WithHint is a helper that can attach a hint to the given error. If there is +// no error (i.e. the given error is nil), it won't do anything. If the given +// error already had a hint, this helper will wrap it so that the new hint is +// "new hint (old hint)". +func WithHint(err error, hint string) error { + if err == nil { + // No error, do nothing + return nil + } + var oldhint HasHint + if errors.As(err, &oldhint) { + // The given error already had a hint, wrap it + hint = hint + " (" + oldhint.Hint() + ")" + } + return withHint{err, hint} +} + +type withHint struct { + error + hint string +} + +func (wh withHint) Unwrap() error { + return wh.error +} + +func (wh withHint) Hint() string { + return wh.hint +} + +var _ HasHint = withHint{}