Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 71 additions & 13 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ name: Go
on: [push]

jobs:
build:
services:
redis:
# Docker Hub image
image: redis
ports:
- 6379:6379
unit-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5
- name: Setup Go
Expand All @@ -19,21 +12,86 @@ jobs:
go-version-file: 'go.mod'
- name: Install dependencies
run: go mod vendor
- name: Run unit tests (no Redis required)
run: go test -short -v ./...

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.6.1
args: --timeout=5m
only-new-issues: false
- name: Test with Go

integration-tests:
services:
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Install dependencies
run: go mod vendor
- name: Run integration tests
run: |
set +e
go test -tags=integration -json ./... > TestResults-integration.json
TEST_EXIT_CODE=$?
exit $TEST_EXIT_CODE
- name: Upload integration test results
uses: actions/upload-artifact@v4
if: always()
with:
name: Integration-results
path: TestResults-integration.json

distributed-tests:
services:
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Install dependencies
run: go mod vendor
- name: Run distributed tests
run: |
set +e
go test -tags=examples -json ./... > TestResults.json
go test -tags=distributed -json ./... > TestResults-distributed.json
TEST_EXIT_CODE=$?
exit $TEST_EXIT_CODE
- name: Upload Go test results
- name: Upload distributed test results
uses: actions/upload-artifact@v4
if: always()
with:
name: Go-results
path: TestResults.json
name: Distributed-results
path: TestResults-distributed.json
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ run:
modules-download-mode: readonly
build-tags:
- examples
- integration
- distributed
- cluster

formatters:
enable:
Expand Down
17 changes: 17 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Mockery v3 Configuration
# Generate moq-style mocks for all internal interfaces

# Use matryer/moq style templates
template: matryer

# Global output configuration
dir: "mocks/{{.SrcPackageName}}"
pkgname: "mock{{.SrcPackageName}}"
filename: "mock_{{.InterfaceName}}.go"

# Packages to scan
packages:
github.com/dcbickfo/redcache/internal:
config:
all: true
recursive: true
98 changes: 72 additions & 26 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,37 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

### Testing

```bash
# Run all tests (requires Redis on localhost:6379)
make test
redcache uses Go build tags to organize tests by category:

# Run tests with single test
go test . -run TestName -v
```bash
# Unit tests only (no Redis required, <0.3s)
make test-unit
go test -short ./...

# Run tests with race detector
go test . -race -count=1

# Run specific package tests
go test ./internal/writelock -v
# Integration tests (requires Redis on localhost:6379)
make test-integration
go test -tags=integration ./...

# Distributed tests (multi-client coordination)
make test-distributed
go test -tags=distributed ./...

# Redis Cluster tests (requires cluster on ports 7000-7005)
# Redis Cluster tests (requires cluster on ports 17000-17005)
make test-cluster
go test -tags=cluster ./...

# Run multiple test suites
go test -tags="integration,distributed" ./...

# Complete test suite (unit + distributed + cluster + examples)
make test-complete
# All tests (comprehensive: unit + integration + distributed + cluster + examples)
make test
go test -tags="integration,distributed,cluster,examples" ./...

# Run specific test with race detector
go test -tags=integration -run TestName -race -count=1 -v

# Run specific package tests
go test -tags=integration ./internal/writelock -v
```

### Docker/Redis
Expand Down Expand Up @@ -210,21 +220,50 @@ The linter enforces cognitive complexity < 15 per function. When implementing co

### Testing Patterns

**Test Categories:**
1. **Unit tests**: Basic functionality, single client
2. **Distributed tests** (`*_distributed_test.go`): Multi-client coordination
3. **Cluster tests** (`*_cluster_test.go`): Redis Cluster specific (gracefully skip if cluster unavailable)
4. **Edge tests** (`*_edge_test.go`): Race conditions, context cancellation, error handling
**Test Categories with Build Tags:**
1. **Unit tests** (`*_unit_test.go`): No build tag, use `-short` flag. Fast isolated tests with mocks, no Redis required.
2. **Integration tests** (`*_test.go`): `//go:build integration` tag. Test with real Redis for end-to-end functionality.
3. **Distributed tests** (`*_distributed_test.go`): `//go:build distributed` tag. Multi-client coordination tests.
4. **Cluster tests** (`*_cluster_test.go`): `//go:build cluster` tag. Redis Cluster specific tests (ports 17000-17005).
5. **Examples** (`examples/*_test.go`): `//go:build examples` tag. Runnable documentation examples.

