setUp(); try { $this->run(); } catch (\Throwable $e) { $this->fehlgeschlagen++; $this->fehler[] = sprintf( 'Ausnahme: %s (%s:%d)', $e->getMessage(), basename($e->getFile()), $e->getLine() ); } finally { $this->tearDown(); } self::$gesamtBestanden += $this->bestanden; self::$gesamtFehler += $this->fehlgeschlagen; foreach ($this->fehler as $meldung) { self::$alleFehler[] = ($this->testName ?: static::class) . ': ' . $meldung; } } protected function setUp(): void { \Finanzmanager\Database::beginTransaction(); } protected function tearDown(): void { try { \Finanzmanager\Database::rollBack(); } catch (\Throwable) { // Transaktion war eventuell nicht aktiv. } // Der Rollback dreht nur die Datenbank zurück; im Einstellungs-Cache // stünden sonst Werte, die es nicht mehr gibt. \Finanzmanager\Models\Einstellung::cacheLeeren(); \Finanzmanager\Mandant::setze(null); $this->raeumeTempPfadeAuf(); } /** * Legt einen Benutzer an und macht ihn zum Mandanten. Stammdatentests * brauchen das: Konten, Kategorien und Klassen hängen an einer benutzer_id. * Der Rollback in tearDown() räumt ihn wieder weg. */ protected function mandantAnlegen(): int { $id = \Finanzmanager\Models\Benutzer::anlegen( 'test_' . bin2hex(random_bytes(4)), 'testpasswort-' . bin2hex(random_bytes(4)) ); \Finanzmanager\Mandant::setze($id); return $id; } // ── Assertions ─────────────────────────────────────────────────────────── protected function assert(bool $bedingung, string $beschreibung): void { if ($bedingung) { $this->bestanden++; return; } $this->fehlgeschlagen++; $this->fehler[] = $beschreibung; } protected function assertEquals(mixed $erwartet, mixed $ist, string $beschreibung): void { $this->assert( $erwartet === $ist, sprintf('%s (erwartet: %s, war: %s)', $beschreibung, self::alsText($erwartet), self::alsText($ist)) ); } protected function assertNotEmpty(mixed $wert, string $beschreibung): void { $this->assert(!empty($wert), $beschreibung . ' (war leer)'); } protected function assertNull(mixed $wert, string $beschreibung): void { $this->assert($wert === null, $beschreibung . ' (war nicht null: ' . self::alsText($wert) . ')'); } protected function assertGreaterThan(float|int $minimum, float|int $wert, string $beschreibung): void { $this->assert($wert > $minimum, sprintf('%s (%s ist nicht größer als %s)', $beschreibung, $wert, $minimum)); } // ── Temporäre Pfade ────────────────────────────────────────────────────── protected function registriereTempPfad(string $pfad): string { $this->tempPfade[] = $pfad; return $pfad; } protected function tempPfad(string $endung = ''): string { return $this->registriereTempPfad( sys_get_temp_dir() . '/finanzmanager_test_' . bin2hex(random_bytes(6)) . $endung ); } protected function raeumeTempPfadeAuf(): void { // Umgekehrte Reihenfolge: verschachtelte Pfade vor ihren Elternverzeichnissen. foreach (array_reverse($this->tempPfade) as $pfad) { if (!file_exists($pfad) && !is_link($pfad)) { continue; } if (!self::liegtImTempBereich($pfad)) { echo " ! Nicht aufgeräumt (außerhalb der Testverzeichnisse): {$pfad}\n"; continue; } self::entferneRekursiv($pfad); } $this->tempPfade = []; } /** Sicherheitsnetz: gelöscht wird ausschließlich unterhalb der Temp-Verzeichnisse. */ private static function liegtImTempBereich(string $pfad): bool { $echt = realpath($pfad); if ($echt === false) { $eltern = realpath(dirname($pfad)); if ($eltern === false) { return false; } $echt = $eltern . '/' . basename($pfad); } foreach ([sys_get_temp_dir(), \Finanzmanager\Config::get('import_dir')] as $wurzel) { $wurzelEcht = is_string($wurzel) && $wurzel !== '' ? realpath($wurzel) : false; if ($wurzelEcht !== false && str_starts_with($echt, $wurzelEcht . '/')) { return true; } } return false; } private static function entferneRekursiv(string $pfad): void { if (is_dir($pfad) && !is_link($pfad)) { foreach (scandir($pfad) ?: [] as $eintrag) { if ($eintrag !== '.' && $eintrag !== '..') { self::entferneRekursiv($pfad . '/' . $eintrag); } } @rmdir($pfad); return; } @unlink($pfad); } // ── Ergebnisse ─────────────────────────────────────────────────────────── public function name(): string { return $this->testName !== '' ? $this->testName : static::class; } public function bestanden(): int { return $this->bestanden; } public function fehlgeschlagen(): int { return $this->fehlgeschlagen; } public function fehlermeldungen(): array { return $this->fehler; } public static function gesamtBestanden(): int { return self::$gesamtBestanden; } public static function gesamtFehler(): int { return self::$gesamtFehler; } public static function alleFehler(): array { return self::$alleFehler; } private static function alsText(mixed $wert): string { return match (true) { is_bool($wert) => $wert ? 'true' : 'false', is_null($wert) => 'null', is_array($wert) => json_encode($wert, JSON_UNESCAPED_UNICODE) ?: 'array', default => (string)$wert, }; } }