Passed
Push — master ( 902a34...c826a7 )
by Kyle
53s queued 11s
created

src/test/php/PHPMD/AbstractTest.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This file is part of PHP Mess Detector.
4
 *
5
 * Copyright (c) Manuel Pichler <[email protected]>.
6
 * All rights reserved.
7
 *
8
 * Licensed under BSD License
9
 * For full copyright and license information, please see the LICENSE file.
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @author Manuel Pichler <[email protected]>
13
 * @copyright Manuel Pichler. All rights reserved.
14
 * @license https://opensource.org/licenses/bsd-license.php BSD License
15
 * @link http://phpmd.org/
16
 */
17
18
namespace PHPMD;
19
20
use ErrorException;
21
use Iterator;
22
use PDepend\Source\AST\ASTClass;
23
use PDepend\Source\AST\ASTFunction;
24
use PDepend\Source\AST\ASTMethod;
25
use PDepend\Source\AST\ASTNamespace;
26
use PDepend\Source\Language\PHP\PHPBuilder;
27
use PDepend\Source\Language\PHP\PHPParserGeneric;
28
use PDepend\Source\Language\PHP\PHPTokenizerInternal;
29
use PDepend\Util\Cache\Driver\MemoryCacheDriver;
30
use PHPMD\Node\ClassNode;
31
use PHPMD\Node\FunctionNode;
32
use PHPMD\Node\InterfaceNode;
33
use PHPMD\Node\MethodNode;
34
use PHPMD\Node\TraitNode;
35
use PHPMD\Rule\Design\TooManyFields;
36
use PHPMD\Stubs\RuleStub;
37
use PHPUnit_Framework_ExpectationFailedException;
38
use PHPUnit_Framework_MockObject_MockBuilder;
39
use PHPUnit_Framework_MockObject_MockObject;
40
use ReflectionProperty;
41
use Traversable;
42
43
/**
44
 * Abstract base class for PHPMD test cases.
45
 */
