Implement core waiterr package with initial functionality and tests.

This commit is contained in:
Dan Jones 2025-11-13 14:01:30 -06:00
commit 635126fc98
7 changed files with 132 additions and 2 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
build/ build/
.task/

View file

@ -1 +0,0 @@
99aa06d3014798d86001c324468d497f

View file

@ -21,7 +21,9 @@ This document outlines the conventions and commands for agents operating within
- Packages: `lowercase` - Packages: `lowercase`
- **Error Handling:** Return errors explicitly. Check errors immediately after a function call that returns an error. - **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. - **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 ## Git Commit Guidelines
- **Format**: Prepend commit messages with a gitmoji emoji (see https://gitmoji.dev) - **Format**: Prepend commit messages with a gitmoji emoji (see https://gitmoji.dev)

2
go.mod
View file

@ -1,3 +1,5 @@
module codeberg.org/danjones000/waiterr module codeberg.org/danjones000/waiterr
go 1.24.9 go 1.24.9
require github.com/nalgeon/be v0.3.0

2
go.sum Normal file
View file

@ -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=

58
waiterr.go Normal file
View file

@ -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
}

66
waiterr_test.go Normal file
View file

@ -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)
}