OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

RequestTimingTest.php

7 KB
<?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);
        }
    });
};