Fix #195: run-tests.sh meldet keinen falschen Erfolg mehr bei Testfehlern

python3-Abhängigkeit entfernt (Auswertung jetzt per php -r), Exit-Code
von tests/run.php wird korrekt ausgewertet statt mit || true verworfen.
--json-Modus gibt zusätzlich keine Fortschrittsanzeige/Warnungen mehr
auf STDOUT aus, damit die JSON-Ausgabe überhaupt parsbar ist.
This commit is contained in:
fds2610-bot 2026-08-21 09:30:34 +02:00
parent a55731b08f
commit 68ce22894f
5 changed files with 131 additions and 42 deletions

View file

@ -56,9 +56,9 @@ Tests liegen in `tests/` und folgen dem Namensschema `NN_FunktionsnameTest.php`.
Jede Testklasse erweitert `TestCase` und implementiert `run()`. Jede Testklasse erweitert `TestCase` und implementiert `run()`.
```bash ```bash
docker compose exec -u www-data web php tests/run.php # alle Tests ausführen docker compose exec -u www-data web php tests/run.php # alle Tests ausführen
./scripts/run-tests.sh # Tests + automatische Issue-Erstellung bei Fehlern docker compose exec -u www-data web ./scripts/run-tests.sh # Tests + automatische Issue-Erstellung bei Fehlern
./scripts/run-tests.sh 42 # Tests; bei Fehlern Kommentar in Issue #42 docker compose exec -u www-data web ./scripts/run-tests.sh 42 # Tests; bei Fehlern Kommentar in Issue #42
``` ```
**Immer `-u www-data`:** ohne die Option laufen die Tests als root und legen **Immer `-u www-data`:** ohne die Option laufen die Tests als root und legen

View file

@ -2,6 +2,12 @@
--- ---
## 2026-08-21 – run-tests.sh: Commit-Gate meldete fälschlich Erfolg bei fehlgeschlagenen Tests (#195)
- **`scripts/run-tests.sh`:** Die Auswertung von `php tests/run.php --json` hing an `python3`, das weder im Container noch auf dem Host installiert ist – der Fallback `|| echo "0"` verwandelte jeden Parse-Fehler in „0 fehlgeschlagene Tests", das Commit-Gate griff also nie. Erfolg/Misserfolg hängt jetzt direkt am Exit-Code von `php tests/run.php --json`; Detailauswertung (`failed`, `failures`) läuft über `php -r` statt Python. Lässt sich die JSON-Ausgabe nicht auswerten, bricht das Skript mit einem Fehler ab statt fälschlich Erfolg zu melden. Der zuvor ungenutzte `EXIT_CODE` (verworfen durch ein voranstehendes `|| true`) wird jetzt tatsächlich ausgewertet. Die JSON-Payloads für die automatische Issue-Erstellung werden ebenfalls per `php -r` statt Python gebaut (Body-Text jetzt mit echten Zeilenumbrüchen statt literalem `\n`).
- **`tests/run.php` / `tests/TestCase.php`:** `--json`-Modus gab bisher zusätzlich zur JSON-Ausgabe die komplette Fortschrittsanzeige (✓/✗ je Assertion) auf STDOUT aus – dadurch war die JSON-Ausgabe nie für sich allein parsbar, unabhängig vom Python-Problem. `TestCase::setQuiet()` unterdrückt diese Ausgabe im `--json`-Modus; PHP-Warnungen/-Notices einzelner Tests gehen in diesem Modus zusätzlich per `ini_set('display_errors', 'stderr')` nach STDERR statt STDOUT.
- **`CLAUDE.md`:** Aufrufzeilen für `./scripts/run-tests.sh` auf `docker compose exec -u www-data web ./scripts/run-tests.sh` korrigiert – das Skript läuft nur im Container, auf dem Host ist kein PHP installiert.
## 2026-08-21 – SMTP-SSRF-Schutz: IP-Pinning gegen DNS-Rebinding, Admin-Pflicht für SMTP-Einstellungen (#193) ## 2026-08-21 – SMTP-SSRF-Schutz: IP-Pinning gegen DNS-Rebinding, Admin-Pflicht für SMTP-Einstellungen (#193)
- **`src/Services/SsrfGuard.php`:** neue `resolvePinnedHost(string $host): string[]` – wie `assertPublicHost()`, gibt aber zusätzlich die geprüfte(n) IP-Adresse(n) zurück, analog zu `resolvePinned()` für https-URLs. `assertPublicHost()` ruft sie jetzt intern auf. - **`src/Services/SsrfGuard.php`:** neue `resolvePinnedHost(string $host): string[]` – wie `assertPublicHost()`, gibt aber zusätzlich die geprüfte(n) IP-Adresse(n) zurück, analog zu `resolvePinned()` für https-URLs. `assertPublicHost()` ruft sie jetzt intern auf.

