Test Failed
Pull Request — main (#3)
by Colin
34:36
created

TestLogger   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 54
c 6
b 0
f 0
dl 0
loc 133
ccs 38
cts 38
cp 1
rs 10
wmc 20

9 Methods

Rating   Name   Duplication   Size   Complexity  
A hasRecordThatPasses() 0 13 4
A hasRecord() 0 13 4
A __call() 0 15 4
A hasRecordThatContains() 0 5 1
A hasRecords() 0 3 1
A __construct() 0 11 3
A log() 0 10 1
A reset() 0 4 1
A hasRecordThatMatches() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ColinODell\PsrTestLogger;
6
7
use Psr\Log\AbstractLogger;
8
use Psr\Log\LogLevel;
9
10
/**
11
 * Used for testing purposes.
12
 *
13
 * It records all records and gives you access to them for verification.
14
 *
15
 * @method bool hasEmergency(string|array $record)
16
 * @method bool hasAlert(string|array $record)
17
 * @method bool hasCritical(string|array $record)
18
 * @method bool hasError(string|array $record)
19
 * @method bool hasWarning(string|array $record)
20
 * @method bool hasNotice(string|array $record)
21
 * @method bool hasInfo(string|array $record)
22
 * @method bool hasDebug(string|array $record)
23
 * @method bool hasEmergencyRecords()
24
 * @method bool hasAlertRecords()
25
 * @method bool hasCriticalRecords()
26
 * @method bool hasErrorRecords()
27
 * @method bool hasWarningRecords()
28
 * @method bool hasNoticeRecords()
29
 * @method bool hasInfoRecords()
30
 * @method bool hasDebugRecords()
31
 * @method bool hasEmergencyThatContains(string $message)
32
 * @method bool hasAlertThatContains(string $message)
33
 * @method bool hasCriticalThatContains(string $message)
34
 * @method bool hasErrorThatContains(string $message)
35
 * @method bool hasWarningThatContains(string $message)
36
 * @method bool hasNoticeThatContains(string $message)
37
 * @method bool hasInfoThatContains(string $message)
38
 * @method bool hasDebugThatContains(string $message)
39
 * @method bool hasEmergencyThatMatches(string $regex)
40
 * @method bool hasAlertThatMatches(string $regex)
41
 * @method bool hasCriticalThatMatches(string $regex)
42
 * @method bool hasErrorThatMatches(string $regex)
43
 * @method bool hasWarningThatMatches(string $regex)
44
 * @method bool hasNoticeThatMatches(string $regex)
45
 * @method bool hasInfoThatMatches(string $regex)
46
 * @method bool hasDebugThatMatches(string $regex)
47
 * @method bool hasEmergencyThatPasses(callable $predicate)
48
 * @method bool hasAlertThatPasses(callable $predicate)
49
 * @method bool hasCriticalThatPasses(callable $predicate)
50
 * @method bool hasErrorThatPasses(callable $predicate)
51
 * @method bool hasWarningThatPasses(callable $predicate)
52
 * @method bool hasNoticeThatPasses(callable $predicate)
53
 * @method bool hasInfoThatPasses(callable $predicate)
54
 * @method bool hasDebugThatPasses(callable $predicate)
55
 *
56
 * Adapted from psr/log,
57
 * Copyright (c) 2012 PHP Framework Interoperability Group
58
 * Used under the MIT license
59
 */
60
final class TestLogger extends AbstractLogger
61
{
62
    /** @var array<int, array<string, mixed>> */
63
    public array $records = [];
64
65
    /** @var array<string|int, array<int, array<string, mixed>>> */
66
    public array $recordsByLevel = [];
67
68
    /** @var array<LogLevel::*, string|int> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<LogLevel:: at position 2 could not be parsed: Expected '>' at position 2, but found 'LogLevel'.
Loading history...
69
    private array $levelMap = [
70
        LogLevel::EMERGENCY => LogLevel::EMERGENCY,
71
        LogLevel::ALERT => LogLevel::ALERT,
72
        LogLevel::CRITICAL => LogLevel::CRITICAL,
73 82
        LogLevel::ERROR => LogLevel::ERROR,
74
        LogLevel::WARNING => LogLevel::WARNING,
75 82
        LogLevel::NOTICE => LogLevel::NOTICE,
76
        LogLevel::INFO => LogLevel::INFO,
77
        LogLevel::DEBUG => LogLevel::DEBUG,
78
    ];
79
80
    /**
81 82
     * @param array<LogLevel::*, string|int>|null $levelMap
82 82
     *   Keys are LogLevel::*, values are alternative strings or integers used as log levels in the SUT.
83
     */
84
    public function __construct(array|null $levelMap = null)
85
    {
86
        if (\is_array($levelMap)) {
87
            // Assert that $levelMap contains exactly the same keys (no more or less) than LogLevel::* values
88 18
            $diff = \array_diff(\array_keys($levelMap), \array_values($this->levelMap));
89
            if (\count($diff) > 0) {
90 18
                throw new \InvalidArgumentException('Level map keys must be the LogLevel::* values; passed ' . \print_r($levelMap, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($levelMap, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
                throw new \InvalidArgumentException('Level map keys must be the LogLevel::* values; passed ' . /** @scrutinizer ignore-type */ \print_r($levelMap, true));
Loading history...
91
            }
92
        }
93
94
        $this->levelMap = $levelMap ?? $this->levelMap;
95
    }
96
97 16
    /**
98
     * {@inheritDoc}
99 16
     *
100 16
     * @param array<array-key, mixed> $context
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, mixed> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, mixed>.
Loading history...
101
     */
102
    public function log($level, $message, array $context = []): void
103 16
    {
104 16
        $record = [
105 16
            'level' => $level,
106
            'message' => $message,
107
            'context' => $context,
108 16
        ];
109
110
        $this->recordsByLevel[$record['level']][] = $record;
111
        $this->records[]                          = $record;
112
    }
113
114
    public function hasRecords(string|int $level): bool
115 16
    {
116
        return isset($this->recordsByLevel[$level]);
117 16
    }
118 16
119
    /**
120
     * @param string|array<string, mixed> $record
121
     */
122
    public function hasRecord(string|array $record, string|int $level): bool
123
    {
124
        if (\is_string($record)) {
0 ignored issues
show
introduced by
The condition is_string($record) is always false.
Loading history...
125 16
            $record = ['message' => $record];
126
        }
127 16
128 16
        return $this->hasRecordThatPasses(static function (array $rec) use ($record) {
129
            if ($rec['message'] !== $record['message']) {
130
                return false;
131
            }
132
133
            return ! isset($record['context']) || $rec['context'] === $record['context'];
134
        }, $level);
135
    }
136 64
137
    public function hasRecordThatContains(string $message, string|int $level): bool
138 64
    {
139 64
        return $this->hasRecordThatPasses(static function (array $rec) use ($message) {
140
            return \strpos($rec['message'], $message) !== false;
141
        }, $level);
142 64
    }
143 64
144 64
    public function hasRecordThatMatches(string $regex, string|int $level): bool
145
    {
146
        return $this->hasRecordThatPasses(static function ($rec) use ($regex) {
147
            return \preg_match($regex, $rec['message']) > 0;
148 32
        }, $level);
149
    }
150
151
    /**
152
     * @param callable(array<string, mixed>, int): bool $predicate
153
     */
154 82
    public function hasRecordThatPasses(callable $predicate, string|int $level): bool
155
    {
156 82
        if (! isset($this->recordsByLevel[$level])) {
157 80
            return false;
158 80
        }
159 80
160 80
        foreach ($this->recordsByLevel[$level] as $i => $rec) {
161 80
            if (\call_user_func($predicate, $rec, $i)) {
162
                return true;
163 80
            }
164
        }
165
166
        return false;
167 2
    }
168
169
    /**
170 2
     * @param array<int, mixed> $args
171
     */
172 2
    public function __call(string $method, array $args): bool
173 2
    {
174
        $levelNames = \implode('|', \array_map('ucfirst', \array_keys($this->levelMap)));
175
        if (\preg_match('/(.*)(' . $levelNames . ')(.*)/', $method, $matches) > 0) {
176
            $genericMethod = $matches[1] . ($matches[3] !== 'Records' ? 'Record' : '') . $matches[3];
177
            $callable      = [$this, $genericMethod];
178
            $level         = $this->levelMap[\strtolower($matches[2])];
179
            if (\is_callable($callable)) {
180
                $args[] = $level;
181
182
                return \call_user_func_array($callable, $args);
183
            }
184
        }
185
186
        throw new \BadMethodCallException('Call to undefined method ' . static::class . '::' . $method . '()');
187
    }
188
189
    public function reset(): void
190
    {
191
        $this->records        = [];
192
        $this->recordsByLevel = [];
193
    }
194
}
195