PhptTestCase   F
last analyzed

Complexity

Total Complexity 84

Size/Duplication

Total Lines 570
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 264
dl 0
loc 570
rs 2
c 0
b 0
f 0
wmc 84

22 Methods

Rating   Name   Duplication   Size   Complexity  
B parse() 0 57 8
A assertPhptExpectation() 0 25 5
A getActualOutput() 0 3 1
A stringifyIni() 0 17 4
A getCoverageFiles() 0 8 1
A parseExternal() 0 28 5
A cleanupForCoverage() 0 14 3
B parseIniSection() 0 29 7
B validate() 0 36 7
A toString() 0 3 1
A renderForCoverage() 0 41 5
A runClean() 0 9 2
A parseEnvSection() 0 13 4
A __construct() 0 13 3
A getName() 0 3 1
A hasOutput() 0 3 1
A count() 0 3 1
F run() 0 91 18
A runSkip() 0 23 4
A usesDataProvider() 0 3 1
A render() 0 12 1
A getNumAssertions() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PhptTestCase 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.

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 PhptTestCase, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of PHPUnit.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace PHPUnit\Runner;
11
12
use PHPUnit\Framework\Assert;
13
use PHPUnit\Framework\AssertionFailedError;
14
use PHPUnit\Framework\IncompleteTestError;
15
use PHPUnit\Framework\SelfDescribing;
16
use PHPUnit\Framework\SkippedTestError;
17
use PHPUnit\Framework\Test;
18
use PHPUnit\Framework\TestResult;
19
use PHPUnit\Util\PHP\AbstractPhpProcess;
20
use SebastianBergmann\Timer\Timer;
21
use Text_Template;
22
use Throwable;
23
24
/**
25
 * Runner for PHPT test cases.
26
 */
