Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Die Zeitangabe, die eine Antwort über sich selbst macht.
*
* Von außen ist nicht zu erkennen, wo die Zeit einer langsamen Seite hingeht:
* Eine Messung mit einem Abrufwerkzeug enthält Leitung, Proxy und Server in
* einer Zahl. Die Anwendung weiß es – und sagt es über `Server-Timing`.
*
* Geprüft wird, dass die Zahlen stimmen, dass sie zum spätestmöglichen
* Zeitpunkt entstehen und dass sie ohne Anmeldung nicht mitgehen.
*/
use OpenNIT\Core\RequestTiming;
return static function (): void {
$wurzel = dirname(__DIR__);
$quelle = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);
// ----------------------------------------------------------------
// Die Zählung
// ----------------------------------------------------------------
test('Ohne Abfragen steht dort null', function () {
RequestTiming::start(microtime(true));
assertSame(0, RequestTiming::queries());
assertSame(0.0, RequestTiming::dbMs());
});
test('Jede Abfrage zählt mit ihrer Dauer', function () {
RequestTiming::start(microtime(true));
RequestTiming::query(0.010);
RequestTiming::query(0.005);
RequestTiming::query(0.0);
assertSame(3, RequestTiming::queries());
assertSame(15.0, RequestTiming::dbMs());
});
test('Ein neuer Anfang setzt die Zählung zurück', function () {
// Sonst schleppte eine Anfrage die Zahlen der vorigen mit – im
// Dauerbetrieb eines Arbeitsprozesses wäre die Angabe wertlos.
RequestTiming::start(microtime(true));
RequestTiming::query(0.020);
assertSame(1, RequestTiming::queries());
RequestTiming::start(microtime(true));
assertSame(0, RequestTiming::queries());
assertSame(0.0, RequestTiming::dbMs());
});
test('Eine unsinnige Dauer verdreht die Summe nicht', function () {
RequestTiming::start(microtime(true));
RequestTiming::query(-5.0);
assertSame(1, RequestTiming::queries(), 'Die Abfrage wurde nicht gezählt');
assertSame(0.0, RequestTiming::dbMs(), 'Eine negative Dauer hat die Summe verkleinert');
});
test('Die verstrichene Zeit wächst', function () {
RequestTiming::start(microtime(true) - 0.25);
$ms = RequestTiming::elapsedMs();
assertTrue($ms >= 240.0 && $ms < 5000.0, 'Verstrichen: ' . $ms . ' ms');
});
test('Die Kopfzeile nennt Laufzeit, Datenbank und Anzahl', function () {
RequestTiming::start(microtime(true) - 0.1);
RequestTiming::query(0.030);
RequestTiming::query(0.012);
$kopf = RequestTiming::header();
assertTrue((bool)preg_match('/^app;dur=[\d.]+, db;dur=42\.0;desc="2 Abfragen"$/', $kopf), $kopf);
});
// ----------------------------------------------------------------
// Wo sie entsteht
// ----------------------------------------------------------------
test('Jede Abfrage läuft durch die Stelle, die sie zählt', function () use ($quelle) {
// Alle Lese- und Schreibwege der Datenbank gehen über execute(); dort
// steht die Messung. Ein zweiter Weg daran vorbei verfälschte die Zahl.
$db = $quelle('src/Core/Database.php');
$start = strpos($db, 'public function execute(');
assertTrue($start !== false, 'Die gemeinsame Stelle fehlt');
assertTrue(
str_contains(substr($db, $start, 700), 'RequestTiming::query('),
'Die Abfragen werden nicht gezählt'
);
foreach (['fetchOne', 'fetchAll', 'fetchColumn'] as $weg) {
$s = strpos($db, 'public function ' . $weg . '(');
assertTrue($s !== false, 'Der Weg ' . $weg . ' fehlt');
assertTrue(
str_contains(substr($db, $s, 400), '$this->execute('),
$weg . ' geht an der Zählung vorbei'
);
}
});
test('Die Zeitangabe entsteht erst, wenn die Antwort steht', function () use ($quelle) {
// Früher wären die Zahlen unvollständig, später ließe sich keine
// Kopfzeile mehr setzen.
$res = $quelle('src/Core/Response.php');
foreach (['public function html(', 'public function json('] as $weg) {
$start = strpos($res, $weg);
assertTrue($start !== false, 'Der Weg ' . $weg . ' fehlt');
$rumpf = substr($res, $start, 1600);
$setzen = strpos($rumpf, '$this->zeitangabeSetzen();');
$senden = strpos($rumpf, 'echo ');
assertTrue($setzen !== false, $weg . ' setzt keine Zeitangabe');
assertTrue($senden !== false, $weg . ' sendet nichts');
assertTrue($setzen < $senden, $weg . ' setzt die Zeitangabe nach der Ausgabe');
}
// Und sie versucht es nicht, wenn die Kopfzeilen schon unterwegs sind.
assertTrue(str_contains($res, 'headers_sent()'), 'Es wird nicht geprüft, ob noch eine Kopfzeile geht');
});
test('Ohne Anmeldung geht die Zeitangabe nicht mit', function () use ($quelle) {
// Wie lange ein Anmeldeversuch dauert, geht niemanden an, der ihn von
// außen stellt.
$app = $quelle('src/Core/Application.php');
assertTrue(
(bool)preg_match(
'/\$this->currentUser !== null\s*\)\s*\{\s*\$this->response->enableTiming\(\);/',
$app
),
'Die Zeitangabe hängt nicht an einer angemeldeten Anfrage'
);
$res = $quelle('src/Core/Response.php');
assertTrue(
str_contains($res, 'private bool $mitZeitangabe = false;'),
'Die Zeitangabe ist nicht standardmäßig aus'
);
});
test('Die Anfrage beginnt zu zählen, bevor gearbeitet wird', function () use ($quelle) {
$app = $quelle('src/Core/Application.php');
$start = strpos($app, 'private function initialize(): void');
assertTrue($start !== false, 'Der Bootstrap fehlt');
$rumpf = substr($app, $start, 900);
$zaehler = strpos($rumpf, 'RequestTiming::start(');
$db = strpos($rumpf, 'new Database(');
assertTrue($zaehler !== false, 'Die Anfrage beginnt nicht zu zählen');
assertTrue($db !== false, 'Die Datenbank wird hier nicht aufgebaut');
assertTrue($zaehler < $db, 'Der Aufbau der Datenbank fällt aus der Messung heraus');
});
test('Die Zeitangabe trägt keine Inhalte nach draußen', function () {
// Nur Zahlen: Laufzeit, Datenbankzeit, Anzahl. Kein Pfad, keine
// Abfrage, keine Kennung.
RequestTiming::start(microtime(true));
RequestTiming::query(0.001);
$kopf = RequestTiming::header();
foreach (['SELECT', 'FROM', 'WHERE', 'user', 'token', '/'] as $wort) {
assertFalse(str_contains($kopf, $wort), 'Die Kopfzeile enthält ' . $wort);
}
});
};