#!/usr/bin/env bash # Barrido de compatibilidad componente <-> core real (change verify-core-compat). # Monta un proyecto temporal por paquete contra el core real publicado, lo # ejercita segun su nivel de evidencia (tests/compat/levels.json) con criterios # estrictos (cualquier Deprecated/Warning/Notice/Fatal cuenta FAIL) y escribe # tests/compat/report.json. # # Uso: scripts/compat-sweep [--only ...] # Env: DUCKBRAIN_CORE_URL repositorio del core (default: Config::coreUrl()) # DUCKBRAIN_CACHE_DIR caché donde CoreProvider mantiene el clon del core set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPAT="$ROOT/tests/compat" LEVELS="$COMPAT/levels.json" REPORT="$COMPAT/report.json" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT ONLY=() args=("$@") i=0 while [ "$i" -lt "${#args[@]}" ]; do case "${args[i]}" in --only) i=$((i + 1)); ONLY+=("${args[i]}") ;; *) echo "Uso: scripts/compat-sweep [--only ...]" >&2; exit 2 ;; esac i=$((i + 1)) done # --- resolver el core real (clon + checkout del mayor tag semver) -------------- CORE_INFO="$(php -d error_reporting=-1 -r ' require "'"$ROOT"'/src/autoload.php"; try { $p = Toolbox\CoreProvider::fromUrl( Toolbox\Config::coreUrl(), Toolbox\Config::cacheDir(), new Toolbox\Console() ); $v = $p->version(); if ($v === null) { fwrite(STDERR, "El core no tiene tags semver publicados; nada que verificar.\n"); exit(1); } echo $p->dir(), "\n", $v, "\n", (string) $p->commit(), "\n"; } catch (Throwable $e) { fwrite(STDERR, "No se pudo acceder al core real: " . $e->getMessage() . "\n"); exit(1); }' 2>"$WORK/core.err")" || { echo "== ERROR: barrido abortado ==" >&2 cat "$WORK/core.err" >&2 exit 1 } CORE_DIR="$(printf '%s\n' "$CORE_INFO" | sed -n 1p)" CORE_VER="$(printf '%s\n' "$CORE_INFO" | sed -n 2p)" CORE_SHA="$(printf '%s\n' "$CORE_INFO" | sed -n 3p)" echo "== core real: v$CORE_VER ($CORE_SHA) en $CORE_DIR ==" echo LEVELS_PATH="$LEVELS" php -r ' $levels = json_decode((string) file_get_contents(getenv("LEVELS_PATH")), true); echo implode("\n", array_keys($levels)), "\n";' > "$WORK/pkgs" RESULTS="$WORK/results" : > "$RESULTS" FAILED=0 wanted() { [ "${#ONLY[@]}" -eq 0 ] && return 0 local w for w in "${ONLY[@]}"; do [ "$w" = "$1" ] && return 0; done return 1 } stop_server() { if [ -n "${SERVER_PID:-}" ]; then kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true SERVER_PID="" fi } # --- por paquete --------------------------------------------------------------- while read -r PKG; do wanted "$PKG" || continue PROJ="$WORK/proj-$PKG" LOG="$WORK/$PKG.server.log" echo "── $PKG" php -d error_reporting=-1 "$ROOT/bin/duckbrain" install "$PROJ" --core-dir="$CORE_DIR" --yes >/dev/null php -d error_reporting=-1 "$ROOT/bin/duckbrain" add "$PKG" --root="$PROJ" --yes >/dev/null \ || { echo "$PKG:add|fail" >> "$RESULTS"; echo " add fallo"; FAILED=1; continue; } # Config del proyecto temporal: sqlite, clave para crypto y db local. PROJ_PATH="$PROJ" php -r ' $proj = getenv("PROJ_PATH"); $f = "$proj/config.php"; $cfg = (string) file_get_contents($f); $cfg = str_replace("define(\x27DB_TYPE\x27, \x27mysql\x27);", "define(\x27DB_TYPE\x27, \x27sqlite\x27);", $cfg); $cfg = str_replace("define(\x27DB_NAME\x27, \x27\x27);", "define(\x27DB_NAME\x27, \x27$proj/app.sqlite\x27);", $cfg); if (!str_contains($cfg, "PRIVATE_KEY")) { $cfg .= "\ndefine(\x27PRIVATE_KEY\x27, \x27compat-sweep-clave-de-prueba-32!\x27);\n"; } file_put_contents($f, $cfg);' # Router de compatibilidad del fixture (si lo hay). if [ -f "$COMPAT/$PKG/router.php" ]; then mkdir -p "$PROJ/src/Routers" sed "s|@COMPAT@|$COMPAT/$PKG|g" "$COMPAT/$PKG/router.php" > "$PROJ/src/Routers/Compat.php" fi # Servidor con diagnósticos al máximo y capturados. BASE="" : > "$LOG" for PORT in $(seq 18100 18120); do PHP_CLI_SERVER_WORKERS=4 php -d error_reporting=-1 -d display_errors=1 \ -d log_errors=1 -d error_log="$LOG" -S "127.0.0.1:$PORT" -t "$PROJ" "$PROJ/index.php" \ >/dev/null 2>&1 & SERVER_PID=$! for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do sleep 0.15 if curl -s -o /dev/null "http://127.0.0.1:$PORT/compat/ping"; then BASE="http://127.0.0.1:$PORT" break 2 fi done stop_server done if [ -z "$BASE" ]; then echo " no hubo servidor en 18100-18120" >&2 echo "$PKG:server|fail" >> "$RESULTS"; FAILED=1; continue fi # Aserciones del fixture (si lo hay; los niveles "none" no tienen). if [ -f "$COMPAT/$PKG/assert.sh" ]; then BASE="$BASE" PROJ="$PROJ" PKG="$PKG" LOG="$LOG" RESULTS="$RESULTS" COMPAT="$COMPAT" \ bash "$COMPAT/$PKG/assert.sh" || true fi stop_server # Criterio estricto: cualquier diagnostico en el log del servidor es fallo. if grep -qE 'Deprecated|Warning:|Notice:|Fatal error|Parse error' "$LOG"; then echo " diagnostico PHP detectado en el servidor (ver $LOG):" grep -E 'Deprecated|Warning:|Notice:|Fatal error|Parse error' "$LOG" | sed 's/^/ /' FAILED=1 fi done < "$WORK/pkgs" # --- merge de resultados con levels.json y reporte ------------------------------ LEVELS_PATH="$LEVELS" RESULTS_PATH="$RESULTS" REPORT_PATH="$REPORT" \ CORE_VER="$CORE_VER" CORE_SHA="$CORE_SHA" php -r ' $levels = json_decode((string) file_get_contents(getenv("LEVELS_PATH")), true); $results = []; foreach (file(getenv("RESULTS_PATH"), FILE_IGNORE_NEW_LINES) as $line) { [$id, $status] = array_pad(explode("|", $line, 2), 2, ""); $results[$id] = $status; } // Corre parciales (--only): hereda del reporte previo los ok del mismo core. $prior = is_file(getenv("REPORT_PATH")) ? (json_decode((string) file_get_contents(getenv("REPORT_PATH")), true) ?: []) : []; $stale = ($prior["core"]["version"] ?? null) !== getenv("CORE_VER"); $out = []; $fail = 0; foreach ($levels as $pkg => $comps) { foreach ($comps as $comp => $level) { $reason = null; if (is_array($level)) { $reason = $level["reason"] ?? null; $level = $level["level"]; } $id = "$pkg:$comp"; $status = $results[$id] ?? ($level === "none" ? "skipped" : "not_attempted"); if ($status === "not_attempted" && !$stale && ($prior["components"][$id]["status"] ?? "") === "ok") { $status = "ok"; } if (!in_array($status, ["ok", "skipped"], true)) { $fail++; } $out[$id] = array_filter([ "status" => $status, "level" => $level, "reason" => $reason, ], fn ($v) => $v !== null); } } $report = [ "core" => ["version" => getenv("CORE_VER"), "commit" => getenv("CORE_SHA")], "generated" => gmdate("Y-m-d\TH:i:s\Z"), "components" => $out, ]; file_put_contents(getenv("REPORT_PATH"), json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); foreach ($out as $id => $r) { printf(" %-24s %-13s %s%s\n", $id, $r["status"], $r["level"], isset($r["reason"]) ? " (" . $r["reason"] . ")" : ""); } exit($fail === 0 ? 0 : 1);' STATUS=$? if [ "$STATUS" = 0 ]; then echo "OK: barrido de compatibilidad completo contra core v$CORE_VER" else echo "FAIL: hay componentes sin verificar o con fallos (ver $REPORT)" >&2 fi exit $STATUS