Skip to content

Commit 37cc0de

Browse files
commonerrors Add ErrFailed (#669)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description <!-- Please add any detail or context that would be useful to a reviewer. --> Add ErrFailed. This is useful because ErrUnexpected is used a lot as a system error even when we are carrying out underlying tasks where a failure isn't an error. This provides us another generic error but one that we can use to distinguish between failures and system errors. ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update). --------- Co-authored-by: Adrien CABARBAYE <adrien.cabarbaye@arm.com>
1 parent b3aeef0 commit 37cc0de

File tree

5 files changed

+91
-10
lines changed

5 files changed

+91
-10
lines changed

changes/20250807151301.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `commonerrors` Add ErrFailed to be used as a generic error where an error is an expected and valid state that should be distinguished from a system error

utils/commonerrors/errors.go

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"errors"
1212
"fmt"
1313
"strings"
14+
15+
"github.com/hashicorp/go-multierror"
1416
)
1517

1618
// List of common errors used to qualify and categorise go errors
@@ -45,19 +47,30 @@ var (
4547
ErrEOF = errors.New("end of file")
4648
ErrMalicious = errors.New("suspected malicious intent")
4749
ErrOutOfRange = errors.New("out of range")
50+
// ErrFailed should be used as a generic error where an error is an expected and valid state.
51+
// For example a failing command may cause subprocess.Execute to return an error if the command exits with 1 but
52+
// this wouldn't be a system error, and you might want to distinguish between this and the subprocess wrapper erroring
53+
// when you pass the message up the stack.
54+
ErrFailed = errors.New(failureStr)
4855
// ErrWarning is a generic error that can be used when an error should be raised but it shouldn't necessary be
4956
// passed up the chain, for example in cases where an error should be logged but the program should continue. In
5057
// these situations it should be handled immediately and then ignored/set to nil.
5158
ErrWarning = errors.New(warningStr)
5259
)
5360

54-
const warningStr = "warning"
61+
const (
62+
warningStr = "warning"
63+
failureStr = "failed"
64+
)
5565

56-
var warningStrPrepend = fmt.Sprintf("%v: ", warningStr)
66+
var (
67+
warningStrPrepend = fmt.Sprintf("%v%v ", warningStr, string(TypeReasonErrorSeparator))
68+
failureStrPrepend = fmt.Sprintf("%v%v ", failureStr, string(TypeReasonErrorSeparator))
69+
)
5770

5871
// IsCommonError returns whether an error is a commonerror
5972
func IsCommonError(target error) bool {
60-
return Any(target, ErrNotImplemented, ErrNoExtension, ErrNoLogger, ErrNoLoggerSource, ErrNoLogSource, ErrUndefined, ErrInvalidDestination, ErrTimeout, ErrLocked, ErrStaleLock, ErrExists, ErrNotFound, ErrUnsupported, ErrUnavailable, ErrWrongUser, ErrUnauthorised, ErrUnknown, ErrInvalid, ErrConflict, ErrMarshalling, ErrCancelled, ErrEmpty, ErrUnexpected, ErrTooLarge, ErrForbidden, ErrCondition, ErrEOF, ErrMalicious, ErrWarning, ErrOutOfRange)
73+
return Any(target, ErrNotImplemented, ErrNoExtension, ErrNoLogger, ErrNoLoggerSource, ErrNoLogSource, ErrUndefined, ErrInvalidDestination, ErrTimeout, ErrLocked, ErrStaleLock, ErrExists, ErrNotFound, ErrUnsupported, ErrUnavailable, ErrWrongUser, ErrUnauthorised, ErrUnknown, ErrInvalid, ErrConflict, ErrMarshalling, ErrCancelled, ErrEmpty, ErrUnexpected, ErrTooLarge, ErrForbidden, ErrCondition, ErrEOF, ErrMalicious, ErrWarning, ErrOutOfRange, ErrFailed)
6174
}
6275

