• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHPCSUtils / 959033513

22 Jun 2021 - 1:09 coverage: 97.302% (-0.04%) from 97.338%
959033513

Pull #277

github

GitHub
Merge e21c525cf into 403ad2cab
Pull Request #277: Tests: use PHPUnit Polyfills

2741 of 2817 relevant lines covered (97.3%)

89.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.0
/PHPCSUtils/TestUtils/UtilityMethodTestCase.php
1
<?php
2
/**
3
 * PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4
 *
5
 * @package   PHPCSUtils
6
 * @copyright 2019-2020 PHPCSUtils Contributors
7
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
8
 * @link      https://github.com/PHPCSStandards/PHPCSUtils
9
 */
10

11
namespace PHPCSUtils\TestUtils;
12

13
use PHP_CodeSniffer\Exceptions\RuntimeException;
14
use PHP_CodeSniffer\Exceptions\TokenizerException;
15
use PHPCSUtils\BackCompat\Helper;
16
use PHPUnit\Framework\TestCase;
17
use ReflectionClass;
18
use RuntimeException as PHPRuntimeException;
19

20
/**
21
 * Base class for use when testing utility methods for PHP_CodeSniffer.
22
 *
23
 * This class is compatible with PHP_CodeSniffer 2.x as well as 3.x.
24
 *
25
 * This class is compatible with {@link http://phpunit.de/ PHPUnit} 4.5 - 9.x providing the PHPCSUtils
26
 * autoload file is included in the test bootstrap. For more information about that, please consult
27
 * the project's {@link https://github.com/PHPCSStandards/PHPCSUtils/blob/develop/README.md README}.
28
 *
29
 * To allow for testing of tab vs space content, the `tabWidth` is set to `4` by default.
30
 *
31
 * Typical usage:
32
 *
33
 * Test case file `path/to/ClassUnderTestUnitTest.inc`:
34
 * ```php
35
 * <?php
36
 *
37
 * /* testTestCaseDescription * /
38
 * const BAR = false;
39
 * ```
40
 *
41
 * Test file `path/to/ClassUnderTestUnitTest.php`:
42
 * ```php
43
 * <?php
44
 *
45
 * use PHPCSUtils\TestUtils\UtilityMethodTestCase;
46
 * use YourStandard\ClassUnderTest;
47
 *
48
 * class ClassUnderTestUnitTest extends UtilityMethodTestCase {
49
 *
50
 *     /**
51
 *      * Testing utility method MyMethod.
52
 *      *
53
 *      * @dataProvider dataMyMethod
54
 *      *
55
 *      * @covers \YourStandard\ClassUnderTest::MyMethod
56
 *      *
57
 *      * @param string $commentString The comment which prefaces the target token in the test file.
58
 *      * @param string $expected      The expected return value.
59
 *      *
60
 *      * @return void
61
 *      * /
62
 *     public function testMyMethod($commentString, $expected)
63
 *     {
64
 *         $stackPtr = $this->getTargetToken($commentString, [\T_TOKEN_CONSTANT, \T_ANOTHER_TOKEN]);
65
 *         $class    = new ClassUnderTest();
66
 *         $result   = $class->MyMethod(self::$phpcsFile, $stackPtr);
67
 *         // Or for static utility methods:
68
 *         $result   = ClassUnderTest::MyMethod(self::$phpcsFile, $stackPtr);
69
 *
70
 *         $this->assertSame($expected, $result);
71
 *     }
72
 *
73
 *     /**
74
 *      * Data Provider.
75
 *      *
76
 *      * @see ClassUnderTestUnitTest::testMyMethod() For the array format.
77
 *      *
78
 *      * @return array
79
 *      * /
80
 *     public function dataMyMethod()
81
 *     {
82
 *         return array(
83
 *             array('/* testTestCaseDescription * /', false),
84
 *         );
85
 *     }
86
 * }
87
 * ```
88
 *
89
 * Note:
90
 * - Remove the space between the comment closers `* /` for a working example.
91
 * - Each test case separator comment MUST start with `/* test`.
92
 *   This is to allow the {@see UtilityMethodTestCase::getTargetToken()} method to
93
 *   distinquish between the test separation comments and comments which may be part
94
 *   of the test case.
95
 * - The test case file and unit test file should be placed in the same directory.
96
 * - For working examples using this abstract class, have a look at the unit tests
97
 *   for the PHPCSUtils utility functions themselves.
98
 *
99
 * @since 1.0.0
100
 */
