# Feature Specification: Development Guidelines & Best Practices **ID:** DEV-001 **Version:** 1.0 **Status:** Planned **Priority:** High ## Overview Guidelines for development workflow, code quality, testing, and contribution standards for OpenSpeak project. ## Development Workflow ### Setting Up Development Environment #### Prerequisites - Go 1.21+ - Git - Protobuf compiler (protoc) v3.20+ - Visual Studio Code or preferred IDE - Audio libraries (PortAudio, libopus) #### Initial Setup ```bash # Clone repository git clone https://github.com/yourusername/openspeak.git cd openspeak # Install dependencies go mod download go mod tidy # Install tools go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest # Setup pre-commit hook cat > .git/hooks/pre-commit << 'EOF' #!/bin/bash go fmt ./... golangci-lint run ./... go test ./... EOF chmod +x .git/hooks/pre-commit ``` ### Development Commands #### Build ```bash # Build server go build -o bin/openspeak-server ./cmd/openspeak-server # Build client go build -o bin/openspeak-client ./cmd/openspeak-client # Build all make build # Debug build (with symbols) go build -gcflags="all=-N -l" -o bin/openspeak-server ./cmd/openspeak-server ``` #### Run ```bash # Run server with default config ./bin/openspeak-server # Run with custom config ./bin/openspeak-server --config config.dev.yaml # Run with debug logging OPENSPEAK_LOG_LEVEL=debug ./bin/openspeak-server # Run client ./bin/openspeak-client ``` #### Test ```bash # Run all tests go test ./... # Run with coverage go test -cover ./... # Generate coverage report go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out # Run specific test go test -run TestChannelManager ./internal/channel # Run with verbose output go test -v ./... # Run benchmarks go test -bench=. -benchmem ./... # Run tests with race detector go test -race ./... ``` #### Code Quality ```bash # Format code go fmt ./... # Run linters golangci-lint run ./... # Vet code go vet ./... # Check imports goimports -w ./... # Staticcheck go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... ``` #### Protocol Buffers ```bash # Generate protobuf code make proto # Equivalent to: protoc --go_out=. --go-grpc_out=. proto/*.proto # Watch for changes watch -n 1 'make proto' ``` #### Documentation ```bash # Generate documentation go doc ./... # View package docs godoc -http=:6060 # Then open http://localhost:6060 ``` ### Makefile ```makefile .PHONY: build test lint fmt clean proto run-server run-client coverage all: fmt lint test build build: @echo "Building server and client..." go build -o bin/openspeak-server ./cmd/openspeak-server go build -o bin/openspeak-client ./cmd/openspeak-client test: @echo "Running tests..." go test -v -race -coverprofile=coverage.out ./... go tool cover -func=coverage.out lint: @echo "Running linters..." go fmt ./... go vet ./... golangci-lint run ./... fmt: @echo "Formatting code..." go fmt ./... goimports -w ./... clean: @echo "Cleaning..." rm -rf bin/ rm -f coverage.out proto: @echo "Generating protobuf code..." protoc --go_out=. --go-grpc_out=. proto/*.proto run-server: go run ./cmd/openspeak-server -- --config config.dev.yaml run-client: go run ./cmd/openspeak-client coverage: @echo "Generating coverage report..." go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html @echo "Report saved to coverage.html" coverage-watch: watch -n 2 'make coverage' bench: @echo "Running benchmarks..." go test -bench=. -benchmem ./... help: @echo "Available targets:" @grep -E '^[a-zA-Z_-]+:' Makefile | sed 's/:.*//g' | sort ``` ## Code Style & Conventions ### Go Code Style Follow [Effective Go](https://golang.org/doc/effective_go) and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). #### Naming Conventions ```go // Interfaces: adjective names (Reader, Writer, Closer) type Reader interface { Read(p []byte) (n int, err error) } // Packages: short, single-word names package voice // Exported: PascalCase func PublishVoicePacket(packet *VoicePacket) error {} // Unexported: camelCase func publishVoicePacket(packet *voicePacket) error {} // Constants: UPPER_SNAKE_CASE or PascalCase const ( DefaultBitrate = 64 MaxBitrate = 128 ) // Errors: errors.New or fmt.Errorf, start with lowercase or "Error" prefix var ErrChannelNotFound = errors.New("channel not found") var ErrInvalidBitrate = fmt.Errorf("invalid bitrate: %d", bitrate) // Receiver names: short (1-2 chars) func (m *Manager) CreateChannel(name string) (*Channel, error) {} ``` #### File Organization ```go // 1. Package declaration package channel // 2. Imports (stdlib, third-party, internal) import ( "context" "sync" "google.golang.org/grpc" "openspeak/internal/logger" ) // 3. Constants const ( DefaultMaxUsers = 0 // unlimited ) // 4. Errors var ( ErrChannelNotFound = errors.New("channel not found") ErrChannelFull = errors.New("channel is full") ) // 5. Type definitions type Channel struct { ID string Name string Members []string MaxUsers int CreatedAt time.Time } // 6. Receiver methods (sorted by type) func (c *Channel) IsFull() bool { return c.MaxUsers > 0 && len(c.Members) >= c.MaxUsers } // 7. Package-level functions func NewChannel(name string) *Channel { return &Channel{ ID: generateID(), Name: name, Members: []string{}, MaxUsers: 0, CreatedAt: time.Now(), } } ``` #### Error Handling ```go // Good: Simple, clear error propagation func (m *Manager) CreateChannel(name string) (*Channel, error) { if err := validateName(name); err != nil { return nil, fmt.Errorf("validate name: %w", err) } channel := NewChannel(name) if err := m.store.Save(channel); err != nil { return nil, fmt.Errorf("save channel: %w", err) } return channel, nil } // Bad: Ignoring errors func (m *Manager) CreateChannel(name string) (*Channel, error) { validateName(name) // Error ignored! channel := NewChannel(name) m.store.Save(channel) // Error ignored! return channel, nil } // Bad: Generic error messages return nil, errors.New("error") // Useless error message ``` #### Comments ```go // Good: Explains purpose, not what code does // ChannelManager handles creation and deletion of voice channels. // It maintains the current state of all channels and enforces permissions. type Manager struct { channels map[string]*Channel mu sync.RWMutex } // Bad: Explains code, not purpose // This is a map of channels channels := make(map[string]*Channel) // Good: Exported functions have godoc // PublishVoicePacket accepts a voice packet from a client and broadcasts it // to all members of the packet's channel. func PublishVoicePacket(packet *VoicePacket) error { // ... } // Bad: No comment on exported function func PublishVoicePacket(packet *VoicePacket) error { // ... } ``` ### Project Layout ``` openspeak/ ├── cmd/ # Executable entry points │ ├── openspeak-server/ │ │ └── main.go │ └── openspeak-client/ │ └── main.go ├── internal/ # Private packages (not importable from outside) │ ├── auth/ │ ├── channel/ │ ├── presence/ │ ├── voice/ │ ├── config/ │ ├── logger/ │ └── grpc/ ├── proto/ # Protocol buffer definitions │ ├── common.proto │ ├── auth.proto │ ├── channel.proto │ ├── presence.proto │ └── voice.proto ├── pkg/ # Generated code (protobuf) │ └── api/ │ └── openspeak/ │ └── v1/ ├── config/ # Configuration files │ ├── config.yaml │ └── config.dev.yaml ├── test/ # Test utilities and fixtures │ ├── fixtures/ │ └── mocks/ ├── Makefile ├── go.mod ├── go.sum ├── README.md ├── CONTRIBUTING.md └── LICENSE ``` ## Testing Standards ### Test Structure ```go // File: internal/channel/channel_test.go package channel import ( "testing" ) // TestChannelCreation tests basic channel creation func TestChannelCreation(t *testing.T) { // Arrange name := "general" // Act channel := NewChannel(name) // Assert if channel.Name != name { t.Errorf("expected name %q, got %q", name, channel.Name) } if channel.ID == "" { t.Error("channel ID should not be empty") } } // TestChannelIsFull tests capacity checking func TestChannelIsFull(t *testing.T) { tests := []struct { name string maxUsers int members int want bool }{ {"unlimited capacity", 0, 100, false}, {"not full", 10, 5, false}, {"exactly full", 10, 10, true}, {"over capacity", 10, 11, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { channel := NewChannel("test") channel.MaxUsers = tt.maxUsers channel.Members = make([]string, tt.members) got := channel.IsFull() if got != tt.want { t.Errorf("IsFull() = %v, want %v", got, tt.want) } }) } } ``` ### Test Requirements - Write tests for all exported functions - Use table-driven tests for multiple cases - Use sub-tests (t.Run) for related test cases - Mock external dependencies - Test both success and error cases - Write benchmarks for performance-critical code ### Coverage Goals - Unit tests: Aim for 80%+ coverage - Integration tests: Critical paths - Packages with <50% coverage: Flag in review ## Git Workflow ### Branch Naming ``` feature/ # New feature bugfix/ # Bug fix docs/ # Documentation refactor/ # Refactoring perf/ # Performance improvement test/ # Test additions ``` ### Commit Messages Follow [Conventional Commits](https://www.conventionalcommits.org/): ``` ():