6376
// Any determines whether the target error is of the same type as any of the errors `err`
@@ -183,6 +196,8 @@ func deserialiseCommonError(errStr string) (bool, error) {
183196
return true, ErrWarning
184197
case CorrespondTo(ErrOutOfRange, errStr):
185198
return true, ErrOutOfRange
199+
case CorrespondTo(ErrFailed, errStr):
200+
return true, ErrFailed
186201
}
187202
return false, ErrUnknown
188203
}
@@ -203,11 +218,24 @@ func ConvertContextError(err error) error {
203218

204219
// IsWarning will return whether an error is actually a warning
205220
func IsWarning(target error) bool {
221+
return isSpecialCase(target, ErrWarning, warningStrPrepend)
222+
}
223+
224+
// IsFailure returns whether an error is unexpected (i.e. deviation from an expected state) but not a system error e.g. test failure
225+
func IsFailure(target error) bool {
226+
return isSpecialCase(target, ErrFailed, failureStrPrepend)
227+
}
228+
229+
func isSpecialCase(target, specialErrorCase error, prefix string) bool {
206230
if target == nil {
207231
return false
208232
}
209233

210-
if Any(target, ErrWarning) {
234+
if Any(target, specialErrorCase) {
235+
return true
236+
}
237+
238+
if strings.HasPrefix(target.Error(), prefix) {
211239
return true
212240
}
213241

@@ -216,7 +244,33 @@ func IsWarning(target error) bool {
216244
return false
217245
}
218246

219-
return strings.TrimSuffix(target.Error(), underlyingErr.Error()) == warningStrPrepend
247+
return strings.TrimSuffix(target.Error(), underlyingErr.Error()) == prefix
248+
}
249+
250+
// MarkAsFailure will tent an error as failure. It will retain its original error type but IsFailure should return true.
251+
func MarkAsFailure(err error) error {
252+
if Any(err, nil, ErrFailed) {
253+
return err
254+
}
255+
result := multierror.Append(err, ErrFailed)
256+
result.ErrorFormat = func(e []error) string {
257+
builder := strings.Builder{}
258+
_, _ = builder.WriteString(failureStr)
259+
for i := range e {
260+
if None(e[i], nil, ErrFailed) {
261+
_, _ = builder.WriteString(string(TypeReasonErrorSeparator))
262+
_, _ = builder.WriteString(" ")
263+
_, _ = builder.WriteString(e[i].Error())
264+
}
265+
}
266+
return builder.String()
267+
}
268+
return result.ErrorOrNil()
269+
}
270+
271+
// NewFailure creates a failure object.
272+
func NewFailure(msgFormat string, args ...any) error {
273+
return Newf(ErrFailed, msgFormat, args...)
220274
}
221275

222276
// NewWarning will create a warning wrapper around an existing commonerror so that it can be easily recovered. If the
@@ -293,7 +347,8 @@ func Errorf(targetErr error, format string, args ...any) error {
293347
}
294348
}
295349

296-
// WrapError wraps an error into a particular targetError. However, if the original error has to do with a contextual error (i.e. ErrCancelled or ErrTimeout), it will be passed through without having is type changed.
350+
// WrapError wraps an error into a particular targetError. However, if the original error has to do with a contextual error (i.e. ErrCancelled or ErrTimeout) or should be considered as a failure rather than an error, it will be passed through without having its type changed.
351+
// Same is true with warnings.
297352
// This method should be used to safely wrap errors without losing information about context control information.
298353
// If the target error is not set, the wrapped error will be of type ErrUnknown.
299354
func WrapError(targetError, originalError error, msg string) error {
@@ -302,7 +357,7 @@ func WrapError(targetError, originalError error, msg string) error {
302357
tErr = ErrUnknown
303358
}
304359
origErr := ConvertContextError(originalError)
305-
if Any(origErr, ErrTimeout, ErrCancelled) {
360+
if Any(origErr, ErrTimeout, ErrCancelled, ErrWarning, ErrFailed) {
306361
tErr = origErr
307362
}
308363
if originalError == nil {
@@ -312,7 +367,8 @@ func WrapError(targetError, originalError error, msg string) error {
312367
if cleansedMsg == "" {
313368
return New(tErr, originalError.Error())
314369
} else {
315-
return Errorf(tErr, "%v%v %v", cleansedMsg, string(TypeReasonErrorSeparator), originalError.Error())
370+
return Errorf(
371+
tErr, "%v%v %v", cleansedMsg, string(TypeReasonErrorSeparator), originalError.Error())
316372
}
317373
}
318374
}

utils/commonerrors/errors_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func TestAny(t *testing.T) {
2727
assert.False(t, Any(nil, ErrInvalid, ErrUnknown))
2828
assert.True(t, Any(fmt.Errorf("an error %w", ErrNotImplemented), ErrInvalid, ErrNotImplemented, ErrUnknown))
2929
assert.False(t, Any(fmt.Errorf("an error %w", ErrNotImplemented), ErrInvalid, ErrUnknown))
30+
assert.True(t, Any(WrapError(ErrUnexpected, WrapError(ErrNotFound, WrapError(ErrFailed, errors.New(faker.Sentence()), "failure!!!"), faker.Sentence()), faker.Sentence()), ErrFailed))
31+
assert.False(t, Any(WrapError(ErrUnexpected, WrapError(ErrNotFound, WrapError(ErrFailed, errors.New(faker.Sentence()), "failure!!!"), faker.Sentence()), faker.Sentence()), ErrUnexpected, ErrNotFound))
3032
}
3133

3234
func TestNone(t *testing.T) {
@@ -38,6 +40,7 @@ func TestNone(t *testing.T) {
3840
assert.False(t, None(nil, nil, ErrInvalid, ErrNotImplemented, ErrUnknown))
3941
assert.False(t, None(fmt.Errorf("an error %w", ErrNotImplemented), ErrInvalid, ErrNotImplemented, ErrUnknown))
4042
assert.True(t, None(fmt.Errorf("an error %w", ErrNotImplemented), ErrInvalid, ErrUnknown))
43+
assert.False(t, None(WrapError(ErrUnexpected, WrapError(ErrNotFound, WrapError(ErrFailed, errors.New(faker.Sentence()), "failure!!!"), faker.Sentence()), faker.Sentence()), ErrFailed, ErrUnauthorised))
4144
}
4245

4346
func TestCorrespondTo(t *testing.T) {
@@ -112,6 +115,7 @@ func TestIsCommonError(t *testing.T) {
112115
ErrMalicious,
113116
ErrWarning,
114117
ErrOutOfRange,
118+
ErrFailed,
115119
}
116120
for i := range commonErrors {
117121
assert.True(t, IsCommonError(commonErrors[i]))
@@ -124,8 +128,22 @@ func TestIsWarning(t *testing.T) {
124128
assert.True(t, IsWarning(ErrWarning))
125129
assert.False(t, IsWarning(ErrUnexpected))
126130
assert.False(t, IsWarning(nil))
127-
assert.True(t, IsWarning(fmt.Errorf("%w: i am i warning", ErrWarning)))
128-
assert.True(t, IsWarning(fmt.Errorf("%w: i am i warning too: %v", ErrWarning, ErrUnknown)))
131+
assert.True(t, IsWarning(fmt.Errorf("%w: i am a warning", ErrWarning)))
132+
assert.True(t, IsWarning(fmt.Errorf("%w: i am a warning too: %v", ErrWarning, ErrUnknown)))
133+
}
134+
135+
func TestIsFailure(t *testing.T) {
136+
assert.True(t, IsFailure(ErrFailed))
137+
assert.False(t, IsFailure(ErrUnexpected))
138+
assert.True(t, IsFailure(MarkAsFailure(ErrUnexpected)))
139+
assert.True(t, Any(MarkAsFailure(ErrUnexpected), ErrUnexpected))
140+
assert.True(t, Any(MarkAsFailure(ErrUnexpected), ErrFailed))
141+
assert.False(t, IsFailure(nil))
142+
assert.True(t, IsFailure(NewFailure("i am a failure and I know it")))
143+
assert.True(t, IsFailure(fmt.Errorf("%w: i am a failure", ErrFailed)))
144+
assert.True(t, IsFailure(fmt.Errorf("%w: i am a failure too: %v", ErrFailed, ErrUnknown)))
145+
assert.True(t, IsFailure(fmt.Errorf("%v: %w", ErrFailed, ErrUnknown)))
146+
assert.True(t, IsFailure(fmt.Errorf("%v: %w: i am a failure too too", ErrFailed, ErrUnknown)))
129147
}
130148

131149
func TestNewWarning(t *testing.T) {

utils/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
2727
github.com/hashicorp/go-cleanhttp v0.5.2
2828
github.com/hashicorp/go-hclog v1.6.3
29+
github.com/hashicorp/go-multierror v1.1.1
2930
github.com/hashicorp/go-retryablehttp v0.7.8
3031
github.com/iamacarpet/go-win64api v0.0.0-20240507095429-873e84e85847
3132
github.com/joho/godotenv v1.5.1
@@ -73,6 +74,7 @@ require (
7374
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
7475
github.com/google/cabbie v1.0.2 // indirect
7576
github.com/google/glazier v0.0.0-20211029225403-9f766cca891d // indirect
77+
github.com/hashicorp/errwrap v1.0.0 // indirect
7678
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
7779
github.com/kevinburke/ssh_config v1.2.0 // indirect
7880
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect

utils/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,14 @@ github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3
126126
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
127127
github.com/google/winops v0.0.0-20210803215038-c8511b84de2b/go.mod h1:ShbX8v8clPm/3chw9zHVwtW3QhrFpL8mXOwNxClt4pg=
128128
github.com/groob/plist v0.0.0-20210519001750-9f754062e6d6/go.mod h1:itkABA+w2cw7x5nYUS/pLRef6ludkZKOigbROmCTaFw=
129+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
130+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
129131
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
130132
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
131133
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
132134
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
135+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
136+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
133137
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
134138
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
135139
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=

0 commit comments

Comments
 (0)