diff --git a/.gitignore b/.gitignore index 567609b..c525b60 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build/ +.task/ diff --git a/.task/checksum/fmt b/.task/checksum/fmt deleted file mode 100644 index c950c84..0000000 --- a/.task/checksum/fmt +++ /dev/null @@ -1 +0,0 @@ -99aa06d3014798d86001c324468d497f diff --git a/AGENTS.md b/AGENTS.md index 46e2024..33ff925 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,9 @@ This document outlines the conventions and commands for agents operating within - Packages: `lowercase` - **Error Handling:** Return errors explicitly. Check errors immediately after a function call that returns an error. - **Linter Rules:** Refer to `.golangci.yaml` for detailed linting rules. -- **Testing**: Use `github.com/nalgeon/be` +- **Testing**: + - Use `github.com/nalgeon/be` + - Tests should be in a separate package, such as waiterr_test ## Git Commit Guidelines - **Format**: Prepend commit messages with a gitmoji emoji (see https://gitmoji.dev) diff --git a/go.mod b/go.mod index e24fc8f..6d6bf2f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module codeberg.org/danjones000/waiterr go 1.24.9 + +require github.com/nalgeon/be v0.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bd32e32 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/nalgeon/be v0.3.0 h1:QsPANqEtcOD5qT2S3KAtIkDBBn8SXUf/Lb5Bi/z4UqM= +github.com/nalgeon/be v0.3.0/go.mod h1:PMwMuBLopwKJkSHnr2qHyLcZYUTqNejN7A8RAqNWO3E= diff --git a/waiterr.go b/waiterr.go new file mode 100644 index 0000000..3da73bf --- /dev/null +++ b/waiterr.go @@ -0,0 +1,58 @@ +package waiterr + +import ( + "errors" + "sync" +) + +// WaitErr wraps a sync.WaitGroup with error handling. +type WaitErr struct { + wg sync.WaitGroup + errs []error + mut sync.RWMutex +} + +// Go runs f in its own goroutine. When f returns, its error is stored, and returned +// with we.Wait() +func (we *WaitErr) Go(f func() error) { + wrap := func() { + err := f() + we.mut.Lock() + defer we.mut.Unlock() + we.errs = append(we.errs, err) + } + we.wg.Go(wrap) +} + +// WaitForError waits for the first error to be returned by one of our go routines, and immediately returns +// with that error. If all functions return successfully, a nil is returned. +func (we *WaitErr) WaitForError() error { + // Implement this + // If we already have an error, return it immediately without waiting + // If no error has yet been returned, wait for the very first error and return it + return nil +} + +// Wait for all current goroutines to finish. Return an error that combines all errors returned +// in the group so far (if any). +func (we *WaitErr) Wait() error { + we.wg.Wait() + we.mut.RLock() + defer we.mut.RUnlock() + return errors.Join(we.errs...) +} + +// Unwrap returns all non-nil errors returned by our functions. +// If we.errs is empty, or all errors are nil, just return nil. +func (we *WaitErr) Unwrap() []error { + errs := make([]error, 0, len(we.errs)) + for _, e := range we.errs { + if e != nil { + errs = append(errs, e) + } + } + if len(errs) == 0 { + return nil + } + return errs +} diff --git a/waiterr_test.go b/waiterr_test.go new file mode 100644 index 0000000..293f7dc --- /dev/null +++ b/waiterr_test.go @@ -0,0 +1,66 @@ +package waiterr_test + +import ( + "errors" + "testing" + + "github.com/nalgeon/be" + + "codeberg.org/danjones000/waiterr" +) + +func TestGo(t *testing.T) { + we := new(waiterr.WaitErr) + err := errors.New("uh-oh") + var run bool + we.Go(func() error { + run = true + return err + }) + + be.Err(t, we.Wait(), err) + be.True(t, run) +} + +func TestWait(t *testing.T) { + we := new(waiterr.WaitErr) + er1 := errors.New("uh-oh") + er2 := errors.New("oops") + we.Go(func() error { return er1 }) + we.Go(func() error { return nil }) + we.Go(func() error { return er2 }) + + err := we.Wait() + be.Err(t, err, er1, er2) + if ers, ok := err.(interface{ Unwrap() []error }); ok { + all := ers.Unwrap() + be.Equal(t, len(all), 2) + be.True(t, all[0] == er1 || all[0] == er2) + be.True(t, all[1] == er2 || all[1] == er1) + } else { + t.Fatal("Returned error should have Unwrap method") + } +} + +func TestWaitForError(t *testing.T) { + we := new(waiterr.WaitErr) + er1 := errors.New("uh-oh") + er2 := errors.New("oops") + we.Go(func() error { return nil }) + we.Go(func() error { return er1 }) + we.Go(func() error { return er2 }) + + err := we.WaitForError() + // Due to how goroutines run, it is possible that either of those return first. This is an acceptable limitation + be.True(t, err == er1 || err == er2) +} + +func TestWaitForErrorNoErr(t *testing.T) { + we := new(waiterr.WaitErr) + we.Go(func() error { return nil }) + we.Go(func() error { return nil }) + we.Go(func() error { return nil }) + + err := we.WaitForError() + be.Err(t, err, nil) +}