Skip to content

Commit 79919d4

Browse files
acabarbayeKem-Gov
andauthored
[errors] Add a way to provide context to an error without changing its type (#724)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description - Added 3 utilities to use to provide further context to an error without changing its type ### 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: Kem Govender <kem.govender@arm.com>
1 parent c51af55 commit 79919d4

File tree

3 files changed

+178
-2
lines changed

3 files changed

+178
-2
lines changed

changes/20251013123506.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `[errors]` Added a way to provide context (`DescribeCircumstance`) to an error but without changing is type if it is a common error

utils/commonerrors/errors.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,89 @@ func IsCommonError(target error) bool {
7373
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)
7474
}
7575

76+
// RetrieveCommonError tries to return the common error of an error.
77+
func RetrieveCommonError(target error) (isCommonError bool, commonError error) {
78+
switch {
79+
case IsEmpty(target):
80+
return true, nil
81+
case Any(target, ErrNotImplemented):
82+
return true, ErrNotImplemented
83+
case Any(target, ErrNoExtension):
84+
return true, ErrNoExtension
85+
case Any(target, ErrNoLogger):
86+
return true, ErrNoLogger
87+
case Any(target, ErrNoLoggerSource):
88+
return true, ErrNoLoggerSource
89+
case Any(target, ErrNoLogSource):
90+
return true, ErrNoLogSource
91+
case Any(target, ErrUndefined):
92+
return true, ErrUndefined
93+
case Any(target, ErrInvalidDestination):
94+
return true, ErrInvalidDestination
95+
case Any(target, ErrTimeout):
96+
return true, ErrTimeout
97+
case Any(target, ErrLocked):
98+
return true, ErrLocked
99+
case Any(target, ErrStaleLock):
100+
return true, ErrStaleLock
101+
case Any(target, ErrExists):
102+
return true, ErrExists
103+
case Any(target, ErrNotFound):
104+
return true, ErrNotFound
105+
case Any(target, ErrUnsupported):
106+
return true, ErrUnsupported
107+
case Any(target, ErrUnavailable):
108+
return true, ErrUnavailable
109+
case Any(target, ErrWrongUser):
110+
return true, ErrWrongUser
111+
case Any(target, ErrUnauthorised):
112+
return true, ErrUnauthorised
113+
case Any(target, ErrUnknown):
114+
return true, ErrUnknown
115+
case Any(target, ErrInvalid):
116+
return true, ErrInvalid
117+
case Any(target, ErrConflict):
118+
return true, ErrConflict
119+
case Any(target, ErrMarshalling):
120+
return true, ErrMarshalling
121+
case Any(target, ErrCancelled):
122+
return true, ErrCancelled
123+
case Any(target, ErrEmpty):
124+
return true, ErrEmpty
125+
case Any(target, ErrUnexpected):
126+
return true, ErrUnexpected
127+
case Any(target, ErrTooLarge):
128+
return true, ErrTooLarge
129+
case Any(target, ErrForbidden):
130+
return true, ErrForbidden
131+
case Any(target, ErrCondition):
132+
return true, ErrCondition
133+
case Any(target, ErrEOF):
134+
return true, ErrEOF
135+
case Any(target, ErrMalicious):
136+
return true, ErrMalicious
137+
case Any(target, ErrOutOfRange):
138+
return true, ErrOutOfRange
139+
case Any(target, ErrFailed):
140+
return true, ErrFailed
141+
case Any(target, ErrWarning):
142+
return true, ErrWarning
143+
}
144+
145+
underlyingErr, parseError := GetUnderlyingErrorType(target)
146+
if parseError != nil {
147+
commonError = ErrUnknown
148+
return
149+
}
150+
if IsCommonError(underlyingErr) {
151+
commonError = underlyingErr
152+
isCommonError = true
153+
} else {
154+
commonError = ErrUnknown
155+
}
156+
return
157+
}
158+
76159
// Any determines whether the target error is of the same type as any of the errors `err`
77160
func Any(target error, err ...error) bool {
78161
for i := range err {
@@ -371,6 +454,36 @@ func Errorf(targetErr error, format string, args ...any) error {
371454
}
372455
}
373456

457+
// DescribeCircumstance adds some context to a particular error. If the error is of a known type (as in, a common error), it will be kept. Otherwise, it will be set as Unexpected.
458+
func DescribeCircumstance(originalError error, circumstance string) error {
459+
origErr := ConvertContextError(originalError)
460+
isCommonError, commonError := RetrieveCommonError(origErr)
461+
if isCommonError {
462+
return WrapError(commonError, origErr, circumstance)
463+
}
464+
return WrapError(ErrUnexpected, origErr, circumstance)
465+
}
466+
467+
// DescribeCircumstanceAndKeepType does almost the same as DescribeCircumstance but if the error is not a common error, it won't wrap it but just add the context string to it.
468+
func DescribeCircumstanceAndKeepType(err error, circumstance string) error {
469+
origErr := ConvertContextError(err)
470+
isCommonError, commonError := RetrieveCommonError(origErr)
471+
if isCommonError {
472+
return WrapError(commonError, origErr, circumstance)
473+
}
474+
return New(origErr, circumstance)
475+
}
476+
477+
// DescribeCircumstanceAndKeepTypef is like DescribeCircumstanceAndKeepType but uses a message format to describe the error context.
478+
func DescribeCircumstanceAndKeepTypef(err error, circumstanceFormat string, args ...any) error {
479+
return DescribeCircumstanceAndKeepType(err, fmt.Sprintf(circumstanceFormat, args...))
480+
}
481+
482+
// DescribeCircumstancef is the same as DescribeCircumstance but uses a format for capturing the error context.
483+
func DescribeCircumstancef(err error, circumstanceFormat string, args ...any) error {
484+
return DescribeCircumstance(err, fmt.Sprintf(circumstanceFormat, args...))
485+
}
486+
374487
// 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.
375488
// Same is true with warnings.
376489
// This method should be used to safely wrap errors without losing information about context control information.
@@ -407,7 +520,7 @@ func WrapIfNotCommonError(targetError, originalError error, msg string) error {
407520
return WrapError(targetError, originalError, msg)
408521
}
409522
if IsCommonError(originalError) {
410-
return New(originalError, msg)
523+
return DescribeCircumstance(originalError, msg)
411524
}
412525
return WrapError(targetError, originalError, msg)
413526
}
@@ -426,7 +539,7 @@ func WrapIfNotCommonErrorf(targetError, originalError error, msgFormat string, a
426539
return WrapErrorf(targetError, originalError, msgFormat, args...)
427540
}
428541
if IsCommonError(originalError) {
429-
return Newf(originalError, msgFormat, args...)
542+
return DescribeCircumstancef(originalError, msgFormat, args...)
430543
}
431544
return WrapErrorf(targetError, originalError, msgFormat, args...)
432545
}

