🧪 test: add JWT secret rotation BDD scenarios and step implementations #12
159
BDD_TAGS.md
159
BDD_TAGS.md
@@ -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
|
|
||||||
112
pkg/bdd/parallel/port_manager.go
Normal file
112
pkg/bdd/parallel/port_manager.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
198
pkg/bdd/parallel/resource_monitor.go
Normal file
198
pkg/bdd/parallel/resource_monitor.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
98
scripts/test-all-features-parallel.sh
Executable file
98
scripts/test-all-features-parallel.sh
Executable file
@@ -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"
|
||||||
Reference in New Issue
Block a user