From 8c595a9080f5a77de356df252a406032b6ffd7d0 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Thu, 13 Nov 2025 11:52:56 -0600 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Initial=20project=20setup=20wit?= =?UTF-8?q?h=20basic=20scaffolding,=20build=20tools,=20and=20agent=20guide?= =?UTF-8?q?lines.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .golangci.yaml | 39 ++++++++++++++++++++++++++++++++ .task/checksum/fmt | 1 + AGENTS.md | 43 ++++++++++++++++++++++++++++++++++++ Taskfile.yml | 55 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +++ 6 files changed, 142 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 .task/checksum/fmt create mode 100644 AGENTS.md create mode 100644 Taskfile.yml create mode 100644 go.mod diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..3f33683 --- /dev/null +++ b/.golangci.yaml @@ -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 diff --git a/.task/checksum/fmt b/.task/checksum/fmt new file mode 100644 index 0000000..c950c84 --- /dev/null +++ b/.task/checksum/fmt @@ -0,0 +1 @@ +99aa06d3014798d86001c324468d497f diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..46e2024 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# 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 ` (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` + +## 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 diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..049fc9a --- /dev/null +++ b/Taskfile.yml @@ -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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e24fc8f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module codeberg.org/danjones000/waiterr + +go 1.24.9 From 635126fc980270fa46f5434829d8f298e4b48010 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Thu, 13 Nov 2025 14:01:30 -0600 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20Implement=20core=20waiterr=20pa?= =?UTF-8?q?ckage=20with=20initial=20functionality=20and=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .task/checksum/fmt | 1 - AGENTS.md | 4 ++- go.mod | 2 ++ go.sum | 2 ++ waiterr.go | 58 ++++++++++++++++++++++++++++++++++++++++ waiterr_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) delete mode 100644 .task/checksum/fmt create mode 100644 go.sum create mode 100644 waiterr.go create mode 100644 waiterr_test.go 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) +}