diff --git a/BDD_TAGS.md b/BDD_TAGS.md deleted file mode 100644 index 53438f0..0000000 --- a/BDD_TAGS.md +++ /dev/null @@ -1,159 +0,0 @@ -# BDD Test Tags Documentation - -This document describes the tagging system used in the dance-lessons-coach BDD tests for selective test execution. - -## Tag Categories - -### Feature Tags -Used to categorize tests by feature area: -- `@auth` - Authentication and user management tests -- `@config` - Configuration and hot reloading tests -- `@greet` - Greeting service tests -- `@health` - Health check and monitoring tests -- `@jwt` - JWT secret rotation and retention tests - -### Priority Tags -Used to categorize tests by importance: -- `@smoke` - Basic smoke tests that verify core functionality -- `@critical` - Critical path tests that must always pass -- `@basic` - Basic functionality tests -- `@advanced` - Advanced or edge case scenarios - -### Component Tags -Used to categorize tests by system component: -- `@api` - API endpoint tests -- `@v2` - Version 2 API tests -- `@database` - Database interaction tests -- `@security` - Security-related tests - -## Usage Examples - -### Running Smoke Tests -```bash -# Run all smoke tests -godog --tags=@smoke features/ - -# Run smoke tests for specific feature -godog --tags=@smoke features/auth/ -``` - -### Running Critical Tests -```bash -# Run all critical tests -godog --tags=@critical features/ - -# Run critical health tests -godog --tags=@critical,@health features/ -``` - -### Running Feature-Specific Tests -```bash -# Run all auth tests -godog --tags=@auth features/ - -# Run v2 API tests -godog --tags=@v2 features/ -``` - -### Combining Tags -```bash -# Run smoke tests for auth and health features -godog --tags=@smoke,@auth,@health features/ - -# Run critical API tests -godog --tags=@critical,@api features/ -``` - -## Tagging Conventions - -1. **Feature tags** should be applied at the feature level -2. **Priority tags** should be applied at the scenario level -3. **Component tags** should be applied at the scenario level -4. **Multiple tags** can be applied to a single scenario - -### Example Feature File -```gherkin -@health @smoke -Feature: Health Endpoint - The health endpoint should indicate server status - - @basic @critical - Scenario: Health check returns healthy status - Given the server is running - When I request the health endpoint - Then the response should be "{\"status\":\"healthy\"}" - - @advanced @api - Scenario: Health check with authentication - Given the server is running with auth enabled - When I request the health endpoint with valid token - Then the response should be "{\"status\":\"healthy\"}" -``` - -## Test Execution Scripts - -### Feature-Specific Testing -```bash -# Test specific feature -./scripts/test-feature.sh greet - -# Test with specific tags -./scripts/test-by-tag.sh @smoke greet -``` - -### Tag-Based Testing -```bash -# Run smoke tests for all features -./scripts/test-by-tag.sh @smoke - -# Run critical auth tests -./scripts/test-by-tag.sh @critical auth -``` - -## CI/CD Integration - -### Smoke Test Pipeline -```yaml -- name: Run Smoke Tests - run: godog --tags=@smoke features/ -``` - -### Critical Path Testing -```yaml -- name: Run Critical Tests - run: godog --tags=@critical features/ -``` - -### Feature-Specific Testing -```yaml -- name: Test Auth Feature - run: ./scripts/test-feature.sh auth -``` - -## Best Practices - -1. **Tag consistently** - Apply tags consistently across similar scenarios -2. **Prioritize tests** - Use priority tags to identify critical tests -3. **Document tags** - Keep this documentation updated with new tags -4. **Review tags** - Regularly review tag usage to ensure relevance -5. **CI/CD optimization** - Use tags to optimize CI/CD pipeline execution times - -## Tag Reference - -| Tag | Purpose | Example Usage | -|-----|---------|--------------| -| `@smoke` | Smoke tests | `@smoke` on critical features | -| `@critical` | Critical path | `@critical` on essential scenarios | -| `@basic` | Basic functionality | `@basic` on standard scenarios | -| `@advanced` | Advanced scenarios | `@advanced` on edge cases | -| `@auth` | Authentication | `@auth` on auth features | -| `@config` | Configuration | `@config` on config scenarios | -| `@api` | API endpoints | `@api` on endpoint tests | -| `@v2` | V2 API | `@v2` on version 2 tests | - -## Future Enhancements - -- **Performance tags** - `@fast`, `@slow` for performance categorization -- **Environment tags** - `@ci`, `@local` for environment-specific tests -- **Risk tags** - `@high-risk`, `@low-risk` for risk-based testing -- **Automated tag validation** - Script to validate tag usage consistency diff --git a/pkg/bdd/parallel/port_manager.go b/pkg/bdd/parallel/port_manager.go new file mode 100644 index 0000000..ccdb195 --- /dev/null +++ b/pkg/bdd/parallel/port_manager.go @@ -0,0 +1,112 @@ +package parallel + +import ( + "errors" + "fmt" + "sync" +) + +// PortManager manages port allocation for parallel test execution +type PortManager struct { + portsInUse map[int]bool + basePort int + maxPort int + mutex sync.Mutex +} + +// NewPortManager creates a new port manager with the specified port range +func NewPortManager(basePort, maxPort int) *PortManager { + return &PortManager{ + portsInUse: make(map[int]bool), + basePort: basePort, + maxPort: maxPort, + } +} + +// AcquirePort acquires an available port for a feature +func (pm *PortManager) AcquirePort(featureName string) (int, error) { + pm.mutex.Lock() + defer pm.mutex.Unlock() + + // Check if this feature already has a port assigned + // In a real implementation, this would be more sophisticated + + // Try to find an available port + for port := pm.basePort; port <= pm.maxPort; port++ { + if !pm.portsInUse[port] { + pm.portsInUse[port] = true + return port, nil + } + } + + return 0, errors.New("no available ports in the specified range") +} + +// ReleasePort releases a port back to the pool +func (pm *PortManager) ReleasePort(port int) { + pm.mutex.Lock() + defer pm.mutex.Unlock() + + if pm.portsInUse[port] { + delete(pm.portsInUse, port) + } +} + +// CheckPortConflict checks if a port is already in use +func (pm *PortManager) CheckPortConflict(port int) bool { + pm.mutex.Lock() + defer pm.mutex.Unlock() + + return pm.portsInUse[port] +} + +// GetAvailablePorts returns a list of available ports +func (pm *PortManager) GetAvailablePorts() []int { + pm.mutex.Lock() + defer pm.mutex.Unlock() + + var available []int + for port := pm.basePort; port <= pm.maxPort; port++ { + if !pm.portsInUse[port] { + available = append(available, port) + } + } + return available +} + +// GetPortForFeature gets the standard port for a feature (without dynamic allocation) +func GetPortForFeature(featureName string) int { + // Standard port mapping for features + switch featureName { + case "auth": + return 9192 + case "config": + return 9193 + case "greet": + return 9194 + case "health": + return 9195 + case "jwt": + return 9196 + default: + return 9191 // Default port + } +} + +// ValidatePortRange validates that a port is within acceptable range +func ValidatePortRange(port int) error { + if port < 1024 || port > 65535 { + return fmt.Errorf("port %d is outside valid range (1024-65535)", port) + } + return nil +} + +// CheckPortAvailable checks if a specific port is available on the system +func CheckPortAvailable(port int) (bool, error) { + // In a real implementation, this would actually check if the port is available + // For now, we'll just validate the range + if err := ValidatePortRange(port); err != nil { + return false, err + } + return true, nil +} diff --git a/pkg/bdd/parallel/resource_monitor.go b/pkg/bdd/parallel/resource_monitor.go new file mode 100644 index 0000000..fafd9d3 --- /dev/null +++ b/pkg/bdd/parallel/resource_monitor.go @@ -0,0 +1,198 @@ +package parallel + +import ( + "fmt" + "runtime" + "sync" + "time" + + "github.com/rs/zerolog/log" +) + +// ResourceMonitor monitors system resources during parallel test execution +type ResourceMonitor struct { + startTime time.Time + maxMemoryMB float64 + maxGoroutines int + checkInterval time.Duration + stopChan chan bool + wg sync.WaitGroup + mutex sync.Mutex +} + +// NewResourceMonitor creates a new resource monitor +type ResourceStats struct { + MemoryMB float64 + Goroutines int + CPUUsage float64 + TestDuration time.Duration +} + +func NewResourceMonitor(interval time.Duration) *ResourceMonitor { + return &ResourceMonitor{ + checkInterval: interval, + stopChan: make(chan bool), + } +} + +// StartMonitoring starts monitoring system resources +func (rm *ResourceMonitor) StartMonitoring() { + rm.startTime = time.Now() + rm.wg.Add(1) + + go func() { + defer rm.wg.Done() + + ticker := time.NewTicker(rm.checkInterval) + defer ticker.Stop() + + for { + select { + case <-rm.stopChan: + return + case <-ticker.C: + rm.checkResources() + } + } + }() +} + +// StopMonitoring stops the resource monitor +func (rm *ResourceMonitor) StopMonitoring() { + close(rm.stopChan) + rm.wg.Wait() +} + +// checkResources checks current system resource usage +func (rm *ResourceMonitor) checkResources() { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + + currentMemoryMB := float64(memStats.Alloc) / 1024 / 1024 + currentGoroutines := runtime.NumGoroutine() + + rm.mutex.Lock() + if currentMemoryMB > rm.maxMemoryMB { + rm.maxMemoryMB = currentMemoryMB + } + if currentGoroutines > rm.maxGoroutines { + rm.maxGoroutines = currentGoroutines + } + rm.mutex.Unlock() + + log.Debug(). + Float64("memory_mb", currentMemoryMB). + Int("goroutines", currentGoroutines). + Msg("Resource usage update") +} + +// GetResourceStats gets the collected resource statistics +func (rm *ResourceMonitor) GetResourceStats() ResourceStats { + rm.mutex.Lock() + defer rm.mutex.Unlock() + + return ResourceStats{ + MemoryMB: rm.maxMemoryMB, + Goroutines: rm.maxGoroutines, + TestDuration: time.Since(rm.startTime), + } +} + +// LogResourceSummary logs a summary of resource usage +func (rm *ResourceMonitor) LogResourceSummary() { + stats := rm.GetResourceStats() + + log.Info(). + Float64("max_memory_mb", stats.MemoryMB). + Int("max_goroutines", stats.Goroutines). + Str("duration", stats.TestDuration.String()). + Msg("Parallel Test Resource Usage Summary") +} + +// CheckResourceLimits checks if resource usage exceeds specified limits +func (rm *ResourceMonitor) CheckResourceLimits(maxMemoryMB float64, maxGoroutines int) (bool, string) { + stats := rm.GetResourceStats() + + if stats.MemoryMB > maxMemoryMB { + return false, fmt.Sprintf("Memory limit exceeded: %.1fMB > %.1fMB", stats.MemoryMB, maxMemoryMB) + } + + if stats.Goroutines > maxGoroutines { + return false, fmt.Sprintf("Goroutine limit exceeded: %d > %d", stats.Goroutines, maxGoroutines) + } + + return true, "Within resource limits" +} + +// MonitorTestExecution monitors a single test execution with timeout +func MonitorTestExecution(testName string, timeout time.Duration, testFunc func() error) error { + done := make(chan error, 1) + + // Start the test in a goroutine + go func() { + done <- testFunc() + }() + + // Wait for test completion or timeout + select { + case err := <-done: + return err + case <-time.After(timeout): + return fmt.Errorf("test '%s' exceeded timeout of %v", testName, timeout) + } +} + +// ParallelTestRunner runs multiple tests in parallel with resource monitoring +type ParallelTestRunner struct { + maxParallel int + semaphore chan struct{} + monitor *ResourceMonitor +} + +// NewParallelTestRunner creates a new parallel test runner +func NewParallelTestRunner(maxParallel int) *ParallelTestRunner { + return &ParallelTestRunner{ + maxParallel: maxParallel, + semaphore: make(chan struct{}, maxParallel), + monitor: NewResourceMonitor(1 * time.Second), + } +} + +// RunTestsInParallel runs tests in parallel +func (ptr *ParallelTestRunner) RunTestsInParallel(tests []func() error) ([]error, error) { + var errors []error + var mutex sync.Mutex + + ptr.monitor.StartMonitoring() + defer ptr.monitor.StopMonitoring() + + var wg sync.WaitGroup + + for _, test := range tests { + wg.Add(1) + + // Acquire semaphore slot + ptr.semaphore <- struct{}{} + + go func(t func() error) { + defer wg.Done() + defer func() { <-ptr.semaphore }() + + if err := t(); err != nil { + mutex.Lock() + errors = append(errors, err) + mutex.Unlock() + } + }(test) + } + + wg.Wait() + + ptr.monitor.LogResourceSummary() + + if len(errors) > 0 { + return errors, fmt.Errorf("%d tests failed", len(errors)) + } + + return nil, nil +} diff --git a/scripts/test-all-features-parallel.sh b/scripts/test-all-features-parallel.sh new file mode 100755 index 0000000..d3a5d1b --- /dev/null +++ b/scripts/test-all-features-parallel.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Parallel Feature Test Runner Script +# Runs multiple feature tests in parallel with proper isolation + +set -e + +SCRIPTS_DIR=$(dirname `realpath ${BASH_SOURCE[0]}`) +cd $SCRIPTS_DIR/.. + +echo "๐Ÿš€ Parallel Feature Test Runner" +echo "================================" +echo + +# Define features and their ports +declare -a features=( + "auth:9192" + "config:9193" + "greet:9194" + "health:9195" + "jwt:9196" +) + +# Function to run a single feature test +run_feature_test() { + local feature_port="$1" + local feature_name="$2" + local port="$3" + + echo "๐Ÿงช Starting ${feature_name} feature tests on port ${port}..." + + # Set feature-specific environment variables + export DLC_DATABASE_HOST="localhost" + export DLC_DATABASE_PORT="5432" + export DLC_DATABASE_USER="postgres" + export DLC_DATABASE_PASSWORD="postgres" + export DLC_DATABASE_NAME="dance_lessons_coach_${feature_name}_test" + export DLC_DATABASE_SSL_MODE="disable" + + # Create feature-specific database using docker + if ! docker exec dance-lessons-coach-postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw "${DLC_DATABASE_NAME}"; then + echo "๐Ÿ“ฆ Creating ${feature_name} test database..." + docker exec dance-lessons-coach-postgres createdb -U postgres "${DLC_DATABASE_NAME}" + fi + + # Run the feature tests + cd "features/${feature_name}" + FEATURE=${feature_name} DLC_DATABASE_NAME="${DLC_DATABASE_NAME}" go test -v . 2>&1 | grep -E "(PASS|FAIL|RUN)" || true + + # Cleanup + cd ../.. + docker exec dance-lessons-coach-postgres dropdb -U postgres "${DLC_DATABASE_NAME}" 2>/dev/null || true + + echo "โœ… ${feature_name} feature tests completed" +} + +# Check if PostgreSQL is running +if ! docker ps --format '{{.Names}}' | grep -q "^dance-lessons-coach-postgres$"; then + echo "โŒ PostgreSQL container is not running. Please start PostgreSQL first." + echo "๐Ÿ’ก Try: docker compose up -d postgres" + exit 1 +fi + +# Check if PostgreSQL is ready +max_attempts=10 +attempt=0 +while [ $attempt -lt $max_attempts ]; do + if docker exec dance-lessons-coach-postgres pg_isready -U postgres 2>/dev/null; then + break + fi + attempt=$((attempt + 1)) + sleep 1 +done + +if [ $attempt -eq $max_attempts ]; then + echo "โŒ PostgreSQL is not ready. Please check the container logs." + exit 1 +fi + +echo "โœ… PostgreSQL is ready for parallel testing" +echo + +# Run feature tests in parallel +for feature_port in "${features[@]}"; do + # Split feature:port into separate variables + IFS=':' read -r feature_name port <<< "${feature_port}" + + # Run test in background + run_feature_test "${feature_port}" "${feature_name}" "${port}" & + +done + +# Wait for all background processes to complete +wait + +echo +echo "๐ŸŽ‰ All parallel feature tests completed!" +echo "๐Ÿ“Š Check individual feature test outputs above for results"