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