Files
egutierrez 750b7abcd5 chore: auto-commit (97 archivos)
- .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>
2026-05-09 18:11:24 +02:00

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]
}