101
abstract class UtilityMethodTestCase extends TestCase
102
{
103

104
    /**
105
     * The PHPCS version the tests are being run on.
106
     *
107
     * @since 1.0.0-alpha3
108
     *
109
     * @var string
110
     */
111
    protected static $phpcsVersion = '0';
112

113
    /**
114
     * The file extension of the test case file (without leading dot).
115
     *
116
     * This allows concrete test classes to overrule the default `"inc"` with, for instance,
117
     * `"js"` or `"css"` when applicable.
118
     *
119
     * @since 1.0.0
120
     *
121
     * @var string
122
     */
123
    protected static $fileExtension = 'inc';
124

125
    /**
126
     * Full path to the test case file associated with the concrete test class.
127
     *
128
     * Optional. If left empty, the case file will be presumed to be in
129
     * the same directory and named the same as the test class, but with an
130
     * `"inc"` file extension.
131
     *
132
     * @since 1.0.0
133
     *
134
     * @var string
135
     */
136
    protected static $caseFile = '';
137

138
    /**
139
     * The tab width setting to use when tokenizing the file.
140
     *
141
     * This allows for test case files to use a different tab width than the default.
142
     *
143
     * @since 1.0.0
144
     *
145
     * @var int
146
     */
147
    protected static $tabWidth = 4;
148

149
    /**
150
     * The \PHP_CodeSniffer\Files\File object containing the parsed contents of the test case file.
151
     *
152
     * @since 1.0.0
153
     *
154
     * @var \PHP_CodeSniffer\Files\File
155
     */
156
    protected static $phpcsFile;
157

158
    /**
159
     * Set the name of a sniff to pass to PHPCS to limit the run (and force it to record errors).
160
     *
161
     * Normally, this propery won't need to be overloaded, but for utility methods which record
162
     * violations and contain fixers, setting a dummy sniff name equal to the sniff name passed
163
     * in the error code for `addError()`/`addWarning()` during the test, will allow for testing
164
     * the recording of these violations, as well as testing the fixer.
165
     *
166
     * @since 1.0.0
167
     *
168
     * @var array
169
     */
170
    protected static $selectedSniff = ['Dummy.Dummy.Dummy'];
171

172
    /**
173
     * Initialize PHPCS & tokenize the test case file.
174
     *
175
     * The test case file for a unit test class has to be in the same directory
176
     * directory and use the same file name as the test class, using the `.inc` extension
177
     * or be explicitly set using the {@see UtilityMethodTestCase::$fileExtension}/
178
     * {@see UtilityMethodTestCase::$caseFile} properties.
179
     *
180
     * Note: This is a PHPUnit cross-version compatible {@see \PHPUnit\Framework\TestCase::setUpBeforeClass()}
181
     * method.
182
     *
183
     * @since 1.0.0
184
     *
185
     * @beforeClass
186
     *
187
     * @return void
188
     */
189
    public static function setUpTestFile()
190
    {
191
        parent::setUpBeforeClass();
12×
192

193
        self::$phpcsVersion = Helper::getVersion();
12×
194

195
        $caseFile = static::$caseFile;
12×
196
        if (\is_string($caseFile) === false || $caseFile === '') {
12×
197
            $testClass = \get_called_class();
12×
198
            $testFile  = (new ReflectionClass($testClass))->getFileName();
12×
199
            $caseFile  = \substr($testFile, 0, -3) . static::$fileExtension;
12×
200
        }
201

202
        if (\is_readable($caseFile) === false) {
12×
203
            parent::fail("Test case file missing. Expected case file location: $caseFile");
4×
204
        }
205

206
        $contents = \file_get_contents($caseFile);
8×
207

208
        if (\version_compare(self::$phpcsVersion, '2.99.99', '>')) {
8×
209
            // PHPCS 3.x.
210
            $config = new \PHP_CodeSniffer\Config();
4×
211

212
            /*
213
             * We just need to provide a standard so PHPCS will tokenize the file.
214
             * The standard itself doesn't actually matter for testing utility methods,
215
             * so use the smallest one to get the fastest results.
216
             */
217
            $config->standards = ['PSR1'];
4×
218

219
            /*
220
             * Limiting the run to just one sniff will make it, yet again, slightly faster.
221
             * Picked the simplest/fastest sniff available which is registered in PSR1.
222
             */
223
            $config->sniffs = static::$selectedSniff;
4×
224

225
            // Disable caching.
226
            $config->cache = false;
4×
227

228
            // Also set a tab-width to enable testing tab-replaced vs `orig_content`.
229
            $config->tabWidth = static::$tabWidth;
4×
230

231
            $ruleset = new \PHP_CodeSniffer\Ruleset($config);
4×
232

233
            // Make sure the file gets parsed correctly based on the file type.
234
            $contents = 'phpcs_input_file: ' . $caseFile . \PHP_EOL . $contents;
4×
235

236
            self::$phpcsFile = new \PHP_CodeSniffer\Files\DummyFile($contents, $ruleset, $config);
4×
237

238
            // Only tokenize the file, do not process it.
239
            try {
240
                self::$phpcsFile->parse();
4×
241
            } catch (TokenizerException $e) {
2×
242
                // PHPCS 3.5.0 and higher.
243
            } catch (RuntimeException $e) {
2×
244
                // PHPCS 3.0.0 < 3.5.0.
245
            }
246
        } else {
247
            // PHPCS 2.x.
248
            $phpcs           = new \PHP_CodeSniffer(null, static::$tabWidth);
4×
249
            self::$phpcsFile = new \PHP_CodeSniffer_File(
4×
250
                $caseFile,
4×
251
                [],
4×
252
                [],
4×
253
                $phpcs
254
            );
255

256
            /*
257
             * Using error silencing to drown out "undefined index" notices for tokenizer
258
             * issues in PHPCS 2.x which won't get fixed anymore anyway.
259
             */
260
            @self::$phpcsFile->start($contents);
4×
261
        }
262

263
        // Fail the test if the case file failed to tokenize.
264
        if (self::$phpcsFile->numTokens === 0) {
8×
265
            parent::fail("Tokenizing of the test case file failed for case file: $caseFile");
4×
266
        }
267
    }
4×
268

269
    /**
270
     * Skip JS and CSS related tests on PHPCS 4.x.
271
     *
272
     * PHPCS 4.x drops support for the JS and CSS tokenizers.
273
     * This method takes care of automatically skipping tests involving JS/CSS case files
274
     * when the tests are being run with PHPCS 4.x.
275
     *
276
     * Note: This is a PHPUnit cross-version compatible {@see \PHPUnit\Framework\TestCase::setUp()}
277
     * method.
278
     *
279
     * @since 1.0.0-alpha3
280
     *
281
     * @before
282
     *
283
     * @return void
284
     */
285
    public function skipJSCSSTestsOnPHPCS4()
286
    {
287
        if (static::$fileExtension !== 'js' && static::$fileExtension !== 'css') {
68×
288
            return;
64×
289
        }
290

291
        if (\version_compare(self::$phpcsVersion, '3.99.99', '<=')) {
4×
292
            return;
4×
293
        }
294

295
        $this->markTestSkipped('JS and CSS support has been removed in PHPCS 4.');
!
296
    }
!
297

298
    /**
299
     * Clean up after finished test.
300
     *
301
     * Note: This is a PHPUnit cross-version compatible {@see \PHPUnit\Framework\TestCase::tearDownAfterClass()}
302
     * method.
303
     *
304
     * @since 1.0.0
305
     *
306
     * @afterClass
307
     *
308
     * @return void
309
     */
310
    public static function resetTestFile()
311
    {
312
        self::$phpcsFile = null;
4×
313
    }
4×
314

315
    /**
316
     * Check whether or not the PHP 8.0 identifier name tokens will be in use.
317
     *
318
     * The expected token positions/token counts for certain tokens will differ depending
319
     * on whether the PHP 8.0 identifier name tokenization is used or the PHP < 8.0
320
     * identifier name tokenization.
321
     *
322
     * Tests can use this method to determine which flavour of tokenization to expect and
323
     * to set test expectations accordingly.
324
     *
325
     * @codeCoverageIgnore Nothing to test.
326
     *
327
     * @since 1.0.0
328
     *
329
     * @return bool
330
     */
331
    public static function usesPhp8NameTokens()
332
    {
333
        $version = Helper::getVersion();
334
        if ((\version_compare(\PHP_VERSION_ID, '80000', '>=') === true
335
            && \version_compare($version, '3.5.7', '<') === true)
336
            || \version_compare($version, '4.0.0', '>=') === true
337
        ) {
338
            return true;
339
        }
340

341
        return false;
342
    }
343

344
    /**
345
     * Get the token pointer for a target token based on a specific comment.
346
     *
347
     * Note: the test delimiter comment MUST start with `/* test` to allow this function to
348
     * distinguish between comments used *in* a test and test delimiters.
349
     *
350
     * If the delimiter comment is not found, the test will automatically be failed.
351
     *
352
     * @since 1.0.0
353
     *
354
     * @param string           $commentString The complete delimiter comment to look for as a string.
355
     *                                        This string should include the comment opener and closer.
356
     * @param int|string|array $tokenType     The type of token(s) to look for.
357
     * @param string           $tokenContent  Optional. The token content for the target token.
358
     * @param bool             $failTest      Optional. Whether the test should be marked as failed when
359
     *                                        the target token cannot be found. Defaults to `true`.
360
     *                                        When set to `false`, a catchable PHP native `RuntimeException`
361
     *                                        will be thrown instead.
362
     *
363
     * @return int
364
     *
365
     * @throws \RuntimeException When the target token cannot be found and `$failTest` has been set to `false`.
366
     */
367
    public function getTargetToken($commentString, $tokenType, $tokenContent = null, $failTest = true)
368
    {
369
        $start   = (self::$phpcsFile->numTokens - 1);
48×
370
        $comment = self::$phpcsFile->findPrevious(
48×
371
            \T_COMMENT,
48×
372
            $start,
373
            null,
48×
374
            false,
48×
375
            $commentString
376
        );
377

378
        if ($comment === false) {
49×
379
            $msg = 'Failed to find the test marker: ' . $commentString;
4×
380
            $this->fail($msg);
4×
381
        }
382

383
        $tokens = self::$phpcsFile->getTokens();
44×
384
        $end    = ($start + 1);
44×
385

386
        // Limit the token finding to between this and the next delimiter comment.
387
        for ($i = ($comment + 1); $i < $end; $i++) {
44×
388
            if ($tokens[$i]['code'] !== \T_COMMENT) {
44×
389
                continue;
44×
390
            }
391

392
            if (\stripos($tokens[$i]['content'], '/* test') === 0) {
44×
393
                $end = $i;
44×
394
                break;
44×
395
            }
396
        }
397

398
        $target = self::$phpcsFile->findNext(
44×
399
            $tokenType,
44×
400
            ($comment + 1),
44×
401
            $end,
402
            false,
44×
403
            $tokenContent
404
        );
405

406
        if ($target === false) {
44×
407
            $msg = 'Failed to find test target token for comment string: ' . $commentString;
8×
408
            if ($tokenContent !== null) {
8×
409
                $msg .= ' With token content: ' . $tokenContent;
8×
410
            }
411

412
            if ($failTest === false) {
8×
413
                throw new PHPRuntimeException($msg);
4×
414
            }
415

416
            $this->fail($msg);
4×
417
        }
418

419
        return $target;
36×
420
    }
421

422
    /**
423
     * Helper method to tell PHPUnit to expect a PHPCS Exception in a PHPUnit and PHPCS cross-version
424
     * compatible manner.
425
     *
426
     * @since 1.0.0
427
     *
428
     * @param string $msg  The expected exception message.
429
     * @param string $type The PHPCS native exception type to expect. Either 'runtime' or 'tokenizer'.
430
     *                     Defaults to 'runtime'.
431
     *
432
     * @return void
433
     */
434
    public function expectPhpcsException($msg, $type = 'runtime')
435
    {
436
        $exception = 'PHP_CodeSniffer\Exceptions\RuntimeException';
8×
437
        if ($type === 'tokenizer') {
8×
438
            $exception = 'PHP_CodeSniffer\Exceptions\TokenizerException';
4×
439
        }
440

441
        if (\method_exists($this, 'expectException')) {
8×
442
            // PHPUnit 5+.
443
            $this->expectException($exception);
8×
444
            $this->expectExceptionMessage($msg);
8×
445
        } else {
446
            // PHPUnit 4.
UNCOV
447
            $this->setExpectedException($exception, $msg);
!
448
        }
449
    }
8×
450
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc