Compare commits

...

2 commits

8 changed files with 272 additions and 0 deletions

2
.gitignore vendored Normal file
View file

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

39
.golangci.yaml Normal file
View file

@ -0,0 +1,39 @@
version: "2"
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- copyloopvar
- dupl
- err113
- errname
- exptostd
- fatcontext
- funlen
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- godox
- gosec
- perfsprint
- testifylint
settings:
testifylint:
enable-all: true
disable:
- require-error
gocognit:
min-complexity: 16
gocyclo:
min-complexity: 15
gocritic:
enable-all: true
settings:
hugeParam:
sizeThreshold: 255

45
AGENTS.md Normal file
View file

@ -0,0 +1,45 @@
# Agent Guidelines for waiterr
This document outlines the conventions and commands for agents operating within the `waiterr` Go project.
## Build/Lint/Test Commands
- **Lint:** `task lint`
- **Test All:** `task test`
- **Test Single File:** `go test -run <TestName> <path/to/file_test.go>` (e.g., `go test -run TestNewHappy ./ezcache_test.go`)
- **Format:** `task fmt`
## Code Style Guidelines
- **Module**: `codeberg.org/danjones000/waiterr`
- **Go version**: 1.24.9
- **Imports:** Group standard library imports separately from third-party imports.
- **Formatting:** Adhere to `go fmt` standards.
- **Naming Conventions:**
- Variables: `camelCase`
- Functions/Methods: `CamelCase` (exported), `camelCase` (unexported)
- 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`
- 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)
- **Style**: Write detailed commit messages that explain what changed and why
- **Examples**: `✨ Add JSON export functionality for log entries`, `🐛 Fix date parsing for RFC3339 timestamps`, `📝 Update README with configuration examples`
## Git Flow Workflow
- **Main branches**: `stable` (production-ready), `develop` (integration branch)
- **Development**: Always commit new features/fixes to `develop` branch or appropriate feature branches
- **Branch prefixes**:
- `feat/feature-name` - New features, merge to `develop` when complete
- `bug/bug-name` - Bug fixes (non-urgent), merge to `develop` when complete
- `rel/version` - Release preparation branches, merge to `stable` and then **also** merge `stable` back to `develop`
- `hot/version` - Hotfixes for production issues follow same merge rules as releases
- **Version tags**: Prefix all version tags with `v` (e.g., `v1.0.2`, `v0.0.6`)
- **Releases**: Update CHANGELOG.md with a summary of changes for each new version
- **Never commit directly to** `stable` branch (only merge from `rel/` or `hot/` branches)
- After merging to `stable`, always merge it back to `develop`
- **Before starting work**: Ensure you're on `develop` branch or create an appropriate feature branch from it

55
Taskfile.yml Normal file
View file

@ -0,0 +1,55 @@
version: '3'
tasks:
default:
cmds:
- task: fmt
- task: test
- task: lint
fmt:
desc: Format go code
sources:
- '**/*.go'
cmds:
- go fmt ./...
- go mod tidy
lint:
desc: Do static analysis
sources:
- '**/*.go'
cmds:
- golangci-lint run
test:
desc: Run unit tests
deps: [fmt]
sources:
- '**/*.go'
generates:
- build/cover.out
cmds:
- go test -race -cover -coverprofile build/cover.out ./...
coverage-report:
desc: Build coverage report
deps: [test]
sources:
- build/cover.out
generates:
- build/cover.html
cmds:
- go tool cover -html=build/cover.out -o build/cover.html
serve-report:
desc: Serve the coverage report
deps: [coverage-report]
cmds:
- ip addr list | grep inet
- python3 -m http.server -d build 3434
serve-docs:
desc: Serve the current docs
cmds:
- godoc -http=0.0.0.0:3434 -play

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module codeberg.org/danjones000/waiterr
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)
}