xref: /PHP-7.4/sapi/fpm/tests/logtool.inc (revision c483b591)
1<?php
2
3namespace FPM;
4
5class LogTool
6{
7    const P_TIME = '\[\d\d-\w\w\w-\d{4} \d\d:\d\d:\d\d\]';
8    const P_PREFIX = '\[pool unconfined\] child \d+ said into stderr: ';
9    const P_PREFIX_STDOUT = '\[pool unconfined\] child \d+ said into stdout: ';
10    const FINAL_SUFFIX = ', pipe is closed';
11
12    const DEBUG = 'DEBUG';
13    const NOTICE = 'NOTICE';
14    const WARNING = 'WARNING';
15    const ERROR = 'ERROR';
16    const ALERT = 'ALERT';
17
18    /**
19     * @var string
20     */
21    private $message;
22
23    /**
24     * @var string
25     */
26    private $level;
27
28    /**
29     * @var int
30     */
31    private $position;
32
33    /**
34     * @var int
35     */
36    private $suffixPosition;
37
38    /**
39     * @var int
40     */
41    private $limit;
42
43    /**
44     * @var string
45     */
46    private $pattern;
47
48    /**
49     * @var string
50     */
51    private $error;
52
53    /**
54     * @var bool
55     */
56    private $pipeClosed = false;
57
58    /**
59     * @param string $message
60     * @param int $limit
61     * @param int $repeat
62     */
63    public function setExpectedMessage(string $message, int $limit, int $repeat = 0)
64    {
65        $this->message = ($repeat > 0) ? str_repeat($message, $repeat) : $message;
66        $this->limit = $limit;
67        $this->position = 0;
68    }
69
70    /**
71     * @param string $level
72     * @return int
73     */
74    public function setExpectedLevel(string $level)
75    {
76        return $this->level = $level;
77    }
78
79    /**
80     * @return string
81     */
82    public function getExpectedLevel(): string
83    {
84        return $this->level ?: 'WARNING';
85    }
86
87    /**
88     * @param bool $pipeClosed
89     */
90    public function setPipeClosed(bool $pipeClosed)
91    {
92        $this->pipeClosed = $pipeClosed;
93    }
94
95    /**
96     * @param string $line
97     * @return bool
98     */
99    public function checkTruncatedMessage(string $line)
100    {
101        if ($this->message === null) {
102            throw new \LogicException('The message has not been set');
103        }
104        $lineLen = strlen($line);
105        if (!$this->checkLineLength($line)) {
106            return false;
107        }
108        $this->pattern = '/^PHP message: (.*?)(\.\.\.)?$/';
109        if (preg_match($this->pattern, $line, $matches) === 0) {
110            return $this->error("Unexpected truncated message: {$line}");
111        }
112
113        if ($lineLen === $this->limit - strlen('NOTICE: ') - 1) {
114            if (!isset($matches[2])) {
115                return $this->error("The truncated line is not ended with '...'");
116            }
117            if (!$this->checkMessage($matches[1])) {
118                return false;
119            }
120        } else {
121            if (isset($matches[2])) {
122                // this is expecting that the expected message does not end with '...'
123                // which should not be an issue for the test purpose.
124                return $this->error("The line is complete and should not end with '...'");
125            }
126            if (!$this->checkMessage($matches[1], -1)) {
127                return false;
128            }
129        }
130
131        return true;
132    }
133
134    /**
135     * @param array $lines
136     * @param bool $terminated
137     * @param bool $decorated
138     * @return bool
139     */
140    public function checkWrappedMessage(array $lines, bool $terminated = true, bool $decorated = true, bool $is_stderr = true)
141    {
142        if ($this->message === null) {
143            throw new \LogicException('The message has not been set');
144        }
145        if ($decorated) {
146            $this->pattern = sprintf(
147                '/^(%s %s: %s)"([^"]*)"(.*)?$/',
148                self::P_TIME,
149                $this->getExpectedLevel(),
150                $is_stderr ? self::P_PREFIX : self::P_PREFIX_STDOUT
151            );
152        } else {
153            $this->pattern = null;
154        }
155
156        $idx = 0;
157        foreach ($lines as $idx => $line) {
158            if (!$this->checkLine($line)) {
159                break;
160            }
161        }
162
163        if ($this->suffixPosition > 0) {
164            $suffixPattern = sprintf(
165                '/^%s %s: %s(.*)$/',
166                self::P_TIME, $this->getExpectedLevel(),
167                $is_stderr ? self::P_PREFIX : self::P_PREFIX_STDOUT
168            );
169            $line = $lines[++$idx];
170            if (preg_match($suffixPattern, $line, $matches) === 0) {
171                return $this->error("Unexpected line: $line");
172            }
173            if ($matches[1] !== substr(self::FINAL_SUFFIX, $this->suffixPosition)) {
174                return $this->error(
175                    "The suffix has not been finished from position $this->suffixPosition in line: $line"
176                );
177            }
178        }
179
180        if ($terminated) {
181            return $this->expectTerminatorLines($lines, $idx);
182        }
183
184        return true;
185    }
186
187    /**
188     * @param string $line
189     * @return bool
190     */
191    private function checkLine(string $line)
192    {
193        if ($this->pattern === null) {
194            // plain (not decorated) output
195            $out = rtrim($line);
196            $finalSuffix = null;
197        } elseif (($res = preg_match($this->pattern, $line, $matches)) > 0) {
198            $out = $matches[2];
199            $finalSuffix = $matches[3] ?? false;
200        } else {
201            return $this->error("Unexpected line: $line");
202        }
203
204        $rem = strlen($this->message) - $this->position;
205        $lineLen = strlen($line);
206        if (!$this->checkLineLength($line, $lineLen)) {
207            return false;
208        }
209        if (!$this->checkMessage($out, $this->position)) {
210            return false;
211        }
212        $outLen = strlen($out);
213        if ($rem > $outLen) { // continuous line
214            if ($lineLen !== $this->limit) {
215                if ($lineLen + ($rem - $outLen) < $this->limit) {
216                    return $this->error("Printed less than the message len");
217                }
218                return $this->error(
219                    "The continuous line length is $lineLen but it should equal to limit $this->limit"
220                );
221            }
222            $this->position += $outLen;
223            return true;
224        }
225        if ($rem !== $outLen)  {
226            return $this->error("Printed more than the message len");
227        }
228        if (!$this->pipeClosed || $finalSuffix === null) {
229            return false;
230        }
231        if ($finalSuffix === false) {
232            return $this->error("No final suffix");
233        }
234        if (empty($finalSuffix) || strpos(self::FINAL_SUFFIX, $finalSuffix) === false) {
235            return $this->error("The final suffix has to be equal to ', pipe is closed'");
236        }
237        if (self::FINAL_SUFFIX !== $finalSuffix) {
238            $this->suffixPosition = strlen($finalSuffix);
239        }
240        // complete final suffix printed
241        return false;
242    }
243
244    /**
245     * @param string $line
246     * @param int $lineLen
247     * @return bool
248     */
249    private function checkLineLength(string $line, $lineLen = null) {
250        $lineLen = $lineLen ?: strlen($line);
251        if ($lineLen > $this->limit) {
252            return $this->error(
253                "The line length is $lineLen which is higher than limit $this->limit"
254            );
255        }
256
257        return true;
258    }
259
260    /**
261     * @param string $matchedMessage
262     * @param int $expectedMessageStart
263     * @return bool
264     */
265    private function checkMessage(string $matchedMessage, int $expectedMessageStart = 0)
266    {
267        if ($expectedMessageStart < 0) {
268            $expectedMessage = $this->message;
269        } else {
270            $expectedMessage = substr($this->message, $expectedMessageStart, strlen($matchedMessage));
271        }
272        if ($expectedMessage !== $matchedMessage) {
273            return $this->error(
274                sprintf(
275                    "The actual string(%d) does not match expected string(%d):\n",
276                    strlen($matchedMessage),
277                    strlen($expectedMessage)
278                ) .
279                "- EXPECT: '$expectedMessage'\n" .
280                "- ACTUAL: '$matchedMessage'"
281            );
282        }
283
284        return true;
285    }
286
287    /**
288     * @param array $lines
289     * @return bool
290     */
291    public function expectReloadingLines(array $lines)
292    {
293        if (
294            !$this->expectNotice($lines[0], 'Reloading in progress ...') ||
295            !$this->expectNotice($lines[1], 'reloading: .*')
296        ) {
297            return false;
298        }
299
300        for ($i = 2; $i < count($lines) - 2; $i++) {
301            if (!$this->expectNotice($lines[$i], 'using inherited socket fd=\d+, "[^"]+"')) {
302                return false;
303            }
304        }
305
306        return $this->expectStartingLines(array_splice($lines, $i));
307    }
308
309    /**
310     * @param array $lines
311     * @return bool
312     */
313    public function expectStartingLines(array $lines)
314    {
315        if ($this->getError()) {
316            return false;
317        }
318
319        if (count($lines) < 2) {
320            return $this->error("No starting lines");
321        }
322
323        return (
324            $this->expectNotice($lines[0], 'fpm is running, pid \d+') &&
325            $this->expectNotice($lines[1], 'ready to handle connections')
326        );
327    }
328
329    /**
330     * @param array $lines
331     * @param int $idx
332     * @return bool
333     */
334    public function expectTerminatorLines(array $lines, int $idx = -1)
335    {
336        if ($this->getError()) {
337            return false;
338        }
339
340        if (count($lines) - $idx < 3) {
341            return $this->error("No terminating lines");
342        }
343
344        return (
345            $this->expectNotice($lines[++$idx], 'Terminating ...') &&
346            $this->expectNotice($lines[++$idx], 'exiting, bye-bye!')
347        );
348    }
349
350    /**
351     * @param string $type
352     * @param string $line
353     * @param string $expectedMessage
354     * @param string|null $pool
355     * @return bool
356     */
357    public function expectEntry(string $type, string $line, string $expectedMessage, $pool = null)
358    {
359        if ($this->getError()) {
360            return false;
361        }
362        if ($pool !== null) {
363            $expectedMessage = '\[pool ' . $pool . '\] ' . $expectedMessage;
364        }
365
366        $line = rtrim($line);
367        $pattern = sprintf('/^%s %s: %s$/', self::P_TIME, $type, $expectedMessage);
368
369        if (preg_match($pattern, $line, $matches) === 0) {
370            return $this->error(
371                "The $type does not match expected message:\n" .
372                "- PATTERN: $pattern\n" .
373                "- MESSAGE: $line\n" .
374                "- EXPECT: '$expectedMessage'\n" .
375                "- ACTUAL: '" . substr($line, strpos($line, $type) + strlen($type) + 2) . "'"
376            );
377        }
378
379        return true;
380    }
381
382    /**
383     * @param string $line
384     * @param string $expectedMessage
385     * @param string|null $pool
386     * @return bool
387     */
388    public function expectDebug(string $line, string $expectedMessage, $pool = null)
389    {
390        return $this->expectEntry(self::DEBUG, $line, $expectedMessage, $pool);
391    }
392
393    /**
394     * @param string $line
395     * @param string $expectedMessage
396     * @param string|null $pool
397     * @return bool
398     */
399    public function expectNotice(string $line, string $expectedMessage, $pool = null)
400    {
401        return $this->expectEntry(self::NOTICE, $line, $expectedMessage, $pool);
402    }
403
404    /**
405     * @param string $line
406     * @param string $expectedMessage
407     * @param string|null $pool
408     * @return bool
409     */
410    public function expectWarning(string $line, string $expectedMessage, $pool = null)
411    {
412        return $this->expectEntry(self::WARNING, $line, $expectedMessage, $pool);
413    }
414
415    /**
416     * @param string $line
417     * @param string $expectedMessage
418     * @param string|null $pool
419     * @return bool
420     */
421    public function expectError(string $line, string $expectedMessage, $pool = null)
422    {
423        return $this->expectEntry(self::ERROR, $line, $expectedMessage, $pool);
424    }
425
426    /**
427     * @param string $line
428     * @param string $expectedMessage
429     * @param string|null $pool
430     * @return bool
431     */
432    public function expectAlert(string $line, string $expectedMessage, $pool = null)
433    {
434        return $this->expectEntry(self::ALERT, $line, $expectedMessage, $pool);
435    }
436
437    /**
438     * @param string $msg
439     * @return bool
440     */
441    private function error(string $msg)
442    {
443        $this->error = $msg;
444        echo "ERROR: $msg\n";
445        return false;
446    }
447
448    /**
449     * @return string
450     */
451    public function getError()
452    {
453        return $this->error;
454    }
455}
456
457if (isset($argv[1]) && $argv[1] === 'logtool-selftest') {
458    $cases = [
459        [
460            'limit' => 1050,
461            'lines' => [
462                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' .
463                    str_repeat('a', 968) . '"',
464                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' .
465                    str_repeat('a', 968) . '"',
466                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' .
467                    str_repeat('a', 112) . '", pipe is closed',
468                '[08-Oct-2017 19:53:55] NOTICE: Terminating ...',
469                '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!',
470            ],
471            'message' => str_repeat('a', 2048),
472            'type' => 'stdio',
473        ],
474        [
475            'limit' => 1050,
476            'lines' => [
477                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' .
478                    str_repeat('a', 968) . '"',
479                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' .
480                    str_repeat('a', 968) . '"',
481                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' .
482                    str_repeat('a', 964) . '", pi',
483                '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: pe is closed',
484                '[08-Oct-2017 19:53:55] NOTICE: Terminating ...',
485                '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!',
486            ],
487            'message' => str_repeat('a', 2900),
488            'type' => 'stdio',
489        ],
490        [
491            'limit' => 1024,
492            'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',989) . '...',
493            'message' => str_repeat('a', 2900),
494            'type' => 'message',
495        ],
496        [
497            'limit' => 1024,
498            'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',20),
499            'message' => str_repeat('a', 20),
500            'type' => 'message',
501        ],
502    ];
503    foreach ($cases as $case) {
504        printf("Test message with len %d and limit %d: ", strlen($case['message']), $case['limit']);
505        $logTool = new LogTool();
506        $logTool->setExpectedMessage($case['message'], $case['limit']);
507        if ($case['type'] === 'stdio') {
508            $logTool->checkWrappedMessage($case['lines']);
509        } else {
510            $logTool->checkTruncatedMessage($case['line']);
511        }
512        if (!$logTool->getError()) {
513            echo "OK\n";
514        }
515    }
516    echo "Done\n";
517}
518