View file

@ -3,8 +3,8 @@
# ============================================================ # ============================================================
# Steuerkram Test-Runner mit automatischer Issue-Erstellung # Steuerkram Test-Runner mit automatischer Issue-Erstellung
# #
# Verwendung: # Verwendung (im Container):
# ./scripts/run-tests.sh [ISSUE_NUMMER] # docker compose exec -u www-data web ./scripts/run-tests.sh [ISSUE_NUMMER]
# #
# Parameter: # Parameter:
# ISSUE_NUMMER (optional) Nummer des aktuell bearbeiteten Issues, # ISSUE_NUMMER (optional) Nummer des aktuell bearbeiteten Issues,
@ -13,7 +13,7 @@
# Umgebungsvariablen: # Umgebungsvariablen:
# CODEBERG_TOKEN Codeberg-API-Token (für Issue-Erstellung) # CODEBERG_TOKEN Codeberg-API-Token (für Issue-Erstellung)
# #
# Exit-Code: 0 = alle Tests OK, 1 = Fehler gefunden # Exit-Code: 0 = alle Tests OK, 1 = Fehler gefunden (oder Auswertung fehlgeschlagen)
# ============================================================ # ============================================================
set -euo pipefail set -euo pipefail
@ -39,22 +39,42 @@ fi
# ── Tests ausführen ────────────────────────────────────────── # ── Tests ausführen ──────────────────────────────────────────
echo "" echo ""
echo "Führe Tests aus..." echo "Führe Tests aus..."
JSON_OUTPUT=$($PHP tests/run.php --json 2>&1) || true # STDERR wird bewusst NICHT mitgefangen: tests/run.php schreibt im --json-Modus
EXIT_CODE=$? # etwaige PHP-Warnungen/-Notices dorthin, damit STDOUT ausschließlich das JSON
# enthält (siehe tests/run.php). STDERR landet direkt im Terminal.
JSON_OUTPUT=$($PHP tests/run.php --json) && EXIT_CODE=0 || EXIT_CODE=$?
# Auch die lesbare Ausgabe zeigen # Auch die lesbare Ausgabe zeigen
$PHP tests/run.php || true $PHP tests/run.php || true
# ── Ergebnis auswerten ─────────────────────────────────────── # ── Ergebnis auswerten ───────────────────────────────────────
FAILED=$(echo "$JSON_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('failed',0))" 2>/dev/null || echo "0") if [ "$EXIT_CODE" -eq 0 ]; then
FAILURES_JSON=$(echo "$JSON_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get('failures',[])))" 2>/dev/null || echo "[]")
if [ "$FAILED" = "0" ] || [ "$FAILED" = "" ]; then
echo "" echo ""
echo "✓ Alle Tests bestanden. Commit kann fortgesetzt werden." echo "✓ Alle Tests bestanden. Commit kann fortgesetzt werden."
exit 0 exit 0
fi fi
# tests/run.php meldet Fehlschläge (oder einen sonstigen Fehler) über den
# Exit-Code. Die Details holen wir uns per PHP aus dem JSON – lässt sich die
# Ausgabe nicht auswerten, bricht das Skript hier ab (set -e), statt
# fälschlich Erfolg zu melden.
FAILED=$(echo "$JSON_OUTPUT" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
if (!is_array($d) || !array_key_exists("failed", $d)) {
fwrite(STDERR, "Ungültige JSON-Ausgabe von tests/run.php --json\n");
exit(1);
}
echo $d["failed"];
')
FAILURES_JSON=$(echo "$JSON_OUTPUT" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
if (!is_array($d) || !array_key_exists("failures", $d)) {
fwrite(STDERR, "Ungültige JSON-Ausgabe von tests/run.php --json\n");
exit(1);
}
echo json_encode($d["failures"]);
')
echo "" echo ""
echo "✗ ${FAILED} Test(s) fehlgeschlagen." echo "✗ ${FAILED} Test(s) fehlgeschlagen."
@ -63,7 +83,10 @@ if [ -z "${CODEBERG_TOKEN:-}" ]; then
echo "" echo ""
echo "Hinweis: CODEBERG_TOKEN nicht gesetzt – keine automatischen Issues erstellt." echo "Hinweis: CODEBERG_TOKEN nicht gesetzt – keine automatischen Issues erstellt."
echo "Fehlgeschlagene Tests:" echo "Fehlgeschlagene Tests:"
echo "$FAILURES_JSON" | python3 -c "import json,sys; [print(' •', f) for f in json.load(sys.stdin)]" 2>/dev/null || true echo "$FAILURES_JSON" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
foreach ((is_array($d) ? $d : []) as $f) { echo " • {$f}\n"; }
'
exit 1 exit 1
fi fi
@ -79,51 +102,83 @@ while IFS= read -r failure; do
# Titel kürzen (max. 100 Zeichen) # Titel kürzen (max. 100 Zeichen)
TITLE="[BUG] Test fehlgeschlagen: ${failure:0:80}" TITLE="[BUG] Test fehlgeschlagen: ${failure:0:80}"
# Body zusammenbauen # Body zusammenbauen (mit echten Zeilenumbrüchen, kein "\n"-Text)
BODY="## Automatisch erkannter Testfehler\n\n" BODY_LINES=(
BODY+="**Fehlermeldung:** \`${failure}\`\n\n" "## Automatisch erkannter Testfehler"
BODY+="**Erkannt am:** $(date '+%Y-%m-%d %H:%M:%S')\n\n" ""
BODY+="**Commit:** $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')\n\n" "**Fehlermeldung:** \`${failure}\`"
""
"**Erkannt am:** $(date '+%Y-%m-%d %H:%M:%S')"
""
"**Commit:** $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')"
""
)
if [ -n "$CURRENT_ISSUE" ]; then if [ -n "$CURRENT_ISSUE" ]; then
BODY+="Dieser Fehler wurde beim Bearbeiten von Issue #${CURRENT_ISSUE} entdeckt.\n\n" BODY_LINES+=("Dieser Fehler wurde beim Bearbeiten von Issue #${CURRENT_ISSUE} entdeckt." "")
fi fi
BODY_LINES+=(
"### Schritte zur Reproduktion"
"1. \`docker compose exec -u www-data web php tests/run.php\` ausführen"
"2. Fehler tritt im oben genannten Test auf"
""
"_Dieser Issue wurde automatisch durch den Test-Runner erstellt._"
"_Das Label 'automate' wurde bewusst NICHT gesetzt (Human-in-the-loop)._"
)
BODY=$(printf '%s\n' "${BODY_LINES[@]}")
BODY+="### Schritte zur Reproduktion\n" PAYLOAD=$(TITLE="$TITLE" BODY="$BODY" php -r '
BODY+="1. \`docker compose exec -u www-data web php tests/run.php\` ausführen\n" echo json_encode([
BODY+="2. Fehler tritt im oben genannten Test auf\n\n" "title" => getenv("TITLE"),
BODY+="_Dieser Issue wurde automatisch durch den Test-Runner erstellt._\n" "body" => getenv("BODY"),
BODY+="_Das Label 'automate' wurde bewusst NICHT gesetzt (Human-in-the-loop)._" "labels" => [],
]);
')
RESPONSE=$(curl -s -X POST \ RESPONSE=$(curl -s -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \ -H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({'title': '${TITLE}', 'body': '${BODY}', 'labels': []}))")" \ -d "$PAYLOAD" \
"${API_BASE}/repos/${REPO}/issues") "${API_BASE}/repos/${REPO}/issues")
ISSUE_NR=$(echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('number','?'))" 2>/dev/null || echo "?") ISSUE_NR=$(echo "$RESPONSE" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
echo (is_array($d) && isset($d["number"])) ? $d["number"] : "?";
')
CREATED_ISSUES+=("#${ISSUE_NR}") CREATED_ISSUES+=("#${ISSUE_NR}")
echo " → Issue #${ISSUE_NR} erstellt: ${failure:0:60}..." echo " → Issue #${ISSUE_NR} erstellt: ${failure:0:60}..."
done < <(echo "$FAILURES_JSON" | python3 -c "import json,sys; [print(f) for f in json.load(sys.stdin)]" 2>/dev/null) done < <(echo "$FAILURES_JSON" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
foreach ((is_array($d) ? $d : []) as $f) { echo $f, "\n"; }
')
# ── Kommentar im aktuellen Issue hinterlassen ───────────────── # ── Kommentar im aktuellen Issue hinterlassen ─────────────────
if [ -n "$CURRENT_ISSUE" ] && [ ${#CREATED_ISSUES[@]} -gt 0 ]; then if [ -n "$CURRENT_ISSUE" ] && [ ${#CREATED_ISSUES[@]} -gt 0 ]; then
ISSUE_LINKS=$(printf '%s, ' "${CREATED_ISSUES[@]}") ISSUE_LINKS=$(printf '%s, ' "${CREATED_ISSUES[@]}")
ISSUE_LINKS="${ISSUE_LINKS%, }" ISSUE_LINKS="${ISSUE_LINKS%, }"
COMMENT="### Automatische Testläufe\n\n" COMMENT_LINES=(
COMMENT+="Beim Bearbeiten dieses Issues wurden **${FAILED} Test(s)** fehlgeschlagen.\n\n" "### Automatische Testläufe"
COMMENT+="Folgende Bug-Issues wurden automatisch erstellt (ohne 'automate'-Label – bitte manuell prüfen):\n\n" ""
"Beim Bearbeiten dieses Issues wurden **${FAILED} Test(s)** fehlgeschlagen."
""
"Folgende Bug-Issues wurden automatisch erstellt (ohne 'automate'-Label – bitte manuell prüfen):"
""
)
for issue in "${CREATED_ISSUES[@]}"; do for issue in "${CREATED_ISSUES[@]}"; do
COMMENT+="- ${issue}\n" COMMENT_LINES+=("- ${issue}")
done done
COMMENT+="\n_Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')_" COMMENT_LINES+=("" "_Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')_")
COMMENT=$(printf '%s\n' "${COMMENT_LINES[@]}")
PAYLOAD=$(COMMENT="$COMMENT" php -r '
echo json_encode(["body" => getenv("COMMENT")]);
')
curl -s -X POST \ curl -s -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \ -H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({'body': '${COMMENT}'}))")" \ -d "$PAYLOAD" \
"${API_BASE}/repos/${REPO}/issues/${CURRENT_ISSUE}/comments" > /dev/null "${API_BASE}/repos/${REPO}/issues/${CURRENT_ISSUE}/comments" > /dev/null
echo "" echo ""

View file

@ -14,6 +14,14 @@ abstract class TestCase
private static int $totalFailed = 0; private static int $totalFailed = 0;
private static array $allFailures = []; private static array $allFailures = [];
/** Unterdrückt die Fortschrittsausgabe (z. B. im --json-Modus von tests/run.php). */
private static bool $quiet = false;
public static function setQuiet(bool $quiet): void
{
self::$quiet = $quiet;
}
protected int $passed = 0; protected int $passed = 0;
protected int $failed = 0; protected int $failed = 0;
protected array $failures = []; protected array $failures = [];
@ -95,6 +103,8 @@ abstract class TestCase
continue; continue;
} }
if (!self::isInsideCleanupRoot($path)) { if (!self::isInsideCleanupRoot($path)) {
// Nicht durch $quiet unterdrückt: Test 79 prüft diese Meldung gezielt
// über ob_start()/ob_get_clean() ihrer eigenen TestCase-Instanz.
echo " \e[33m!\e[0m Nicht aufgeräumt (außerhalb der Testverzeichnisse): {$path}\n"; echo " \e[33m!\e[0m Nicht aufgeräumt (außerhalb der Testverzeichnisse): {$path}\n";
continue; continue;
} }
@ -145,11 +155,15 @@ abstract class TestCase
{ {
if ($condition) { if ($condition) {
$this->passed++; $this->passed++;
echo " \e[32m✓\e[0m {$message}\n"; if (!self::$quiet) {
echo " \e[32m✓\e[0m {$message}\n";
}
} else { } else {
$this->failed++; $this->failed++;
$this->failures[] = "[{$this->testName}] {$message}"; $this->failures[] = "[{$this->testName}] {$message}";
echo " \e[31m✗\e[0m {$message}\n"; if (!self::$quiet) {
echo " \e[31m✗\e[0m {$message}\n";
}
} }
} }
@ -185,7 +199,9 @@ abstract class TestCase
public function execute(): array public function execute(): array
{ {
echo "\n\e[1m{$this->testName}\e[0m\n"; if (!self::$quiet) {
echo "\n\e[1m{$this->testName}\e[0m\n";
}
$this->setUp(); $this->setUp();
try { try {
$this->run(); $this->run();
@ -193,15 +209,19 @@ abstract class TestCase
$this->failed++; $this->failed++;
$msg = "[{$this->testName}] Ausnahme: " . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(); $msg = "[{$this->testName}] Ausnahme: " . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
$this->failures[] = $msg; $this->failures[] = $msg;
echo " \e[31m✗\e[0m Unerwartete Ausnahme: " . $e->getMessage() . "\n"; if (!self::$quiet) {
echo " \e[31m✗\e[0m Unerwartete Ausnahme: " . $e->getMessage() . "\n";
}
} finally { } finally {
$this->tearDown(); $this->tearDown();
} }
$status = $this->failed === 0 if (!self::$quiet) {
? "\e[32mBestanden ({$this->passed}/{$this->passed})\e[0m" $status = $this->failed === 0
: "\e[31m{$this->failed} Fehler, {$this->passed} OK\e[0m"; ? "\e[32mBestanden ({$this->passed}/{$this->passed})\e[0m"
echo " → {$status}\n"; : "\e[31m{$this->failed} Fehler, {$this->passed} OK\e[0m";
echo " → {$status}\n";
}
self::$totalPassed += $this->passed; self::$totalPassed += $this->passed;
self::$totalFailed += $this->failed; self::$totalFailed += $this->failed;

View file

@ -28,6 +28,14 @@ require __DIR__ . '/TestCase.php';
\Steuerkram\Config::load(); \Steuerkram\Config::load();
$jsonMode = in_array('--json', $argv ?? [], true); $jsonMode = in_array('--json', $argv ?? [], true);
TestCase::setQuiet($jsonMode);
// PHP-Warnungen/-Notices einzelner Tests (z. B. absichtlich provozierte
// Fehlerfälle) dürfen die JSON-Ausgabe auf STDOUT nicht verunreinigen –
// scripts/run-tests.sh wertet sie sonst nicht mehr auswertbar aus.
if ($jsonMode) {
ini_set('display_errors', 'stderr');
}
/** /**
* Erfasst den Bestand unterhalb der Datenverzeichnisse. * Erfasst den Bestand unterhalb der Datenverzeichnisse.