utils/commonerrors/errors_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,44 @@ func TestIsCommonError(t *testing.T) {
127127
}
128128
for i := range commonErrors {
129129
assert.True(t, IsCommonError(commonErrors[i]))
130+
is, rawError := RetrieveCommonError(commonErrors[i])
131+
assert.True(t, is)
132+
assert.ErrorIs(t, rawError, commonErrors[i])
133+
assert.True(t, Any(rawError, commonErrors[i]))
130134
}
131135

132136
assert.False(t, IsCommonError(errors.New(faker.Sentence())))
133137
}
134138

139+
func TestRetrieveCommonError(t *testing.T) {
140+
is, rawError := RetrieveCommonError(nil)
141+
assert.True(t, is)
142+
assert.True(t, Any(rawError, nil))
143+
144+
is, rawError = RetrieveCommonError(errors.New(" "))
145+
assert.True(t, is)
146+
assert.True(t, Any(rawError, nil))
147+
148+
is, rawError = RetrieveCommonError(ErrUndefined)
149+
assert.True(t, is)
150+
assert.ErrorIs(t, rawError, ErrUndefined)
151+
assert.True(t, Any(rawError, ErrUndefined))
152+
153+
is, rawError = RetrieveCommonError(fmt.Errorf("%w: %v", ErrInvalid, faker.Sentence()))
154+
assert.True(t, is)
155+
assert.ErrorIs(t, rawError, ErrInvalid)
156+
assert.True(t, Any(rawError, ErrInvalid))
157+
158+
is, rawError = RetrieveCommonError(fmt.Errorf("%v: %v", ErrCondition.Error(), faker.Sentence()))
159+
assert.True(t, is)
160+
assert.ErrorIs(t, rawError, ErrCondition)
161+
assert.True(t, Any(rawError, ErrCondition))
162+
163+
is, rawError = RetrieveCommonError(errors.New(faker.Sentence()))
164+
assert.False(t, is)
165+
assert.ErrorIs(t, rawError, ErrUnknown)
166+
}
167+
135168
func TestIsWarning(t *testing.T) {
136169
assert.True(t, IsWarning(ErrWarning))
137170
assert.True(t, IsWarning(NewWarningMessage(faker.Sentence())))
@@ -256,6 +289,35 @@ func TestWrapError(t *testing.T) {
256289
assert.True(t, Any(WrapIfNotCommonErrorf(ErrUndefined, errors.New(faker.Sentence()), faker.Sentence()), ErrUndefined))
257290
}
258291

292+
func TestDescribeCircumstance(t *testing.T) {
293+
tmpErr := errors.New(faker.Name())
294+
assert.False(t, IsCommonError(tmpErr))
295+
assert.True(t, Any(DescribeCircumstance(context.Canceled, faker.Sentence()), ErrCancelled))
296+
assert.True(t, Any(DescribeCircumstance(ErrConflict, faker.Sentence()), ErrConflict))
297+
assert.True(t, Any(DescribeCircumstance(New(ErrConflict, faker.Sentence()), faker.Sentence()), ErrConflict))
298+
assert.True(t, Any(DescribeCircumstance(errors.New(ErrConflict.Error()), faker.Sentence()), ErrConflict))
299+
assert.True(t, Any(DescribeCircumstance(tmpErr, faker.Sentence()), ErrUnexpected))
300+
assert.Equal(t, "conflict: some context: conflict: initial error", DescribeCircumstance(New(ErrConflict, "initial error"), "some context").Error())
301+
assert.True(t, Any(DescribeCircumstancef(context.Canceled, "%v", faker.Sentence()), ErrCancelled))
302+
assert.True(t, Any(DescribeCircumstancef(ErrConflict, "%v", faker.Sentence()), ErrConflict))
303+
assert.True(t, Any(DescribeCircumstancef(New(ErrConflict, faker.Sentence()), "%v", faker.Sentence()), ErrConflict))
304+
assert.True(t, Any(DescribeCircumstancef(errors.New(ErrConflict.Error()), "%v", faker.Sentence()), ErrConflict))
305+
assert.True(t, Any(DescribeCircumstancef(tmpErr, "%v", faker.Sentence()), ErrUnexpected))
306+
assert.True(t, Any(DescribeCircumstanceAndKeepType(context.Canceled, faker.Sentence()), ErrCancelled))
307+
assert.True(t, Any(DescribeCircumstanceAndKeepType(ErrConflict, faker.Sentence()), ErrConflict))
308+
assert.True(t, Any(DescribeCircumstanceAndKeepType(New(ErrConflict, faker.Sentence()), faker.Sentence()), ErrConflict))
309+
assert.True(t, Any(DescribeCircumstanceAndKeepType(errors.New(ErrConflict.Error()), faker.Sentence()), ErrConflict))
310+
assert.False(t, Any(DescribeCircumstanceAndKeepType(tmpErr, faker.Sentence()), ErrUnexpected))
311+
assert.ErrorIs(t, DescribeCircumstanceAndKeepType(tmpErr, faker.Sentence()), tmpErr)
312+
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(context.Canceled, "%v", faker.Sentence()), ErrCancelled))
313+
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(ErrConflict, "%v", faker.Sentence()), ErrConflict))
314+
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(New(ErrConflict, faker.Sentence()), "%v", faker.Sentence()), ErrConflict))
315+
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(errors.New(ErrConflict.Error()), "%v", faker.Sentence()), ErrConflict))
316+
assert.False(t, Any(DescribeCircumstanceAndKeepTypef(tmpErr, "%v", faker.Sentence()), ErrUnexpected))
317+
assert.ErrorIs(t, DescribeCircumstanceAndKeepTypef(tmpErr, "%v", faker.Sentence()), tmpErr)
318+
319+
}
320+
259321
func TestString(t *testing.T) {
260322
assert.Equal(t, "unknown", New(nil, "").Error())
261323
assert.Equal(t, "unknown", Newf(nil, "").Error())

0 commit comments

Comments
 (0)