From 484623180f050781aa555c3a8a8abb6ec64b52e0 Mon Sep 17 00:00:00 2001 From: Facu Date: Mon, 16 Dec 2024 10:57:53 +0100 Subject: [PATCH] Caution package (#376) * add caution lib * refactor and add documentation --- utils/caution/caution.go | 69 +++++++++++++++++++++++++++++++++++ utils/caution/caution_test.go | 68 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 utils/caution/caution.go create mode 100644 utils/caution/caution_test.go diff --git a/utils/caution/caution.go b/utils/caution/caution.go new file mode 100644 index 000000000..87c8dc860 --- /dev/null +++ b/utils/caution/caution.go @@ -0,0 +1,69 @@ +// package caution provides utility functions for handling errors in defer +// cleanup and closing resources. +package caution + +import ( + "errors" + "fmt" + "io" +) + +// ExecuteAndReportError is intended for use in production code to handle errors +// ignored in defer clean ups. +// +// - The first argument is the error variable where the error, if any, will be +// accumulated. If it already contains an error, the new error will be combined. +// - The second argument is a cleanup function that may return an error. This +// function will always be executed. +// - The third argument is a mandatory context message that will be used in the +// error if the cleanup function fails. +// +// Usage example: +// +// Original code: +// +// func F(....) error { +// [...] +// defer f.CleanUpThatMayFail(someArg) +// [...] +// } +// +// Refactored with the new functions: +// +// func F(....) (err error) { +// [...] +// defer ExecuteAndReportError(&err, f() error {f.CleanUpThatMayFail(someArg) }, "failed to cleanup f") +// [...] +// } +func ExecuteAndReportError(err *error, f func() error, message string) { + fErr := f() + if fErr != nil { + *err = errors.Join(*err, fmt.Errorf("%s: %w", message, fErr)) + } +} + +// CloseAndReportError is specialization of ExecuteAndReportError for types that +// implement the Closer interface, to add error management in the +// `defer f.Close()` pattern. +// Usage example: +// +// Original code: +// +// func F(....) error { +// [...] +// defer f.Close() +// [...] +// } +// +// Refactored with the new functions: +// +// func F(....) (err error) { +// [...] +// defer CloseAndReportError(&err, f, "failed to close f") +// [...] +// } +func CloseAndReportError(err *error, closer io.Closer, message string) { + ExecuteAndReportError(err, func() error { + return closer.Close() + }, message) +} diff --git a/utils/caution/caution_test.go b/utils/caution/caution_test.go new file mode 100644 index 000000000..a89a3a22c --- /dev/null +++ b/utils/caution/caution_test.go @@ -0,0 +1,68 @@ +package caution + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExecuteAndReportError_ExecutesAndReturnsError(t *testing.T) { + var err error + ExecuteAndReportError(&err, func() error { + return nil + }, "message") + require.NoError(t, err) + someError := fmt.Errorf("someError") + ExecuteAndReportError(&err, func() error { + return someError + }, "message") + require.ErrorIs(t, err, someError) +} + +func TestExecuteAndReportError_ExecutesAndCombinesErrors(t *testing.T) { + firstError := fmt.Errorf("firstError") + err := firstError + ExecuteAndReportError(&err, func() error { + return nil + }, "message") + require.ErrorIs(t, err, firstError) + + ExecuteAndReportError(&err, func() error { + return fmt.Errorf("secondError") + }, "message") + require.ErrorContains(t, err, "firstError") + require.ErrorContains(t, err, "secondError") +} + +type closeMe struct { + err error +} + +func (c *closeMe) Close() error { + return c.err +} + +func TestCloseAndReportError_(t *testing.T) { + file := &closeMe{} + var err error + CloseAndReportError(&err, file, "message") + require.NoError(t, err) + + file.err = fmt.Errorf("someError") + CloseAndReportError(&err, file, "message") + require.ErrorContains(t, err, "message: someError") +} + +func TestCloseAndReportError_UsagePatternPropagatesError(t *testing.T) { + expectedError := fmt.Errorf("someError") + + testFun := func() (outErr error) { + file := &closeMe{err: expectedError} + defer CloseAndReportError(&outErr, file, "message") + return + } + + gotError := testFun() + require.ErrorIs(t, gotError, expectedError) +}