TapParser   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 97%

Importance

Changes 6
Bugs 5 Features 1
Metric Value
wmc 43
c 6
b 5
f 1
lcom 1
cbo 1
dl 0
loc 273
ccs 97
cts 100
cp 0.97
rs 8.3157

11 Methods

Rating   Name   Duplication   Size   Complexity  
B testLine() 0 15 5
A yamlLine() 0 16 3
A getTotalFailures() 0 4 1
A __construct() 0 4 1
C parse() 0 28 7
B findTapLog() 0 16 5
A nextLine() 0 8 2
C parseLine() 0 22 7
A processTestLine() 0 20 4
A processYamlBlock() 0 20 4
A processDirective() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like TapParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TapParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PHPCI\Plugin\Util;
4
5
use Exception;
6
use PHPCI\Helper\Lang;
7
use Symfony\Component\Yaml\Yaml;
8
9
/**
10
 * Processes TAP format strings into usable test result data.
11
 */
12
class TapParser
13
{
14
    const TEST_COUNTS_PATTERN = '/^\d+\.\.(\d+)/';
15
    const TEST_LINE_PATTERN = '/^(ok|not ok)(?:\s+\d+)?(?:\s+\-)?\s*(.*?)(?:\s*#\s*(skip|todo)\s*(.*))?\s*$/i';
16
    const TEST_YAML_START = '/^(\s*)---/';
17
    const TEST_DIAGNOSTIC = '/^#/';
18
    const TEST_COVERAGE = '/^Generating/';
19
20
    /**
21
     * @var string
22
     */
23
    protected $tapString;
24
25
    /**
26
     * @var int
27
     */
28
    protected $failures = 0;
29
30
    /**
31
     * @var array
32
     */
33
    protected $lines;
34
35
    /**
36
     * @var int
37
     */
38
    protected $lineNumber;
39
40
    /**
41
     * @var int
42
     */
43
    protected $testCount;
44
45
    /**
46
     * @var array
47
     */
48
    protected $results;
49
50
    /**
51
     * Create a new TAP parser for a given string.
52
     *
53
     * @param string $tapString The TAP format string to be parsed.
54
     */
55 14
    public function __construct($tapString)
56
    {
57 14
        $this->tapString = trim($tapString);
58 14
    }
59
60
    /**
61
     * Parse a given TAP format string and return an array of tests and their status.
62
     */
63 14
    public function parse()
64
    {
65
        // Split up the TAP string into an array of lines, then
66
        // trim all of the lines so there's no leading or trailing whitespace.
67 14
        $this->lines = array_map('rtrim', explode("\n", $this->tapString));
68 14
        $this->lineNumber = 0;
69
70 14
        $this->testCount = false;
0 ignored issues
show
Documentation Bug introduced by
The property $testCount was declared of type integer, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
71 14
        $this->results = array();
72
73 14
        $header = $this->findTapLog();
74
75 12
        $line = $this->nextLine();
76 12
        if ($line === $header) {
77 1
            throw new Exception('Duplicated TAP log, please check the configuration.');
78
        }
79
80 11
        while ($line !== false && ($this->testCount === false || count($this->results) < $this->testCount)) {
81 11
            $this->parseLine($line);
82 11
            $line = $this->nextLine();
83 11
        }
84
85 10
        if (false !== $this->testCount && count($this->results) !== $this->testCount) {
86 1
            throw new Exception(Lang::get('tap_error'));
87
        }
88
89 9
        return $this->results;
90
    }
91
92
    /** Looks for the start of the TAP log in the string.
93
     * @return string The TAP header line.
94
     *
95
     * @throws Exception if no TAP log is found or versions mismatch.
96
     */
97 14
    protected function findTapLog()
98
    {
99
        // Look for the beginning of the TAP output
100
        do {
101 14
            $header = $this->nextLine();
102 14
        } while ($header !== false && substr($header, 0, 12) !== 'TAP version ');
103
104
        //
105 14
        if ($header === false) {
106 2
            throw new Exception('No TAP log found, please check the configuration.');
107 12
        } elseif ($header !== 'TAP version 13') {
108
            throw new Exception(Lang::get('tap_version'));
109
        }
110
111 12
        return $header;
112
    }
113
114
    /** Fetch the next line.
115
     * @return string|false The next line or false if the end has been reached.
116
     */
117 14
    protected function nextLine()
118
    {
119 14
        if ($this->lineNumber < count($this->lines)) {
120 14
            return $this->lines[$this->lineNumber++];
121
        }
122
123 11
        return false;
124
    }
125
126
    /**
127
     * @param string $line
128
     *
129
     * @return bool
130
     */
131 10
    protected function testLine($line)
132
    {
133 10
        if (preg_match(self::TEST_LINE_PATTERN, $line, $matches)) {
134 10
            $this->results[] = $this->processTestLine(
135 10
                $matches[1],
136 10
                isset($matches[2]) ? $matches[2] : '',
137 10
                isset($matches[3]) ? $matches[3] : null,
138 10
                isset($matches[4]) ? $matches[4] : null
139 10
            );
140
141 10
            return true;
142
        }
143
144 2
        return false;
145
    }
146
147
    /**
148
     * @param string $line
149
     *
150
     * @return bool
151
     */
152 2
    protected function yamlLine($line)
153
    {
154 2
        if (preg_match(self::TEST_YAML_START, $line, $matches)) {
155 2
            $diagnostic = $this->processYamlBlock($matches[1]);
156 1
            $test = array_pop($this->results);
157 1
            if (isset($test['message'], $diagnostic['message'])) {
158 1
                $test['message'] .= PHP_EOL.$diagnostic['message'];
159 1
                unset($diagnostic['message']);
160 1
            }
161 1
            $this->results[] = array_replace($test, $diagnostic);
162
163 1
            return true;
164
        }
165
166
        return false;
167
    }
168
169
    /** Parse a single line.
170
     * @param string $line
171
     *
172
     * @throws Exception
173
     */
174 11
    protected function parseLine($line)
175
    {
176 11
        if (preg_match(self::TEST_DIAGNOSTIC, $line) || preg_match(self::TEST_COVERAGE, $line) || !$line) {
177 2
            return;
178
        }
179
180 10
        if (preg_match(self::TEST_COUNTS_PATTERN, $line, $matches)) {
181 9
            $this->testCount = intval($matches[1]);
182
183 9
            return;
184
        }
185
186 10
        if ($this->testLine($line)) {
187 10
            return;
188
        }
189
190 2
        if ($this->yamlLine($line)) {
191 1
            return;
192
        }
193
194
        throw new Exception(sprintf('Incorrect TAP data, line %d: %s', $this->lineNumber, $line));
195
    }
196
197
    /**
198
     * Process an individual test line.
199
     *
200
     * @param string $result
201
     * @param string $message
202
     * @param string $directive
203
     * @param string $reason
204
     *
205
     * @return array
206
     */
207 10
    protected function processTestLine($result, $message, $directive, $reason)
208
    {
209
        $test = array(
210 10
            'pass' => true,
211 10
            'message' => $message,
212 10
            'severity' => 'success',
213 10
        );
214
215 10
        if ($result !== 'ok') {
216 6
            $test['pass'] = false;
217 6
            $test['severity'] = substr($message, 0, 6) === 'Error:' ? 'error' : 'fail';
218 6
            ++$this->failures;
219 6
        }
220
221 10
        if ($directive) {
222 2
            $test = $this->processDirective($test, $directive, $reason);
223 2
        }
224
225 10
        return $test;
226
    }
227
228
    /** Process an indented Yaml block.
229
     * @param string $indent The block indentation to ignore.
230
     *
231
     * @return array The processed Yaml content.
232
     */
233 2
    protected function processYamlBlock($indent)
234
    {
235 2
        $startLine = $this->lineNumber + 1;
236 2
        $endLine = $indent.'...';
237 2
        $yamlLines = array();
238
239
        do {
240 2
            $line = $this->nextLine();
241
242 2
            if ($line === false) {
243 1
                throw new Exception(Lang::get('tap_error_endless_yaml', $startLine));
244 2
            } elseif ($line === $endLine) {
245 1
                break;
246
            }
247
248 2
            $yamlLines[] = substr($line, strlen($indent));
249 2
        } while (true);
250
251 1
        return Yaml::parse(implode("\n", $yamlLines));
252
    }
253
254
    /** Process a TAP directive
255
     * @param array  $test
256
     * @param string $directive
257
     * @param string $reason
258
     *
259
     * @return array
260
     */
261 2
    protected function processDirective($test, $directive, $reason)
262
    {
263 2
        $test['severity'] = strtolower($directive) === 'skip' ? 'skipped' : 'todo';
264
265 2
        if (!empty($reason)) {
266 2
            if (!empty($test['message'])) {
267 2
                $test['message'] .= ', '.$test['severity'].': ';
268 2
            }
269 2
            $test['message'] .= $reason;
270 2
        }
271
272 2
        return $test;
273
    }
274
275
    /**
276
     * Get the total number of failures from the current TAP file.
277
     *
278
     * @return int
279
     */
280 8
    public function getTotalFailures()
281
    {
282 8
        return $this->failures;
283
    }
284
}
285