750b7abcd5
- .claude/CLAUDE.md - .claude/agents/fn-recopilador/SKILL.md - .claude/rules/INDEX.md - .claude/rules/cpp_apps.md - bash/functions/infra/build_cpp_windows.sh - cpp/CMakeLists.txt - cpp/PATTERNS.md - cpp/framework/app_base.cpp - cpp/framework/app_base.h - dev/issues/README.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
4.4 KiB
Go
172 lines
4.4 KiB
Go
package infra
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultTimeoutS = 60
|
|
defaultSeverity = "critical"
|
|
maxStdoutBytes = 4096
|
|
healthIntervalMs = 500
|
|
)
|
|
|
|
// E2ERunChecks executes a list of E2ECheck declarations in order and returns
|
|
// one CheckResult per check. The slice is always len(checks) long; individual
|
|
// check failures do not abort the run.
|
|
//
|
|
// For each check:
|
|
// - If Cmd is non-empty, it is executed via "bash -c". Commands ending with
|
|
// "&" are launched in background; the function does not wait for exit and
|
|
// proceeds to Health (if any) or records an immediate pass.
|
|
// - If Health is non-empty (after any Cmd), HealthCheckHTTP polls the URL
|
|
// until status 200 or timeout.
|
|
// - ExpectExit (default 0) and ExpectStdoutContains are evaluated after Cmd.
|
|
// - Ref is not yet implemented: the check is recorded as skip with a
|
|
// descriptive error.
|
|
// - Checks with no Cmd, Health, or Ref are skipped.
|
|
//
|
|
// workDir sets the working directory for subprocesses. Pass "" to inherit the
|
|
// current process directory.
|
|
//
|
|
// Returns an error only for setup failures (e.g. bad workDir), not for
|
|
// individual check failures.
|
|
func E2ERunChecks(checks []E2ECheck, workDir string) ([]CheckResult, error) {
|
|
results := make([]CheckResult, 0, len(checks))
|
|
|
|
for _, ch := range checks {
|
|
result := runSingleCheck(ch, workDir)
|
|
results = append(results, result)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func runSingleCheck(ch E2ECheck, workDir string) CheckResult {
|
|
sev := ch.Severity
|
|
if sev == "" {
|
|
sev = defaultSeverity
|
|
}
|
|
timeoutS := ch.TimeoutS
|
|
if timeoutS <= 0 {
|
|
timeoutS = defaultTimeoutS
|
|
}
|
|
|
|
base := CheckResult{
|
|
ID: ch.ID,
|
|
Severity: sev,
|
|
}
|
|
|
|
// Skip: nothing to do.
|
|
if ch.Cmd == "" && ch.Ref == "" && ch.Health == "" {
|
|
base.Status = "skip"
|
|
return base
|
|
}
|
|
|
|
// Ref: not implemented yet.
|
|
if ch.Ref != "" && ch.Cmd == "" && ch.Health == "" {
|
|
base.Status = "skip"
|
|
base.Error = "ref handler not implemented"
|
|
return base
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
// Run Cmd if present.
|
|
if ch.Cmd != "" {
|
|
background := strings.HasSuffix(strings.TrimSpace(ch.Cmd), "&")
|
|
|
|
if background {
|
|
// Launch background process, do not wait.
|
|
bgCmd := exec.Command("bash", "-c", ch.Cmd)
|
|
if workDir != "" {
|
|
bgCmd.Dir = workDir
|
|
}
|
|
if err := bgCmd.Start(); err != nil {
|
|
base.DurationMs = time.Since(start).Milliseconds()
|
|
base.Status = "fail"
|
|
base.Error = fmt.Sprintf("background start failed: %v", err)
|
|
return base
|
|
}
|
|
// Do not wait; proceed to Health or pass.
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutS)*time.Second)
|
|
defer cancel()
|
|
|
|
fgCmd := exec.CommandContext(ctx, "bash", "-c", ch.Cmd)
|
|
if workDir != "" {
|
|
fgCmd.Dir = workDir
|
|
}
|
|
var stdout, stderr bytes.Buffer
|
|
fgCmd.Stdout = &stdout
|
|
fgCmd.Stderr = &stderr
|
|
|
|
runErr := fgCmd.Run()
|
|
base.DurationMs = time.Since(start).Milliseconds()
|
|
|
|
stdoutStr := truncate(stdout.String(), maxStdoutBytes)
|
|
stderrStr := truncate(stderr.String(), maxStdoutBytes)
|
|
base.Stdout = stdoutStr
|
|
base.Stderr = stderrStr
|
|
|
|
// Exit code.
|
|
exitCode := 0
|
|
if fgCmd.ProcessState != nil {
|
|
exitCode = fgCmd.ProcessState.ExitCode()
|
|
}
|
|
base.ExitCode = exitCode
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
base.Status = "fail"
|
|
base.Error = fmt.Sprintf("command timed out after %ds", timeoutS)
|
|
return base
|
|
}
|
|
|
|
expectedExit := 0
|
|
if ch.ExpectExit != nil {
|
|
expectedExit = *ch.ExpectExit
|
|
}
|
|
if exitCode != expectedExit {
|
|
base.Status = "fail"
|
|
if runErr != nil {
|
|
base.Error = runErr.Error()
|
|
} else {
|
|
base.Error = fmt.Sprintf("exit code %d, expected %d", exitCode, expectedExit)
|
|
}
|
|
return base
|
|
}
|
|
|
|
if ch.ExpectStdoutContains != "" && !strings.Contains(stdoutStr, ch.ExpectStdoutContains) {
|
|
base.Status = "fail"
|
|
base.Error = fmt.Sprintf("stdout does not contain %q", ch.ExpectStdoutContains)
|
|
return base
|
|
}
|
|
}
|
|
}
|
|
|
|
// Health check (after Cmd or standalone).
|
|
if ch.Health != "" {
|
|
if err := HealthCheckHTTP(ch.Health, timeoutS, healthIntervalMs); err != nil {
|
|
base.DurationMs = time.Since(start).Milliseconds()
|
|
base.Status = "fail"
|
|
base.Error = fmt.Sprintf("health check failed: %v", err)
|
|
return base
|
|
}
|
|
}
|
|
|
|
base.DurationMs = time.Since(start).Milliseconds()
|
|
base.Status = "pass"
|
|
return base
|
|
}
|
|
|
|
func truncate(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
return s[:max]
|
|
}
|