Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Deckt die Fortschreibung der Statusseiten-Tageshistorie ab.
*
* Der Snapshot schreibt aus `status_services` in `status_service_history`.
* Beide Tabellen führen eine Spalte `status`, weshalb in der Aktualisierung
* jede Spalte ihre Tabelle nennen muss. Fachlich gilt: pro Tag bleibt der
* schlechteste Status stehen, damit eine zwischenzeitlich behobene Störung in
* der Tagesanzeige sichtbar bleibt.
*/
return static function (): void {
/** Die Anweisung aus dem Handler. */
$sql = static function (): string {
$src = (string)file_get_contents(
dirname(__DIR__) . '/src/Cron/Handlers/StatusServiceSnapshotHandler.php'
);
$von = strpos($src, 'INSERT INTO status_service_history');
$bis = strpos($src, '"', $von);
return $von !== false ? substr($src, $von, $bis - $von) : '';
};
/** Spaltennamen einer Tabelle aus den Migrationen. @return list<string> */
$spalten = static function (string $tabelle): array {
foreach (glob(dirname(__DIR__) . '/database/migrations/*.sql') ?: [] as $datei) {
$inhalt = (string)file_get_contents($datei);
if (preg_match(
'/CREATE TABLE IF NOT EXISTS `' . preg_quote($tabelle, '/') . '` \((.*?)\n\) ENGINE/s',
$inhalt,
$m
)) {
preg_match_all('/^\s*`([a-z_]+)`\s+[A-Z]/mi', $m[1], $sm);
return $sm[1];
}
}
return [];
};
// ----------------------------------------------------------------
// Die Mehrdeutigkeit selbst
// ----------------------------------------------------------------
test('Beide Tabellen führen eine Spalte status', function () use ($spalten) {
// Das ist der Grund, warum unqualifizierte Namen nicht auflösbar sind.
assertTrue(in_array('status', $spalten('status_services'), true), 'Quelle ohne status');
assertTrue(in_array('status', $spalten('status_service_history'), true), 'Ziel ohne status');
// severity gibt es nur im Ziel – deshalb ist allein status mehrdeutig.
assertFalse(in_array('severity', $spalten('status_services'), true));
assertTrue(in_array('severity', $spalten('status_service_history'), true));
});
test('Die Aktualisierung nennt zu jeder Spalte ihre Tabelle', function () use ($sql) {
$text = $sql();
assertTrue($text !== '', 'Die Anweisung wurde nicht gefunden');
$odku = substr($text, (int)strpos($text, 'ON DUPLICATE KEY UPDATE'));
// Zuweisungsziele stehen am Zeilenanfang; alles Gelesene muss
// entweder VALUES(...) oder tabellenqualifiziert sein.
$rest = preg_replace(
['/status\s*=/', '/severity\s*=/', '/VALUES\([a-z_]+\)/', '/status_service_history\.[a-z_]+/'],
'',
$odku
);
assertFalse(str_contains($rest, 'status'), 'Unqualifiziertes status in der Aktualisierung');
assertFalse(str_contains($rest, 'severity'), 'Unqualifiziertes severity in der Aktualisierung');
});
test('Die Quelle ist benannt und ihre Spalten sind qualifiziert', function () use ($sql) {
$text = $sql();
$select = substr($text, (int)strpos($text, 'SELECT'), (int)strpos($text, 'ON DUPLICATE') - (int)strpos($text, 'SELECT'));
assertTrue(str_contains($select, 'FROM status_services s'), 'Die Quelle trägt keinen Namen');
assertTrue(str_contains($select, 's.id') && str_contains($select, 's.status'), 'Spalten der Quelle ohne Präfix');
});
test('Die Anweisung bleibt für MariaDB lesbar', function () use ($sql) {
// Die Alias-Schreibweise (INSERT … SELECT … AS neu) gibt es erst ab
// MySQL 8.0.19 und nicht in MariaDB; VALUES() können beide.
$text = $sql();
assertTrue(str_contains($text, 'VALUES(severity)'), 'VALUES() wird nicht genutzt');
assertFalse((bool)preg_match('/\bAS\s+new\b/i', $text), 'MySQL-eigene Alias-Schreibweise');
});
// ----------------------------------------------------------------
// Die fachliche Regel
// ----------------------------------------------------------------
/** Bildet die Aktualisierung nach: was nach dem Schreiben im Tag steht. */
$fortschreiben = static function (array $alt, array $neu): array {
return [
'status' => $neu['severity'] >= $alt['severity'] ? $neu['status'] : $alt['status'],
'severity' => max($alt['severity'], $neu['severity']),
];
};
test('Eine Verschlechterung setzt sich durch', function () use ($fortschreiben) {
assertSame(
['status' => 'major_outage', 'severity' => 3],
$fortschreiben(['status' => 'degraded', 'severity' => 1], ['status' => 'major_outage', 'severity' => 3])
);
});
test('Eine Erholung überschreibt die Störung des Tages nicht', function () use ($fortschreiben) {
// Kern der Historie: Der schlechteste Wert des Tages bleibt stehen.
assertSame(
['status' => 'major_outage', 'severity' => 3],
$fortschreiben(['status' => 'major_outage', 'severity' => 3], ['status' => 'operational', 'severity' => 0])
);
});
test('Bei gleicher Schwere zählt der jüngere Stand', function () use ($fortschreiben) {
assertSame(
['status' => 'degraded', 'severity' => 1],
$fortschreiben(['status' => 'degraded', 'severity' => 1], ['status' => 'degraded', 'severity' => 1])
);
});
test('Der Vergleich liest die bisherige Schwere, nicht die neu gesetzte', function () use ($sql) {
// Sonst hinge das Ergebnis an der Reihenfolge der Zuweisungen.
$odku = substr($sql(), (int)strpos($sql(), 'ON DUPLICATE KEY UPDATE'));
assertTrue(
strpos($odku, 'status =') < strpos($odku, 'severity ='),
'Die Schwere wird vor dem Status fortgeschrieben'
);
});
};