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>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user