46
abstract class AbstractTest extends AbstractStaticTest
47
{
48
    /** @var int At least one violation is expected */
49
    const AL_LEAST_ONE_VIOLATION = -1;
50
51
    /** @var int No violation is expected */
52
    const NO_VIOLATION = 0;
53
54
    /** @var int One violation is expected */
55
    const ONE_VIOLATION = 1;
56
57
    /**
58
     * Get a list of files that should trigger a rule violation.
59
     *
60
     * By default, files named like "testRuleAppliesTo*", but it can be overridden in sub-classes.
61
     *
62
     * @return string[]
63
     */
64
    public function getApplyingFiles()
65
    {
66
        return $this->getFilesForCalledClass('testRuleAppliesTo*');
67
    }
68
69
    /**
70
     * Get a list of files that should not trigger a rule violation.
71
     *
72
     * By default, files named like "testRuleDoesNotApplyTo*", but it can be overridden in sub-classes.
73
     *
74
     * @return string[]
75
     */
76
    public function getNotApplyingFiles()
77
    {
78
        return $this->getFilesForCalledClass('testRuleDoesNotApplyTo*');
79
    }
80
81
    /**
82
     * Get a list of test files specified by getApplyingFiles() as an array of 1-length arguments lists.
83
     *
84
     * @return string[][]
85
     */
86
    public function getApplyingCases()
87
    {
88
        return static::getValuesAsArrays($this->getApplyingFiles());
89
    }
90
91
    /**
92
     * Get a list of test files specified by getNotApplyingFiles() as an array of 1-length arguments lists.
93
     *
94
     * @return string[][]
95
     */
96
    public function getNotApplyingCases()
97
    {
98
        return static::getValuesAsArrays($this->getNotApplyingFiles());
99
    }
100
101
    /**
102
     * Resets a changed working directory.
103
     *
104
     * @return void
105
     */
106
    protected function tearDown()
107
    {
108
        static::returnToOriginalWorkingDirectory();
109
        static::cleanupTempFiles();
110
111
        parent::tearDown();
112
    }
113
114
    /**
115
     * Returns the first class found in a source file related to the calling
116
     * test method.
117
     *
118
     * @return ClassNode
119
     */
120
    protected function getClass()
121
    {
122
        return new ClassNode(
123
            $this->getNodeForCallingTestCase(
124
                $this->parseTestCaseSource()->getClasses()
125
            )
126
        );
127
    }
128
129
    /**
130
     * Returns the first interface found in a source file related to the calling
131
     * test method.
132
     *
133
     * @return InterfaceNode
134
     */
135
    protected function getInterface()
136
    {
137
        return new InterfaceNode(
138
            $this->getNodeForCallingTestCase(
139
                $this->parseTestCaseSource()->getInterfaces()
140
            )
141
        );
142
    }
143
144
    /**
145
     * @return TraitNode
146
     */
147
    protected function getTrait()
148
    {
149
        return new TraitNode(
150
            $this->getNodeForCallingTestCase(
151
                $this->parseTestCaseSource()->getTraits()
152
            )
153
        );
154
    }
155
156
    /**
157
     * Returns the first method found in a source file related to the calling
158
     * test method.
159
     *
160
     * @return MethodNode
161
     */
162
    protected function getMethod()
163
    {
164
        return new MethodNode(
165
            $this->getNodeForCallingTestCase(
166
                $this->parseTestCaseSource()
167
                    ->getTypes()
168
                    ->current()
169
                    ->getMethods()
170
            )
171
        );
172
    }
173
174
    /**
175
     * Returns the first function found in a source files related to the calling
176
     * test method.
177
     *
178
     * @return FunctionNode
179
     */
180
    protected function getFunction()
181
    {
182
        return new FunctionNode(
183
            $this->getNodeForCallingTestCase(
184
                $this->parseTestCaseSource()->getFunctions()
185
            )
186
        );
187
    }
188
189
    /**
190
     * Returns the first class found for a given test file.
191
     *
192
     * @return ClassNode
193
     */
194
    protected function getClassNodeForTestFile($file)
195
    {
196
        return new ClassNode(
197
            $this->parseSource($file)
198
                ->getTypes()
199
                ->current()
200
        );
201
    }
202
203
    /**
204
     * Returns the first method or function node for a given test file.
205
     *
206
     * @param string $file
207
     * @return MethodNode|FunctionNode
208
     * @since 2.8.3
209
     */
210
    protected function getNodeForTestFile($file)
211
    {
212
        $source = $this->parseSource($file);
213
        $class = $source
214
            ->getTypes()
215
            ->current();
216
        $nodeClassName = 'PHPMD\\Node\\FunctionNode';
217
        $getter = 'getFunctions';
218
219
        if ($class) {
220
            $source = $class;
221
            $nodeClassName = 'PHPMD\\Node\\MethodNode';
222
            $getter = 'getMethods';
223
        }
224
225
        return new $nodeClassName(
226
            $this->getNodeByName(
227
                $source->$getter(),
228
                pathinfo($file, PATHINFO_FILENAME)
229
            )
230
        );
231
    }
232
233
    /**
234
     * Assert that a given file trigger N times the given rule.
235
     *
236
     * Rethrows the PHPUnit ExpectationFailedException with the base name
237
     * of the file for better readability.
238
     *
239
     * @param Rule $rule Rule to test.
240
     * @param int $expectedInvokes Count of expected invocations.
241
     * @param string $file Test file containing a method with the same name to be tested.
242
     * @return void
243
     * @throws PHPUnit_Framework_ExpectationFailedException
244
     */
245
    protected function expectRuleHasViolationsForFile(Rule $rule, $expectedInvokes, $file)
246
    {
247
        $report = new Report();
248
        $rule->setReport($report);
249
        $rule->apply($this->getNodeForTestFile($file));
250
        $violations = $report->getRuleViolations();
251
        $actualInvokes = count($violations);
252
        $assertion = $expectedInvokes === self::AL_LEAST_ONE_VIOLATION
253
            ? $actualInvokes > 0
254
            : $actualInvokes === $expectedInvokes;
255
256
        if (!$assertion) {
257
            throw new PHPUnit_Framework_ExpectationFailedException(
258
                $this->getViolationFailureMessage($file, $expectedInvokes, $actualInvokes, $violations)
259
            );
260
        }
261
262
        $this->assertTrue($assertion);
263
    }
264
265
    /**
266
     * Return a human-friendly failure message for a given list of violations and the actual/expected counts.
267
     *
268
     * @param string $file
269
     * @param int $expectedInvokes
270
     * @param int $actualInvokes
271
     * @param array|iterable|Traversable $violations
272
     *
273
     * @return string
274
     */
275
    protected function getViolationFailureMessage($file, $expectedInvokes, $actualInvokes, $violations)
276
    {
277
        return basename($file)." failed:\n".
278
            "Expected $expectedInvokes violation".($expectedInvokes !== 1 ? 's' : '')."\n".
279
            "But $actualInvokes violation".($actualInvokes !== 1 ? 's' : '')." raised".
280
            ($actualInvokes > 0
281
                ? ":\n".$this->getViolationsSummary($violations)
282
                : '.'
283
            );
284
    }
285
286
    /**
287
     * Return a human-friendly summary for a list of violations.
288
     *
289
     * @param array|iterable|Traversable $violations
290
     * @return string
291
     */
292
    protected function getViolationsSummary($violations)
293
    {
294
        if (!is_array($violations)) {
295
            $violations = iterator_to_array($violations);
296
        }
297
298
        return implode("\n", array_map(function (RuleViolation $violation) {
299
            $nodeExtractor = new ReflectionProperty('PHPMD\\RuleViolation', 'node');
300
            $nodeExtractor->setAccessible(true);
301
            $node = $nodeExtractor->getValue($violation);
302
            $node = $node ? $node->getNode() : null;
303
            $message = '  - line '.$violation->getBeginLine();
304
305
            if ($node) {
306
                $type = preg_replace('/^PDepend\\\\Source\\\\AST\\\\AST/', '', get_class($node));
307
                $message .= ' on '.$type.' '.$node->getImage();
308
            }
309
310
            return $message;
311
        }, $violations));
312
    }
313
314
    /**
315
     * Returns the absolute path for a test resource for the current test.
316
     *
317
     * @return string
318
     * @since 1.1.0
319
     */
320
    protected static function createCodeResourceUriForTest()
321
    {
322
        $frame = static::getCallingTestCase();
323
324
        return self::createResourceUriForTest($frame['function'] . '.php');
325
    }
326
327
    /**
328
     * Returns the absolute path for a test resource for the current test.
329
     *
330
     * @param string $localPath The local/relative file location
331
     * @return string
332
     * @since 1.1.0
333
     */
334
    protected static function createResourceUriForTest($localPath)
335
    {
336
        $frame = static::getCallingTestCase();
337
338
        return static::getResourceFilePathFromClassName($frame['class'], $localPath);
339
    }
340
341
    /**
342
     * Return URI for a given pattern with directory based on the current called class name.
343
     *
344
     * @param string $pattern
345
     * @return string
346
     */
347
    protected function createResourceUriForCalledClass($pattern)
348
    {
349
        return $this->getResourceFilePathFromClassName(get_class($this), $pattern);
350
    }
351
352
    /**
353
     * Return list of files matching a given pattern with directory based on the current called class name.
354
     *
355
     * @param string $pattern
356
     * @return string[]
357
     */
358
    protected function getFilesForCalledClass($pattern = '*')
359
    {
360
        return glob($this->createResourceUriForCalledClass($pattern));
361
    }
362
363
    /**
364
     * Creates a mocked class node instance.
365
     *
366
     * @param string $metric
367
     * @param integer $value
368
     * @return ClassNode
369
     */
370
    protected function getClassMock($metric = null, $value = null)
371
    {
372
        $class = $this->getMockFromBuilder(
373
            $this->getMockBuilder('PHPMD\\Node\\ClassNode')
374
                ->setConstructorArgs(array(new ASTClass('FooBar')))
375
        );
376
377
        if ($metric !== null) {
378
            $class->expects($this->atLeastOnce())
379
                ->method('getMetric')
380
                ->with($this->equalTo($metric))
381
                ->willReturn($value);
382
        }
383
384
        return $class;
385
    }
386
387
    /**
388
     * Creates a mocked method node instance.
389
     *
390
     * @param string $metric
391
     * @param integer $value
392
     * @return MethodNode
393
     */
394
    protected function getMethodMock($metric = null, $value = null)
395
    {
396
        return $this->createFunctionOrMethodMock('PHPMD\\Node\\MethodNode', new ASTMethod('fooBar'), $metric, $value);
397
    }
398
399
    /**
400
     * Creates a mocked function node instance.
401
     *
402
     * @param string $metric The metric acronym used by PHP_Depend.
403
     * @param mixed $value The expected metric return value.
404
     * @return FunctionNode
405
     */
406
    protected function createFunctionMock($metric = null, $value = null)
407
    {
408
        return $this->createFunctionOrMethodMock(
409
            'PHPMD\\Node\\FunctionNode',
410
            new ASTFunction('fooBar'),
411
            $metric,
412
            $value
413
        );
414
    }
415
416
    /**
417
     * Initializes the getMetric() method of the given function or method node.
418
     *
419
     * @param FunctionNode|MethodNode|PHPUnit_Framework_MockObject_MockObject $mock
420
     * @param string $metric
421
     * @param mixed $value
422
     * @return FunctionNode|MethodNode
423
     */
424
    protected function initFunctionOrMethod($mock, $metric, $value)
425
    {
426
        if ($metric === null) {
427
            return $mock;
428
        }
429
430
        $mock->expects($this->atLeastOnce())
431
            ->method('getMetric')
432
            ->with($this->equalTo($metric))
433
            ->willReturn($value);
434
435
        return $mock;
436
    }
437
438
    /**
439
     * Creates a mocked report instance.
440
     *
441
     * @param integer $expectedInvokes Number of expected invokes.
442
     * @return Report|PHPUnit_Framework_MockObject_MockObject
443
     */
444
    protected function getReportMock($expectedInvokes = -1)
445
    {
446
        if ($expectedInvokes === self::AL_LEAST_ONE_VIOLATION) {
447
            $expects = $this->atLeastOnce();
448
        } elseif ($expectedInvokes === self::NO_VIOLATION) {
449
            $expects = $this->never();
450
        } elseif ($expectedInvokes === self::ONE_VIOLATION) {
451
            $expects = $this->once();
452
        } else {
453
            $expects = $this->exactly($expectedInvokes);
454
        }
455
456
        $report = $this->getMockFromBuilder($this->getMockBuilder('PHPMD\\Report'));
457
        $report->expects($expects)
458
            ->method('addRuleViolation');
459
460
        return $report;
461
    }
462
463
    /**
464
     * Get a mocked report with one violation
465
     *
466
     * @return Report
467
     */
468
    public function getReportWithOneViolation()
469
    {
470
        return $this->getReportMock(self::ONE_VIOLATION);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getReportMock(self::ONE_VIOLATION); of type PHPMD\Report|PHPUnit_Fra...k_MockObject_MockObject adds the type PHPUnit_Framework_MockObject_MockObject to the return on line 470 which is incompatible with the return type documented by PHPMD\AbstractTest::getReportWithOneViolation of type PHPMD\Report.
Loading history...
471
    }
472
473
    /**
474
     * Get a mocked report with no violation
475
     *
476
     * @return Report
477
     */
478
    public function getReportWithNoViolation()
479
    {
480
        return $this->getReportMock(self::NO_VIOLATION);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getReportMock(self::NO_VIOLATION); of type PHPMD\Report|PHPUnit_Fra...k_MockObject_MockObject adds the type PHPUnit_Framework_MockObject_MockObject to the return on line 480 which is incompatible with the return type documented by PHPMD\AbstractTest::getReportWithNoViolation of type PHPMD\Report.
Loading history...
481
    }
482
483
    /**
484
     * Get a mocked report with at least one violation
485
     *
486
     * @return Report
487
     */
488
    public function getReportWithAtLeastOneViolation()
489
    {
490
        return $this->getReportMock(self::AL_LEAST_ONE_VIOLATION);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getReportMock(sel...L_LEAST_ONE_VIOLATION); of type PHPMD\Report|PHPUnit_Fra...k_MockObject_MockObject adds the type PHPUnit_Framework_MockObject_MockObject to the return on line 490 which is incompatible with the return type documented by PHPMD\AbstractTest::getR...WithAtLeastOneViolation of type PHPMD\Report.
Loading history...
491
    }
492
493
    protected function getMockFromBuilder(PHPUnit_Framework_MockObject_MockBuilder $builder)
494
    {
495
        if (version_compare(PHP_VERSION, '7.4.0-dev', '<')) {
496
            return $builder->getMock();
497
        }
498
499
        return @$builder->getMock();
500
    }
501
502
    /**
503
     * Creates a mocked {@link \PHPMD\AbstractRule} instance.
504
     *
505
     * @return AbstractRule|PHPUnit_Framework_MockObject_MockObject
506
     */
507
    protected function getRuleMock()
508
    {
509
        if (version_compare(PHP_VERSION, '7.4.0-dev', '<')) {
510
            return $this->getMockForAbstractClass('PHPMD\\AbstractRule');
511
        }
512
513
        return @$this->getMockForAbstractClass('PHPMD\\AbstractRule');
514
    }
515
516
    /**
517
     * Creates a mocked rule-set instance.
518
     *
519
     * @param string $expectedClass Optional class name for apply() expected at least once.
520
     * @param int|string $count How often should apply() be called?
521
     * @return RuleSet|PHPUnit_Framework_MockObject_MockObject
522
     */
523
    protected function getRuleSetMock($expectedClass = null, $count = '*')
524
    {
525
        $ruleSet = $this->getMockFromBuilder($this->getMockBuilder('PHPMD\RuleSet'));
526
        if ($expectedClass === null) {
527
            $ruleSet->expects($this->never())->method('apply');
528
529
            return $ruleSet;
530
        }
531
532
        if ($count === '*') {
533
            $count = $this->atLeastOnce();
534
        } else {
535
            $count = $this->exactly($count);
536
        }
537
538
        $ruleSet->expects($count)
539
            ->method('apply')
540
            ->with($this->isInstanceOf($expectedClass));
541
542
        return $ruleSet;
543
    }
544
545
    /**
546
     * Creates a mocked rule violation instance.
547
     *
548
     * @param string $fileName The filename to use.
549
     * @param integer $beginLine The begin of violation line number to use.
550
     * @param integer $endLine The end of violation line number to use.
551
     * @param null|object $rule The rule object to use.
552
     * @param null|string $description The violation description to use.
553
     * @return PHPUnit_Framework_MockObject_MockObject
554
     */
555
    protected function getRuleViolationMock(
556
        $fileName = '/foo/bar.php',
557
        $beginLine = 23,
558
        $endLine = 42,
559
        $rule = null,
560
        $description = null
561
    ) {
562
        $ruleViolation = $this->getMockFromBuilder(
563
            $this->getMockBuilder('PHPMD\\RuleViolation')
564
                ->setConstructorArgs(array(new TooManyFields(), new FunctionNode(new ASTFunction('fooBar')), 'Hello'))
565
        );
566
567
        if ($rule === null) {
568
            $rule = new RuleStub();
569
        }
570
571
        if ($description === null) {
572
            $description = 'Test description';
573
        }
574
575
        $ruleViolation
576
            ->method('getRule')
577
            ->willReturn($rule);
578
        $ruleViolation
579
            ->method('getFileName')
580
            ->willReturn($fileName);
581
        $ruleViolation
582
            ->method('getBeginLine')
583
            ->willReturn($beginLine);
584
        $ruleViolation
585
            ->method('getEndLine')
586
            ->willReturn($endLine);
587
        $ruleViolation
588
            ->method('getNamespaceName')
589
            ->willReturn('TestStubPackage');
590
        $ruleViolation
591
            ->method('getDescription')
592
            ->willReturn($description);
593
594
        return $ruleViolation;
595
    }
596
597
    /**
598
     * Creates a mocked rul violation instance.
599
     *
600
     * @param string $file
601
     * @param string $message
602
     * @return ProcessingError|PHPUnit_Framework_MockObject_MockObject
603
     */
604
    protected function getErrorMock(
605
        $file = '/foo/baz.php',
606
        $message = 'Error in file "/foo/baz.php"'
607
    ) {
608
609
        $processingError = $this->getMockFromBuilder(
610
            $this->getMockBuilder('PHPMD\\ProcessingError')
611
                ->setConstructorArgs(array(null))
612
                ->setMethods(array('getFile', 'getMessage'))
613
        );
614
615
        $processingError
616
            ->method('getFile')
617
            ->willReturn($file);
618
        $processingError
619
            ->method('getMessage')
620
            ->willReturn($message);
621
622
        return $processingError;
623
    }
624
625
    /**
626
     * Parses the source code for the calling test method and returns the first
627
     * package node found in the parsed file.
628
     *
629
     * @return ASTNamespace
630
     */
631
    private function parseTestCaseSource()
632
    {
633
        return $this->parseSource($this->createCodeResourceUriForTest());
634
    }
635
636
    /**
637
     * @param string $mockBuilder
638
     * @param ASTFunction|ASTMethod $mock
639
     * @param string $metric The metric acronym used by PHP_Depend.
640
     * @param mixed $value The expected metric return value.
641
     * @return FunctionNode|MethodNode
642
     */
643
    private function createFunctionOrMethodMock($mockBuilder, $mock, $metric = null, $value = null)
644
    {
645
        return $this->initFunctionOrMethod(
646
            $this->getMockFromBuilder(
647
                $this->getMockBuilder($mockBuilder)
648
                    ->setConstructorArgs(array($mock))
649
            ),
650
            $metric,
651
            $value
652
        );
653
    }
654
655
    /**
656
     * Returns the PHP_Depend node having the given name.
657
     *
658
     * @param Iterator $nodes
659
     * @return PHP_Depend_Code_AbstractItem
660
     * @throws ErrorException
661
     */
662
    private function getNodeByName(Iterator $nodes, $name)
663
    {
664
        foreach ($nodes as $node) {
665
            if ($node->getName() === $name) {
666
                return $node;
667
            }
668
        }
669
        throw new ErrorException("Cannot locate node named $name.");
670
    }
671
672
    /**
673
     * Returns the PHP_Depend node for the calling test case.
674
     *
675
     * @param Iterator $nodes
676
     * @return PHP_Depend_Code_AbstractItem
677
     * @throws ErrorException
678
     */
679
    private function getNodeForCallingTestCase(Iterator $nodes)
680
    {
681
        $frame = $this->getCallingTestCase();
682
683
        return $this->getNodeByName($nodes, $frame['function']);
684
    }
685
686
    /**
687
     * Parses the source of the given file and returns the first package found
688
     * in that file.
689
     *
690
     * @param string $sourceFile
691
     * @return ASTNamespace
692
     * @throws ErrorException
693
     */
694
    private function parseSource($sourceFile)
695
    {
696
        if (file_exists($sourceFile) === false) {
697
            throw new ErrorException('Cannot locate source file: ' . $sourceFile);
698
        }
699
700
        $tokenizer = new PHPTokenizerInternal();
701
        $tokenizer->setSourceFile($sourceFile);
702
703
        $builder = new PHPBuilder();
704
705
        $parser = new PHPParserGeneric(
706
            $tokenizer,
707
            $builder,
708
            new MemoryCacheDriver()
709
        );
710
        $parser->parse();
711
712
        return $builder->getNamespaces()->current();
713
    }
714
}
715