**Test Organization - IMPORTANT:**
- **Always use subtests** with `t.Run()` for test organization
- **Always use `t.Context()`** instead of `context.Background()` in tests
- **Unit tests must check `testing.Short()`**: Skip when NOT in short mode (`if !testing.Short() { t.Skip(...) }`)

**Test Naming Convention:**
- `Test<Component>_<Operation>_<Scenario>`
- Example: `TestPrimeableCacheAside_SetMulti_ConcurrentWrites`
- Parent test: `Test<Component>_<Operation>`
- Subtests: Descriptive names like `"CacheHit"`, `"CacheMiss_LockAcquired"`, `"CallbackError"`
- Example: `TestCacheAside_Get` with subtests `"CacheHit"` and `"CacheMiss_LockAcquired"`

**Test Structure Pattern:**
```go
func TestCacheAside_Get(t *testing.T) {
if !testing.Short() {
t.Skip("Skipping unit test in non-short mode")
}

// Shared setup here if needed

t.Run("CacheHit", func(t *testing.T) {
ctx := t.Context() // Use test context
// ... test logic
})

t.Run("CacheMiss_LockAcquired", func(t *testing.T) {
ctx := t.Context()
// ... test logic
})
}
```

**Redis Setup:**
- Single Redis: Tests assume `localhost:6379`
- Cluster: Tests check ports 7000-7005, skip if unavailable
- Cluster: Tests use ports 17000-17005
- Always use `makeClient(t)` helper for setup
- Always defer `client.Close()`
- Always use `t.Context()` for automatic cleanup

### Benchmarking Best Practices

Expand Down Expand Up @@ -263,18 +302,21 @@ go tool pprof -inuse_space mem.prof

3. **Test sequence**:
```bash
# Unit tests
go test . -run TestNewFeature -v
# Unit tests (with mocks, no Redis)
go test -short -run TestNewFeature -v

# Integration tests (with real Redis)
go test -tags=integration -run TestNewFeature -v

# Race detector
go test . -run TestNewFeature -race -count=1
go test -tags=integration -run TestNewFeature -race -count=1

# Distributed coordination
go test . -run TestNewFeature_Distributed -v
go test -tags=distributed -run TestNewFeature -v

# Cluster support
make docker-cluster-up
go test . -run TestNewFeature_Cluster -v
go test -tags=cluster -run TestNewFeature -v
```

4. **Linter verification**:
Expand Down Expand Up @@ -353,9 +395,13 @@ redcache/
5. **Don't create high cognitive complexity** - Extract helper functions to stay under 15
6. **Don't forget cleanup in tests** - Always `defer client.Close()` and `defer cleanup()`
7. **Don't assume keys are in same slot** - Even similar-looking keys may hash differently
8. **Don't use `context.Background()` in tests** - Always use `t.Context()` for automatic cleanup
9. **Don't skip organizing tests with subtests** - Always use `t.Run()` for better structure and isolation
10. **Don't forget build tags** - Unit tests need `testing.Short()` check, integration tests need `//go:build integration` tag

## Additional Resources

- See `TESTING.md` for comprehensive testing guide (build tags, mocks, best practices)
- See `README.md` for user-facing documentation
- See `DISTRIBUTED_LOCK_SAFETY.md` for lock safety analysis
- See `REDIS_CLUSTER.md` for cluster deployment guide
Expand Down
46 changes: 34 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help test test-fast test-unit test-distributed test-cluster test-examples test-coverage lint lint-fix build clean vendor install-tools docker-up docker-down docker-cluster-up docker-cluster-down
.PHONY: help test test-fast test-unit test-unit-mocked test-integration test-distributed test-cluster test-examples test-coverage lint lint-fix build clean vendor install-tools mocks docker-up docker-down docker-cluster-up docker-cluster-down