27
class PhptTestCase implements SelfDescribing, Test
28
{
29
    /**
30
     * @var string[]
31
     */
32
    private const SETTINGS = [
33
        'allow_url_fopen=1',
34
        'auto_append_file=',
35
        'auto_prepend_file=',
36
        'disable_functions=',
37
        'display_errors=1',
38
        'docref_ext=.html',
39
        'docref_root=',
40
        'error_append_string=',
41
        'error_prepend_string=',
42
        'error_reporting=-1',
43
        'html_errors=0',
44
        'log_errors=0',
45
        'magic_quotes_runtime=0',
46
        'open_basedir=',
47
        'output_buffering=Off',
48
        'output_handler=',
49
        'report_memleaks=0',
50
        'report_zend_debug=0',
51
        'safe_mode=0',
52
        'xdebug.default_enable=0',
53
    ];
54
55
    /**
56
     * @var string
57
     */
58
    private $filename;
59
60
    /**
61
     * @var AbstractPhpProcess
62
     */
63
    private $phpUtil;
64
65
    /**
66
     * @var string
67
     */
68
    private $output = '';
69
70
    /**
71
     * Constructs a test case with the given filename.
72
     *
73
     * @throws Exception
74
     */
75
    public function __construct(string $filename, AbstractPhpProcess $phpUtil = null)
76
    {
77
        if (!\is_file($filename)) {
78
            throw new Exception(
79
                \sprintf(
80
                    'File "%s" does not exist.',
81
                    $filename
82
                )
83
            );
84
        }
85
86
        $this->filename = $filename;
87
        $this->phpUtil  = $phpUtil ?: AbstractPhpProcess::factory();
88
    }
89
90
    /**
91
     * Counts the number of test cases executed by run(TestResult result).
92
     */
93
    public function count(): int
94
    {
95
        return 1;
96
    }
97
98
    /**
99
     * Runs a test and collects its result in a TestResult instance.
100
     *
101
     * @throws Exception
102
     * @throws \ReflectionException
103
     * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
104
     * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
105
     * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException
106
     * @throws \SebastianBergmann\CodeCoverage\RuntimeException
107
     * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
108
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
109
     */
110
    public function run(TestResult $result = null): TestResult
111
    {
112
        if ($result === null) {
113
            $result = new TestResult;
114
        }
115
116
        try {
117
            $sections = $this->parse();
118
        } catch (Exception $e) {
119
            $result->startTest($this);
120
            $result->addFailure($this, new SkippedTestError($e->getMessage()), 0);
121
            $result->endTest($this, 0);
122
123
            return $result;
124
        }
125
126
        $code     = $this->render($sections['FILE']);
127
        $xfail    = false;
128
        $settings = $this->parseIniSection(self::SETTINGS);
129
130
        $result->startTest($this);
131
132
        if (isset($sections['INI'])) {
133
            $settings = $this->parseIniSection($sections['INI'], $settings);
134
        }
135
136
        if (isset($sections['ENV'])) {
137
            $env = $this->parseEnvSection($sections['ENV']);
138
            $this->phpUtil->setEnv($env);
139
        }
140
141
        $this->phpUtil->setUseStderrRedirection(true);
142
143
        if ($result->enforcesTimeLimit()) {
144
            $this->phpUtil->setTimeout($result->getTimeoutForLargeTests());
145
        }
146
147
        $skip = $this->runSkip($sections, $result, $settings);
148
149
        if ($skip) {
150
            return $result;
151
        }
152
153
        if (isset($sections['XFAIL'])) {
154
            $xfail = \trim($sections['XFAIL']);
155
        }
156
157
        if (isset($sections['STDIN'])) {
158
            $this->phpUtil->setStdin($sections['STDIN']);
159
        }
160
161
        if (isset($sections['ARGS'])) {
162
            $this->phpUtil->setArgs($sections['ARGS']);
163
        }
164
165
        if ($result->getCollectCodeCoverageInformation()) {
166
            $this->renderForCoverage($code);
167
        }
168
169
        Timer::start();
170
171
        $jobResult    = $this->phpUtil->runJob($code, $this->stringifyIni($settings));
172
        $time         = Timer::stop();
173
        $this->output = $jobResult['stdout'] ?? '';
174
175
        if ($result->getCollectCodeCoverageInformation() && ($coverage = $this->cleanupForCoverage())) {
176
            $result->getCodeCoverage()->append($coverage, $this, true, [], [], true);
177
        }
178
179
        try {
180
            $this->assertPhptExpectation($sections, $this->output);
181
        } catch (AssertionFailedError $e) {
182
            $failure = $e;
183
184
            if ($xfail !== false) {
185
                $failure = new IncompleteTestError($xfail, 0, $e);
186
            }
187
            $result->addFailure($this, $failure, $time);
188
        } catch (Throwable $t) {
189
            $result->addError($this, $t, $time);
190
        }
191
192
        if ($result->allCompletelyImplemented() && $xfail !== false) {
193
            $result->addFailure($this, new IncompleteTestError('XFAIL section but test passes'), $time);
194
        }
195
196
        $this->runClean($sections);
197
198
        $result->endTest($this, $time);
199
200
        return $result;
201
    }
202
203
    /**
204
     * Returns the name of the test case.
205
     */
206
    public function getName(): string
207
    {
208
        return $this->toString();
209
    }
210
211
    /**
212
     * Returns a string representation of the test case.
213
     */
214
    public function toString(): string
215
    {
216
        return $this->filename;
217
    }
218
219
    public function usesDataProvider(): bool
220
    {
221
        return false;
222
    }
223
224
    public function getNumAssertions(): int
225
    {
226
        return 1;
227
    }
228
229
    public function getActualOutput(): string
230
    {
231
        return $this->output;
232
    }
233
234
    public function hasOutput(): bool
235
    {
236
        return !empty($this->output);
237
    }
238
239
    /**
240
     * Parse --INI-- section key value pairs and return as array.
241
     *
242
     * @param array|string
243
     */
244
    private function parseIniSection($content, $ini = []): array
245
    {
246
        if (\is_string($content)) {
247
            $content = \explode("\n", \trim($content));
248
        }
249
250
        foreach ($content as $setting) {
251
            if (\strpos($setting, '=') === false) {
252
                continue;
253
            }
254
255
            $setting = \explode('=', $setting, 2);
256
            $name    = \trim($setting[0]);
257
            $value   = \trim($setting[1]);
258
259
            if ($name === 'extension' || $name === 'zend_extension') {
260
                if (!isset($ini[$name])) {
261
                    $ini[$name] = [];
262
                }
263
264
                $ini[$name][] = $value;
265
266
                continue;
267
            }
268
269
            $ini[$name] = $value;
270
        }
271
272
        return $ini;
273
    }
274
275
    private function parseEnvSection(string $content): array
276
    {
277
        $env = [];
278
279
        foreach (\explode("\n", \trim($content)) as $e) {
280
            $e = \explode('=', \trim($e), 2);
281
282
            if (!empty($e[0]) && isset($e[1])) {
283
                $env[$e[0]] = $e[1];
284
            }
285
        }
286
287
        return $env;
288
    }
289
290
    /**
291
     * @throws Exception
292
     */
293
    private function assertPhptExpectation(array $sections, string $output): void
294
    {
295
        $assertions = [
296
            'EXPECT'      => 'assertEquals',
297
            'EXPECTF'     => 'assertStringMatchesFormat',
298
            'EXPECTREGEX' => 'assertRegExp',
299
        ];
300
301
        $actual = \preg_replace('/\r\n/', "\n", \trim($output));
302
303
        foreach ($assertions as $sectionName => $sectionAssertion) {
304
            if (isset($sections[$sectionName])) {
305
                $sectionContent = \preg_replace('/\r\n/', "\n", \trim($sections[$sectionName]));
306
                $expected       = $sectionName === 'EXPECTREGEX' ? "/{$sectionContent}/" : $sectionContent;
307
308
                if ($expected === null) {
309
                    throw new Exception('No PHPT expectation found');
310
                }
311
                Assert::$sectionAssertion($expected, $actual);
312
313
                return;
314
            }
315
        }
316
317
        throw new Exception('No PHPT assertion found');
318
    }
319
320
    /**
321
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
322
     */
323
    private function runSkip(array &$sections, TestResult $result, array $settings): bool
324
    {
325
        if (!isset($sections['SKIPIF'])) {
326
            return false;
327
        }
328
329
        $skipif    = $this->render($sections['SKIPIF']);
330
        $jobResult = $this->phpUtil->runJob($skipif, $this->stringifyIni($settings));
331
332
        if (!\strncasecmp('skip', \ltrim($jobResult['stdout']), 4)) {
333
            $message = '';
334
335
            if (\preg_match('/^\s*skip\s*(.+)\s*/i', $jobResult['stdout'], $skipMatch)) {
336
                $message = \substr($skipMatch[1], 2);
337
            }
338
339
            $result->addFailure($this, new SkippedTestError($message), 0);
340
            $result->endTest($this, 0);
341
342
            return true;
343
        }
344
345
        return false;
346
    }
347
348
    private function runClean(array &$sections): void
349
    {
350
        $this->phpUtil->setStdin('');
351
        $this->phpUtil->setArgs('');
352
353
        if (isset($sections['CLEAN'])) {
354
            $cleanCode = $this->render($sections['CLEAN']);
355
356
            $this->phpUtil->runJob($cleanCode, self::SETTINGS);
357
        }
358
    }
359
360
    /**
361
     * @throws Exception
362
     */
363
    private function parse(): array
364
    {
365
        $sections = [];
366
        $section  = '';
367
368
        $unsupportedSections = [
369
            'CGI',
370
            'COOKIE',
371
            'DEFLATE_POST',
372
            'EXPECTHEADERS',
373
            'EXTENSIONS',
374
            'GET',
375
            'GZIP_POST',
376
            'HEADERS',
377
            'PHPDBG',
378
            'POST',
379
            'POST_RAW',
380
            'PUT',
381
            'REDIRECTTEST',
382
            'REQUEST',
383
        ];
384
385
        foreach (\file($this->filename) as $line) {
386
            if (\preg_match('/^--([_A-Z]+)--/', $line, $result)) {
387
                $section            = $result[1];
388
                $sections[$section] = '';
389
390
                continue;
391
            }
392
393
            if (empty($section)) {
394
                throw new Exception('Invalid PHPT file: empty section header');
395
            }
396
397
            $sections[$section] .= $line;
398
        }
399
400
        if (isset($sections['FILEEOF'])) {
401
            $sections['FILE'] = \rtrim($sections['FILEEOF'], "\r\n");
402
            unset($sections['FILEEOF']);
403
        }
404
405
        $this->parseExternal($sections);
406
407
        if (!$this->validate($sections)) {
408
            throw new Exception('Invalid PHPT file');
409
        }
410
411
        foreach ($unsupportedSections as $section) {
412
            if (isset($sections[$section])) {
413
                throw new Exception(
414
                    "PHPUnit does not support PHPT $section sections"
415
                );
416
            }
417
        }
418
419
        return $sections;
420
    }
421
422
    /**
423
     * @throws Exception
424
     */
425
    private function parseExternal(array &$sections): void
426
    {
427
        $allowSections = [
428
            'FILE',
429
            'EXPECT',
430
            'EXPECTF',
431
            'EXPECTREGEX',
432
        ];
433
        $testDirectory = \dirname($this->filename) . \DIRECTORY_SEPARATOR;
434
435
        foreach ($allowSections as $section) {
436
            if (isset($sections[$section . '_EXTERNAL'])) {
437
                $externalFilename = \trim($sections[$section . '_EXTERNAL']);
438
439
                if (!\is_file($testDirectory . $externalFilename) ||
440
                    !\is_readable($testDirectory . $externalFilename)) {
441
                    throw new Exception(
442
                        \sprintf(
443
                            'Could not load --%s-- %s for PHPT file',
444
                            $section . '_EXTERNAL',
445
                            $testDirectory . $externalFilename
446
                        )
447
                    );
448
                }
449
450
                $sections[$section] = \file_get_contents($testDirectory . $externalFilename);
451
452
                unset($sections[$section . '_EXTERNAL']);
453
            }
454
        }
455
    }
456
457
    private function validate(array &$sections): bool
458
    {
459
        $requiredSections = [
460
            'FILE',
461
            [
462
                'EXPECT',
463
                'EXPECTF',
464
                'EXPECTREGEX',
465
            ],
466
        ];
467
468
        foreach ($requiredSections as $section) {
469
            if (\is_array($section)) {
470
                $foundSection = false;
471
472
                foreach ($section as $anySection) {
473
                    if (isset($sections[$anySection])) {
474
                        $foundSection = true;
475
476
                        break;
477
                    }
478
                }
479
480
                if (!$foundSection) {
481
                    return false;
482
                }
483
484
                continue;
485
            }
486
487
            if (!isset($sections[$section])) {
488
                return false;
489
            }
490
        }
491
492
        return true;
493
    }
494
495
    private function render(string $code): string
496
    {
497
        return \str_replace(
498
            [
499
                '__DIR__',
500
                '__FILE__',
501
            ],
502
            [
503
                "'" . \dirname($this->filename) . "'",
504
                "'" . $this->filename . "'",
505
            ],
506
            $code
507
        );
508
    }
509
510
    private function getCoverageFiles(): array
511
    {
512
        $baseDir  = \dirname(\realpath($this->filename)) . \DIRECTORY_SEPARATOR;
513
        $basename = \basename($this->filename, 'phpt');
514
515
        return [
516
            'coverage' => $baseDir . $basename . 'coverage',
517
            'job'      => $baseDir . $basename . 'php',
518
        ];
519
    }
520
521
    private function renderForCoverage(string &$job): void
522
    {
523
        $files = $this->getCoverageFiles();
524
525
        $template = new Text_Template(
526
            __DIR__ . '/../Util/PHP/Template/PhptTestCase.tpl'
527
        );
528
529
        $composerAutoload = '\'\'';
530
531
        if (\defined('PHPUNIT_COMPOSER_INSTALL') && !\defined('PHPUNIT_TESTSUITE')) {
532
            $composerAutoload = \var_export(PHPUNIT_COMPOSER_INSTALL, true);
0 ignored issues
show
Bug introduced by
The constant PHPUnit\Runner\PHPUNIT_COMPOSER_INSTALL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
533
        }
534
535
        $phar = '\'\'';
536
537
        if (\defined('__PHPUNIT_PHAR__')) {
538
            $phar = \var_export(__PHPUNIT_PHAR__, true);
0 ignored issues
show
Bug introduced by
The constant __PHPUNIT_PHAR__ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
539
        }
540
541
        $globals = '';
542
543
        if (!empty($GLOBALS['__PHPUNIT_BOOTSTRAP'])) {
544
            $globals = '$GLOBALS[\'__PHPUNIT_BOOTSTRAP\'] = ' . \var_export(
545
                $GLOBALS['__PHPUNIT_BOOTSTRAP'],
546
                true
547
            ) . ";\n";
548
        }
549
550
        $template->setVar(
551
            [
552
                'composerAutoload' => $composerAutoload,
553
                'phar'             => $phar,
554
                'globals'          => $globals,
555
                'job'              => $files['job'],
556
                'coverageFile'     => $files['coverage'],
557
            ]
558
        );
559
560
        \file_put_contents($files['job'], $job);
561
        $job = $template->render();
562
    }
563
564
    private function cleanupForCoverage(): array
565
    {
566
        $files    = $this->getCoverageFiles();
567
        $coverage = @\unserialize(\file_get_contents($files['coverage']));
568
569
        if ($coverage === false) {
570
            $coverage = [];
571
        }
572
573
        foreach ($files as $file) {
574
            @\unlink($file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

574
            /** @scrutinizer ignore-unhandled */ @\unlink($file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
575
        }
576
577
        return $coverage;
578
    }
579
580
    private function stringifyIni(array $ini): array
581
    {
582
        $settings = [];
583
584
        foreach ($ini as $key => $value) {
585
            if (\is_array($value)) {
586
                foreach ($value as $val) {
587
                    $settings[] = $key . '=' . $val;
588
                }
589
590
                continue;
591
            }
592
593
            $settings[] = $key . '=' . $value;
594
        }
595
596
        return $settings;
597
    }
598
}
599