diff --git a/internal/cleanups/composite.go b/internal/cleanups/composite.go new file mode 100644 index 0000000000..b724473ee7 --- /dev/null +++ b/internal/cleanups/composite.go @@ -0,0 +1,42 @@ +package cleanups + +import ( + "github.com/docker/docker/internal/multierror" +) + +type Composite struct { + cleanups []func() error +} + +// Add adds a cleanup to be called. +func (c *Composite) Add(f func() error) { + c.cleanups = append(c.cleanups, f) +} + +// Call calls all cleanups in reverse order and returns an error combining all +// non-nil errors. +func (c *Composite) Call() error { + err := call(c.cleanups) + c.cleanups = nil + return err +} + +// Release removes all cleanups, turning Call into a no-op. +// Caller still can call the cleanups by calling the returned function +// which is equivalent to calling the Call before Release was called. +func (c *Composite) Release() func() error { + cleanups := c.cleanups + c.cleanups = nil + return func() error { + return call(cleanups) + } +} + +func call(cleanups []func() error) error { + var errs []error + for idx := len(cleanups) - 1; idx >= 0; idx-- { + c := cleanups[idx] + errs = append(errs, c()) + } + return multierror.Join(errs...) +} diff --git a/internal/cleanups/composite_test.go b/internal/cleanups/composite_test.go new file mode 100644 index 0000000000..313256d0f1 --- /dev/null +++ b/internal/cleanups/composite_test.go @@ -0,0 +1,53 @@ +package cleanups + +import ( + "errors" + "fmt" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestCall(t *testing.T) { + c := Composite{} + var err1 = errors.New("error1") + var err2 = errors.New("error2") + var errX = errors.New("errorX") + var errY = errors.New("errorY") + var errZ = errors.New("errorZ") + var errYZ = errors.Join(errY, errZ) + + c.Add(func() error { + return err1 + }) + c.Add(func() error { + return nil + }) + c.Add(func() error { + return fmt.Errorf("something happened: %w", err2) + }) + c.Add(func() error { + return errors.Join(errX, fmt.Errorf("joined: %w", errYZ)) + }) + + err := c.Call() + + errs := err.(interface{ Unwrap() []error }).Unwrap() + + assert.Check(t, is.ErrorContains(err, err1.Error())) + assert.Check(t, is.ErrorContains(err, err2.Error())) + assert.Check(t, is.ErrorContains(err, errX.Error())) + assert.Check(t, is.ErrorContains(err, errY.Error())) + assert.Check(t, is.ErrorContains(err, errZ.Error())) + assert.Check(t, is.ErrorContains(err, "something happened: "+err2.Error())) + + t.Logf(err.Error()) + assert.Assert(t, is.Len(errs, 3)) + + // Cleanups executed in reverse order. + assert.Check(t, is.ErrorIs(errs[2], err1)) + assert.Check(t, is.ErrorIs(errs[1], err2)) + assert.Check(t, is.ErrorIs(errs[0], errX)) + assert.Check(t, is.ErrorIs(errs[0], errYZ)) +}