349 lines
10 KiB
Go
349 lines
10 KiB
Go
package tests
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
tests "github.com/jfraeys/fetch_ml/tests/fixtures"
|
|
)
|
|
|
|
const (
|
|
manageScriptPath = "../../tools/manage.sh"
|
|
)
|
|
|
|
// TestHomelabSetupE2E tests the complete homelab setup workflow end-to-end
|
|
func TestHomelabSetupE2E(t *testing.T) {
|
|
// Skip if essential tools not available
|
|
manageScript := manageScriptPath
|
|
if _, err := os.Stat(manageScript); os.IsNotExist(err) {
|
|
t.Skip("manage.sh not found")
|
|
}
|
|
|
|
cliPath := e2eCLIPath(t)
|
|
if _, err := os.Stat(cliPath); os.IsNotExist(err) {
|
|
t.Skip("CLI not built - run 'make build' first")
|
|
}
|
|
|
|
// Use fixtures for manage script operations
|
|
ms := tests.NewManageScript(manageScript)
|
|
defer ms.StopAndCleanup()
|
|
|
|
testDir := t.TempDir()
|
|
|
|
// Phase 1: Fresh Setup Simulation
|
|
t.Run("FreshSetup", func(t *testing.T) {
|
|
// Stop any existing services
|
|
_ = ms.Stop()
|
|
|
|
// Test initial status
|
|
output, err := ms.Status()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get status: %v", err)
|
|
}
|
|
t.Logf("Initial status: %s", output)
|
|
|
|
// Start services
|
|
if err := ms.Start(); err != nil {
|
|
t.Skipf("Failed to start services: %v", err)
|
|
}
|
|
|
|
// Wait for health instead of fixed sleep
|
|
for range 10 {
|
|
healthOutput, err := ms.Health()
|
|
if err == nil &&
|
|
(strings.Contains(healthOutput, "API is healthy") ||
|
|
strings.Contains(healthOutput, "Port 9101 is open")) {
|
|
break
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
}
|
|
|
|
// Verify with health check
|
|
healthOutput, err := ms.Health()
|
|
if err != nil {
|
|
t.Logf("Health check failed (services may not be fully started)")
|
|
} else {
|
|
if !strings.Contains(healthOutput, "API is healthy") && !strings.Contains(healthOutput, "Port 9101 is open") {
|
|
t.Errorf("Unexpected health check output: %s", healthOutput)
|
|
}
|
|
t.Log("Health check passed")
|
|
}
|
|
})
|
|
|
|
// Phase 2: Service Management Workflow
|
|
t.Run("ServiceManagement", func(t *testing.T) {
|
|
// Check initial status
|
|
output, err := ms.Status()
|
|
if err != nil {
|
|
t.Errorf("Status check failed: %v", err)
|
|
}
|
|
t.Logf("Initial status: %s", output)
|
|
|
|
// Start services
|
|
if err := ms.Start(); err != nil {
|
|
t.Skipf("Failed to start services: %v", err)
|
|
}
|
|
|
|
// Wait for health instead of fixed sleep
|
|
for range 15 {
|
|
healthOutput, err := ms.Health()
|
|
if err == nil &&
|
|
(strings.Contains(healthOutput, "API is healthy") ||
|
|
strings.Contains(healthOutput, "Port 9101 is open")) {
|
|
break
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
}
|
|
|
|
// Verify with health check
|
|
healthOutput, err := ms.Health()
|
|
t.Logf("Health check output: %s", healthOutput)
|
|
if err != nil {
|
|
t.Logf("Health check failed (expected if services not fully started): %v", err)
|
|
}
|
|
|
|
// Check final status
|
|
statusOutput, err := ms.Status()
|
|
if err != nil {
|
|
t.Errorf("Final status check failed: %v", err)
|
|
}
|
|
t.Logf("Final status: %s", statusOutput)
|
|
})
|
|
|
|
// Phase 3: CLI Configuration Workflow
|
|
t.Run("CLIConfiguration", func(t *testing.T) {
|
|
// Create CLI config directory
|
|
cliConfigDir := filepath.Join(testDir, "cli_config")
|
|
if err := os.MkdirAll(cliConfigDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create CLI config dir: %v", err)
|
|
}
|
|
|
|
// Create minimal config
|
|
configPath := filepath.Join(cliConfigDir, "config.yaml")
|
|
configContent := `
|
|
redis_addr: localhost:6379
|
|
redis_db: 13
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("Failed to create CLI config: %v", err)
|
|
}
|
|
|
|
// Test CLI init
|
|
initCmd := exec.CommandContext(context.Background(), cliPath, "init")
|
|
initCmd.Dir = cliConfigDir
|
|
initOutput, err := initCmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Logf("CLI init failed (may be expected): %v", err)
|
|
}
|
|
t.Logf("CLI init output: %s", string(initOutput))
|
|
|
|
// Test CLI status
|
|
statusCmd := exec.CommandContext(context.Background(), cliPath, "status")
|
|
statusCmd.Dir = cliConfigDir
|
|
statusOutput, err := statusCmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Logf("CLI status failed (may be expected): %v", err)
|
|
}
|
|
t.Logf("CLI status output: %s", string(statusOutput))
|
|
})
|
|
}
|
|
|
|
// TestDockerDeploymentE2E tests Docker deployment workflow
|
|
func TestDockerDeploymentE2E(t *testing.T) {
|
|
t.Parallel() // Enable parallel execution
|
|
|
|
if os.Getenv("FETCH_ML_E2E_DOCKER") != "1" {
|
|
t.Skip("Skipping DockerDeploymentE2E (set FETCH_ML_E2E_DOCKER=1 to enable)")
|
|
}
|
|
|
|
// Skip if Docker not available
|
|
dockerCompose := "../../docker-compose.yml"
|
|
if _, err := os.Stat(dockerCompose); os.IsNotExist(err) {
|
|
t.Skip("docker-compose.yml not found")
|
|
}
|
|
|
|
t.Run("DockerDeployment", func(t *testing.T) {
|
|
// Stop any existing containers
|
|
downCmd := exec.CommandContext(context.Background(),
|
|
"docker-compose", "-f", dockerCompose, "down", "--remove-orphans")
|
|
if err := downCmd.Run(); err != nil {
|
|
t.Logf("Warning: Failed to stop existing containers: %v", err)
|
|
}
|
|
|
|
// Start Docker containers
|
|
upCmd := exec.CommandContext(context.Background(), "docker-compose", "-f", dockerCompose, "up", "-d")
|
|
if err := upCmd.Run(); err != nil {
|
|
t.Fatalf("Failed to start Docker containers: %v", err)
|
|
}
|
|
|
|
// Wait for containers to be healthy using health checks instead of fixed sleep
|
|
maxWait := 15 * time.Second // Reduced from 30 seconds
|
|
start := time.Now()
|
|
apiHealthy := false
|
|
redisHealthy := false
|
|
|
|
for time.Since(start) < maxWait && (!apiHealthy || !redisHealthy) {
|
|
// Check if API container is healthy
|
|
if !apiHealthy {
|
|
healthCmd := exec.CommandContext(context.Background(),
|
|
"docker", "ps", "--filter", "name=ml-experiments-api", "--format", "{{.Status}}")
|
|
healthOutput, err := healthCmd.CombinedOutput()
|
|
if err == nil && strings.Contains(string(healthOutput), "healthy") {
|
|
t.Logf("API container became healthy in %v", time.Since(start))
|
|
apiHealthy = true
|
|
} else if err == nil && strings.Contains(string(healthOutput), "Up") {
|
|
// Accept "Up" status as good enough for testing
|
|
t.Logf("API container is up in %v (not necessarily healthy)", time.Since(start))
|
|
apiHealthy = true
|
|
}
|
|
}
|
|
|
|
// Check if Redis is healthy
|
|
if !redisHealthy {
|
|
redisCmd := exec.CommandContext(context.Background(),
|
|
"docker", "ps", "--filter", "name=ml-experiments-redis", "--format", "{{.Status}}")
|
|
redisOutput, err := redisCmd.CombinedOutput()
|
|
if err == nil && strings.Contains(string(redisOutput), "healthy") {
|
|
t.Logf("Redis container became healthy in %v", time.Since(start))
|
|
redisHealthy = true
|
|
}
|
|
}
|
|
|
|
// Break if both are healthy/up
|
|
if apiHealthy && redisHealthy {
|
|
t.Logf("All containers ready in %v", time.Since(start))
|
|
break
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond) // Check more frequently
|
|
}
|
|
|
|
// Check container status
|
|
psCmd := exec.CommandContext(context.Background(),
|
|
"docker-compose", "-f", dockerCompose, "ps", "--format", "table {{.Name}}\t{{.Status}}")
|
|
psOutput, err := psCmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Errorf("Docker ps failed: %v", err)
|
|
}
|
|
t.Logf("Docker containers status: %s", string(psOutput))
|
|
|
|
// Test API endpoint in Docker (quick check)
|
|
testDockerAPI(t)
|
|
|
|
// Cleanup Docker synchronously to ensure proper cleanup
|
|
t.Cleanup(func() {
|
|
downCmd := exec.CommandContext(context.Background(),
|
|
"docker-compose", "-f", dockerCompose, "down", "--remove-orphans", "--volumes")
|
|
if err := downCmd.Run(); err != nil {
|
|
t.Logf("Warning: Failed to stop Docker containers: %v", err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// testDockerAPI tests the Docker API endpoint
|
|
func testDockerAPI(t *testing.T) {
|
|
// This would test the API endpoint - simplified for now
|
|
t.Log("Testing Docker API functionality...")
|
|
// In a real test, you would make HTTP requests to the API
|
|
}
|
|
|
|
// TestPerformanceE2E tests performance characteristics end-to-end
|
|
func TestPerformanceE2E(t *testing.T) {
|
|
t.Parallel() // Enable parallel execution
|
|
|
|
if os.Getenv("FETCH_ML_E2E_PERF") != "1" {
|
|
t.Skip("Skipping PerformanceE2E (set FETCH_ML_E2E_PERF=1 to enable)")
|
|
}
|
|
|
|
manageScript := manageScriptPath
|
|
if _, err := os.Stat(manageScript); os.IsNotExist(err) {
|
|
t.Skip("manage.sh not found")
|
|
}
|
|
|
|
// Use fixtures for manage script operations
|
|
ms := tests.NewManageScript(manageScript)
|
|
|
|
t.Run("PerformanceMetrics", func(t *testing.T) {
|
|
// Test health check performance
|
|
start := time.Now()
|
|
_, err := ms.Health()
|
|
duration := time.Since(start)
|
|
|
|
t.Logf("Health check took %v", duration)
|
|
|
|
if duration > 10*time.Second {
|
|
t.Errorf("Health check took too long: %v", duration)
|
|
}
|
|
|
|
if err != nil {
|
|
t.Logf("Health check failed (expected if services not running)")
|
|
} else {
|
|
t.Log("Health check passed")
|
|
}
|
|
|
|
// Test status check performance
|
|
start = time.Now()
|
|
output, err := ms.Status()
|
|
duration = time.Since(start)
|
|
|
|
t.Logf("Status check took %v", duration)
|
|
t.Logf("Status output length: %d characters", len(output))
|
|
|
|
if duration > 5*time.Second {
|
|
t.Errorf("Status check took too long: %v", duration)
|
|
}
|
|
|
|
_ = err // Suppress unused variable warning
|
|
})
|
|
}
|
|
|
|
// TestConfigurationScenariosE2E tests various configuration scenarios end-to-end
|
|
func TestConfigurationScenariosE2E(t *testing.T) {
|
|
t.Parallel() // Enable parallel execution
|
|
|
|
manageScript := "../../tools/manage.sh"
|
|
if _, err := os.Stat(manageScript); os.IsNotExist(err) {
|
|
t.Skip("manage.sh not found")
|
|
}
|
|
|
|
// Use fixtures for manage script operations
|
|
ms := tests.NewManageScript(manageScript)
|
|
|
|
t.Run("ConfigurationHandling", func(t *testing.T) {
|
|
testDir := t.TempDir()
|
|
// Test status with different configuration states
|
|
originalConfigDir := "../../configs"
|
|
tempConfigDir := filepath.Join(testDir, "configs_backup")
|
|
|
|
// Backup original configs if they exist
|
|
if _, err := os.Stat(originalConfigDir); err == nil {
|
|
if err := os.Rename(originalConfigDir, tempConfigDir); err != nil {
|
|
t.Fatalf("Failed to backup configs: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = os.Rename(tempConfigDir, originalConfigDir)
|
|
}()
|
|
}
|
|
|
|
// Test status without configs
|
|
output, err := ms.Status()
|
|
if err != nil {
|
|
t.Errorf("Status check failed: %v", err)
|
|
}
|
|
t.Logf("Status without configs: %s", output)
|
|
|
|
// Test health without configs
|
|
_, err = ms.Health()
|
|
if err != nil {
|
|
t.Logf("Health check failed without configs (expected)")
|
|
} else {
|
|
t.Log("Health check passed without configs")
|
|
}
|
|
})
|
|
}
|