252 lines
7.2 KiB
Markdown
252 lines
7.2 KiB
Markdown
---
|
|
title: Development Guide
|
|
description: How to build, test, and contribute to go-ratelimit -- prerequisites, test patterns, coding standards, and commit conventions.
|
|
---
|
|
|
|
# Development Guide
|
|
|
|
## Prerequisites
|
|
|
|
- **Go 1.26** or later (the module declares `go 1.26.0`)
|
|
- No CGO required -- `modernc.org/sqlite` is a pure Go port
|
|
|
|
No C toolchain, no system SQLite library, no external build tools. A plain
|
|
`go build ./...` is sufficient.
|
|
|
|
---
|
|
|
|
## Build and Test
|
|
|
|
```bash
|
|
# Run all tests
|
|
go test ./...
|
|
|
|
# Run all tests with the race detector (required before every commit)
|
|
go test -race ./...
|
|
|
|
# Run a single test by name
|
|
go test -v -run TestCanSend ./...
|
|
|
|
# Run a single subtest
|
|
go test -v -run "TestCanSend/RPM_at_exact_limit_is_rejected" ./...
|
|
|
|
# Run benchmarks
|
|
go test -bench=. -benchmem ./...
|
|
|
|
# Run a specific benchmark
|
|
go test -bench=BenchmarkCanSend -benchmem ./...
|
|
|
|
# Check for vet issues
|
|
go vet ./...
|
|
|
|
# Lint (requires golangci-lint)
|
|
golangci-lint run ./...
|
|
|
|
# Tidy dependencies
|
|
go mod tidy
|
|
```
|
|
|
|
All three commands (`go test -race ./...`, `go vet ./...`, and `go mod tidy`)
|
|
must produce no errors or warnings before a commit is pushed.
|
|
|
|
---
|
|
|
|
## Test Organisation
|
|
|
|
### File Layout
|
|
|
|
| File | Scope |
|
|
|------|-------|
|
|
| `ratelimit_test.go` | Core sliding window logic, provider profiles, concurrency, benchmarks |
|
|
| `sqlite_test.go` | SQLite backend, migration, concurrent persistence |
|
|
| `error_test.go` | SQLite and YAML error paths |
|
|
| `iter_test.go` | `Models()` and `Iter()` iterators, `CountTokens` edge cases |
|
|
|
|
All test files are in `package ratelimit` (white-box tests), giving access to
|
|
unexported fields and methods such as `prune()`, `filePath`, and `sqlite`.
|
|
|
|
### Naming Convention
|
|
|
|
SQLite tests follow the `_Good`, `_Bad`, `_Ugly` suffix pattern:
|
|
|
|
- `_Good` -- happy path
|
|
- `_Bad` -- expected error conditions (invalid paths, corrupt input)
|
|
- `_Ugly` -- panic-adjacent edge cases (corrupt database files, truncated files)
|
|
|
|
Core logic tests use plain descriptive names without suffixes, grouped by method
|
|
with table-driven subtests.
|
|
|
|
### Test Helper
|
|
|
|
`newTestLimiter(t)` creates a `RateLimiter` with Gemini defaults and redirects
|
|
the YAML file path into `t.TempDir()`:
|
|
|
|
```go
|
|
func newTestLimiter(t *testing.T) *RateLimiter {
|
|
t.Helper()
|
|
rl, err := New()
|
|
require.NoError(t, err)
|
|
rl.filePath = filepath.Join(t.TempDir(), "ratelimits.yaml")
|
|
return rl
|
|
}
|
|
```
|
|
|
|
Use `t.TempDir()` for all file paths in tests. Go cleans these up automatically
|
|
after each test completes.
|
|
|
|
### Testify Usage
|
|
|
|
Tests use `github.com/stretchr/testify` exclusively:
|
|
|
|
- `require.NoError(t, err)` -- fail immediately on setup errors
|
|
- `assert.NoError(t, err)` -- record failure but continue
|
|
- `assert.Equal(t, expected, actual, "message")` -- prefer over raw comparisons
|
|
- `assert.True` / `assert.False` -- for boolean checks
|
|
- `assert.Empty` / `assert.Len` -- for slice length checks
|
|
- `assert.ErrorIs(t, err, target)` -- for sentinel errors
|
|
|
|
Do not use `t.Error`, `t.Fatal`, or `t.Log` directly.
|
|
|
|
### Concurrency Tests
|
|
|
|
Race tests spin up goroutines and use `sync.WaitGroup`. Some assert specific
|
|
outcomes (e.g., correct RPD count after concurrent recordings), while others
|
|
rely solely on the race detector to catch data races:
|
|
|
|
```go
|
|
var wg sync.WaitGroup
|
|
for range 20 {
|
|
wg.Go(func() {
|
|
for range 50 {
|
|
rl.CanSend(model, 10)
|
|
rl.RecordUsage(model, 5, 5)
|
|
rl.Stats(model)
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
```
|
|
|
|
Always run concurrency tests with `-race`.
|
|
|
|
### Benchmarks
|
|
|
|
The following benchmarks are included:
|
|
|
|
| Benchmark | What it measures |
|
|
|-----------|------------------|
|
|
| `BenchmarkCanSend` | CanSend with a 1,000-entry sliding window |
|
|
| `BenchmarkRecordUsage` | Recording usage on a single model |
|
|
| `BenchmarkCanSendConcurrent` | Parallel CanSend across goroutines |
|
|
| `BenchmarkCanSendWithPrune` | CanSend with 500 old + 500 new entries |
|
|
| `BenchmarkStats` | Stats retrieval with a 1,000-entry window |
|
|
| `BenchmarkAllStats` | AllStats across 5 models x 200 entries each |
|
|
| `BenchmarkPersist` | YAML persistence I/O |
|
|
| `BenchmarkSQLitePersist` | SQLite persistence I/O |
|
|
| `BenchmarkSQLiteLoad` | SQLite state loading |
|
|
|
|
### Coverage
|
|
|
|
Current coverage: 95.1%. The remaining paths cannot be covered in unit tests
|
|
without modifying production code:
|
|
|
|
1. `CountTokens` success path -- the Google API URL is hardcoded; unit tests
|
|
cannot intercept the HTTP call without URL injection support.
|
|
2. `yaml.Marshal` error path in `Persist()` -- `yaml.Marshal` does not fail on
|
|
valid Go structs.
|
|
3. `os.UserHomeDir()` error path in `NewWithConfig()` -- triggered only when
|
|
`$HOME` is unset, which test infrastructure prevents.
|
|
|
|
Do not lower coverage below 95% without a documented reason.
|
|
|
|
---
|
|
|
|
## Coding Standards
|
|
|
|
### Language
|
|
|
|
UK English throughout: colour, organisation, serialise, initialise, behaviour.
|
|
Do not use American spellings in identifiers, comments, or documentation.
|
|
|
|
### Go Style
|
|
|
|
- All exported types, functions, and fields must have doc comments.
|
|
- Error strings must be lowercase and not end with punctuation (Go convention).
|
|
- Contextual errors use `fmt.Errorf("ratelimit.Function: what: %w", err)` so
|
|
errors identify their origin clearly.
|
|
- No `init()` functions.
|
|
- No global mutable state. `DefaultProfiles()` returns a fresh map on each call.
|
|
|
|
### Mutex Discipline
|
|
|
|
The `RateLimiter.mu` mutex is the only synchronisation primitive. Rules:
|
|
|
|
- Methods that call `prune()` always acquire the write lock (`mu.Lock()`), even
|
|
if they appear read-only, because `prune()` mutates state slices.
|
|
- `Persist()` acquires the write lock briefly to clone state, then releases it
|
|
before performing I/O.
|
|
- Lock acquisition always happens at the top of the public method, never inside
|
|
a helper. Helpers document "Caller must hold the lock".
|
|
- Never call a public method from inside another public method while holding the
|
|
lock (deadlock risk).
|
|
|
|
### Dependencies
|
|
|
|
Direct dependencies are intentionally minimal:
|
|
|
|
| Dependency | Purpose |
|
|
|------------|---------|
|
|
| `gopkg.in/yaml.v3` | YAML serialisation for legacy backend |
|
|
| `modernc.org/sqlite` | Pure Go SQLite for persistent backend |
|
|
| `github.com/stretchr/testify` | Test assertions (test-only) |
|
|
|
|
Do not add `database/sql` drivers beyond `modernc.org/sqlite`. Do not add HTTP
|
|
client libraries; the existing `CountTokens` function uses the standard library.
|
|
|
|
---
|
|
|
|
## Licence
|
|
|
|
EUPL-1.2. Confirm with the project lead before adding files under a different
|
|
licence.
|
|
|
|
---
|
|
|
|
## Commit Convention
|
|
|
|
Format: `type(scope): description`
|
|
|
|
Common types: `feat`, `fix`, `test`, `refactor`, `docs`, `perf`, `chore`
|
|
|
|
Common scopes: `ratelimit`, `sqlite`, `persist`, `config`
|
|
|
|
Every commit must include:
|
|
|
|
```
|
|
Co-Authored-By: Virgil <virgil@lethean.io>
|
|
```
|
|
|
|
Example:
|
|
|
|
```
|
|
feat(sqlite): add WAL-mode SQLite backend with migration helper
|
|
|
|
Co-Authored-By: Virgil <virgil@lethean.io>
|
|
```
|
|
|
|
Commits must not be pushed unless `go test -race ./...` and `go vet ./...` both
|
|
pass. `go mod tidy` must produce no changes.
|
|
|
|
---
|
|
|
|
## Linting
|
|
|
|
The project uses `golangci-lint` with the following enabled linters (see
|
|
`.golangci.yml`):
|
|
|
|
- `govet`, `errcheck`, `staticcheck`, `unused`, `gosimple`
|
|
- `ineffassign`, `typecheck`, `gocritic`, `gofmt`
|
|
|
|
Disabled linters: `exhaustive`, `wrapcheck`.
|
|
|
|
Run `golangci-lint run ./...` to check before committing.
|