# Colors for output
CYAN := \033[36m
Expand All @@ -18,12 +18,16 @@ help:
@echo "$(BOLD)Testing:$(RESET)"
@echo " $(CYAN)make test$(RESET) - Run all tests including examples (default)"
@echo " $(CYAN)make test-fast$(RESET) - Run all tests quickly (no examples)"
@echo " $(CYAN)make test-unit$(RESET) - Run unit tests only (no distributed/cluster)"
@echo " $(CYAN)make test-unit$(RESET) - Run unit tests with mocks (no Redis required)"
@echo " $(CYAN)make test-integration$(RESET) - Run integration tests (requires Redis)"
@echo " $(CYAN)make test-distributed$(RESET) - Run distributed tests (multi-client coordination)"
@echo " $(CYAN)make test-cluster$(RESET) - Run Redis cluster tests (requires cluster)"
@echo " $(CYAN)make test-examples$(RESET) - Run example tests only"
@echo " $(CYAN)make test-coverage$(RESET) - Run tests with coverage report"
@echo ""
@echo "$(BOLD)Mocking:$(RESET)"
@echo " $(CYAN)make mocks$(RESET) - Generate all mocks using mockery v3"
@echo ""
@echo "$(BOLD)Docker/Redis:$(RESET)"
@echo " $(CYAN)make docker-up$(RESET) - Start single Redis instance"
@echo " $(CYAN)make docker-down$(RESET) - Stop single Redis instance"
Expand Down Expand Up @@ -53,37 +57,49 @@ install-tools:
@echo "$(BOLD)Installed versions:$(RESET)"
@asdf current

# Run all tests including examples (default, most comprehensive)
# Run all tests: unit + integration + distributed + cluster + examples (default, most comprehensive)
test:
@echo "$(YELLOW)Running all tests including examples...$(RESET)"
@echo "$(YELLOW)Running all tests (unit + integration + distributed + cluster + examples)...$(RESET)"
@echo "$(YELLOW)Note: Requires Redis on localhost:6379 AND Redis Cluster on localhost:17000-17005$(RESET)"
@echo "$(YELLOW) Start with: make docker-up && make docker-cluster-up$(RESET)"
@go test -tags=examples -v ./... && echo "$(GREEN)✓ All tests passed!$(RESET)" || (echo "$(RED)✗ Tests failed!$(RESET)" && exit 1)
@echo ""
@echo "$(CYAN)Step 1/2: Running unit tests (no Redis)...$(RESET)"
@go test -v ./... && echo "$(GREEN)✓ Unit tests passed!$(RESET)" || (echo "$(RED)✗ Unit tests failed!$(RESET)" && exit 1)
@echo ""
@echo "$(CYAN)Step 2/2: Running integration/distributed/cluster/examples tests...$(RESET)"
@go test -tags="integration,distributed,cluster,examples" -v ./... && echo "$(GREEN)✓ All integration tests passed!$(RESET)" || (echo "$(RED)✗ Integration tests failed!$(RESET)" && exit 1)
@echo ""
@echo "$(GREEN)$(BOLD)✓ All tests passed!$(RESET)"

# Run tests quickly without examples
test-fast:
@echo "$(YELLOW)Running tests (no examples)...$(RESET)"
@echo "$(YELLOW)Note: Requires Redis on localhost:6379 AND Redis Cluster on localhost:17000-17005$(RESET)"
@echo "$(YELLOW) Start with: make docker-up && make docker-cluster-up$(RESET)"
@go test -v ./... && echo "$(GREEN)✓ Tests passed!$(RESET)" || (echo "$(RED)✗ Tests failed!$(RESET)" && exit 1)
@go test -tags="integration,distributed,cluster" -v ./... && echo "$(GREEN)✓ Tests passed!$(RESET)" || (echo "$(RED)✗ Tests failed!$(RESET)" && exit 1)

# Run only unit tests (no distributed or cluster tests)
# Run only unit tests (no Redis required, no build tags)
test-unit:
@echo "$(YELLOW)Running unit tests only (excluding distributed and cluster tests)...$(RESET)"
@echo "$(YELLOW)Running unit tests (no Redis required)...$(RESET)"
@go test -v ./... && echo "$(GREEN)✓ Unit tests passed!$(RESET)" || (echo "$(RED)✗ Unit tests failed!$(RESET)" && exit 1)

# Run integration tests (requires Redis)
test-integration:
@echo "$(YELLOW)Running integration tests (requires Redis)...$(RESET)"
@echo "$(YELLOW)Note: Requires Redis on localhost:6379$(RESET)"
@go test -v -run '^Test[^_]*$$|TestCacheAside_Get$$|TestCacheAside_GetMulti$$|TestPrimeableCacheAside_Set$$' ./... && echo "$(GREEN)✓ Unit tests passed!$(RESET)" || (echo "$(RED)✗ Unit tests failed!$(RESET)" && exit 1)
@go test -tags=integration -v ./... && echo "$(GREEN)✓ Integration tests passed!$(RESET)" || (echo "$(RED)✗ Integration tests failed!$(RESET)" && exit 1)

# Run distributed tests (multi-client, single Redis instance)
test-distributed:
@echo "$(YELLOW)Running distributed tests (multi-client coordination)...$(RESET)"
@echo "$(YELLOW)Note: Requires Redis on localhost:6379 (start with: make docker-up)$(RESET)"
@go test -v -run 'Distributed' ./... && echo "$(GREEN)✓ Distributed tests passed!$(RESET)" || (echo "$(RED)✗ Distributed tests failed!$(RESET)" && exit 1)
@go test -tags=distributed -v ./... && echo "$(GREEN)✓ Distributed tests passed!$(RESET)" || (echo "$(RED)✗ Distributed tests failed!$(RESET)" && exit 1)

# Run Redis cluster tests (requires cluster setup)
test-cluster:
@echo "$(YELLOW)Running Redis cluster tests...$(RESET)"
@echo "$(YELLOW)Note: Requires Redis Cluster on localhost:17000-17005 (start with: make docker-cluster-up)$(RESET)"
@go test -v -run 'Cluster' ./... && echo "$(GREEN)✓ Cluster tests passed!$(RESET)" || (echo "$(RED)✗ Cluster tests failed!$(RESET)" && exit 1)
@go test -tags=cluster -v ./... && echo "$(GREEN)✓ Cluster tests passed!$(RESET)" || (echo "$(RED)✗ Cluster tests failed!$(RESET)" && exit 1)

# Run example tests with build tag
test-examples:
Expand All @@ -95,7 +111,7 @@ test-coverage:
@echo "$(YELLOW)Running tests with coverage...$(RESET)"
@echo "$(YELLOW)Note: Requires Redis on localhost:6379 AND Redis Cluster on localhost:17000-17005$(RESET)"
@echo "$(YELLOW) Start with: make docker-up && make docker-cluster-up$(RESET)"
@go test -v -coverprofile=coverage.out -covermode=atomic ./... && echo "$(GREEN)✓ Tests passed!$(RESET)" || (echo "$(RED)✗ Tests failed!$(RESET)" && exit 1)
@go test -tags="integration,distributed,cluster" -v -coverprofile=coverage.out -covermode=atomic ./... && echo "$(GREEN)✓ Tests passed!$(RESET)" || (echo "$(RED)✗ Tests failed!$(RESET)" && exit 1)
@echo ""
@echo "$(CYAN)Coverage report:$(RESET)"
@go tool cover -func=coverage.out | tail -1
Expand All @@ -122,6 +138,12 @@ build-examples:
@echo "$(YELLOW)Building all packages (including examples)...$(RESET)"
@go build -tags=examples ./... && echo "$(GREEN)✓ Build successful!$(RESET)" || (echo "$(RED)✗ Build failed!$(RESET)" && exit 1)

# Generate mocks using mockery v3
mocks:
@echo "$(YELLOW)Generating mocks with mockery v3...$(RESET)"
@command -v mockery >/dev/null 2>&1 || { echo "$(RED)Error: mockery is not installed. Install with: go install github.com/vektra/mockery/v3@latest$(RESET)"; exit 1; }
@mockery --config .mockery.yaml && echo "$(GREEN)✓ Mocks generated successfully!$(RESET)" || (echo "$(RED)✗ Mock generation failed!$(RESET)" && exit 1)

# Clean build artifacts
clean:
@echo "$(YELLOW)Cleaning build artifacts...$(RESET)"
Expand Down
Loading