Root Zanli
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
opt
/
cpanel
/
ea-wappspector
/
vendor
/
squizlabs
/
php_codesniffer
/
tests
/
Core
/
Util
/
Help
/
Filename :
HelpTest.php
back
Copy
<?php /** * Tests to verify that the "help" command functions as expected. * * @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl> * @copyright 2024 PHPCSStandards and contributors * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence */ namespace PHP_CodeSniffer\Tests\Core\Util\Help; use PHP_CodeSniffer\Tests\ConfigDouble; use PHP_CodeSniffer\Util\Help; use PHPUnit\Framework\TestCase; use ReflectionMethod; use ReflectionProperty; /** * Test the Help class. * * @covers \PHP_CodeSniffer\Util\Help */ final class HelpTest extends TestCase { /** * QA check: verify that the category names are at most the minimum screen width * and that option argument names are always at most half the length of the minimum screen width. * * If this test would start failing, either wrapping of argument info would need to be implemented * or the minimum screen width needs to be upped. * * @coversNothing * * @return void */ public function testQaArgumentNamesAreWithinAcceptableBounds() { $help = new Help(new ConfigDouble(), []); $allOptions = $this->invokeReflectionMethod($help, 'getAllOptions'); $this->assertGreaterThan(0, count($allOptions), 'No categories found'); $minScreenWidth = Help::MIN_WIDTH; $maxArgWidth = ($minScreenWidth / 2); foreach ($allOptions as $category => $options) { $this->assertLessThanOrEqual( Help::MIN_WIDTH, strlen($category), "Category name $category is longer than the minimum screen width of $minScreenWidth" ); foreach ($options as $option) { if (isset($option['argument']) === false) { continue; } $this->assertLessThanOrEqual( $maxArgWidth, strlen($option['argument']), "Option name {$option['argument']} is longer than the half the minimum screen width of $minScreenWidth" ); } } }//end testQaArgumentNamesAreWithinAcceptableBounds() /** * QA check: verify that each option only contains a spacer, text or argument + description combo. * * @coversNothing * * @return void */ public function testQaValidCategoryOptionDefinitions() { $help = new Help(new ConfigDouble(), []); $allOptions = $this->invokeReflectionMethod($help, 'getAllOptions'); $this->assertGreaterThan(0, count($allOptions), 'No categories found'); foreach ($allOptions as $category => $options) { $this->assertGreaterThan(0, count($options), "No options found in category $category"); foreach ($options as $name => $option) { if (isset($option['spacer']) === true) { $this->assertStringStartsWith('blank-line', $name, 'The name for spacer items should start with "blank-line"'); } $this->assertFalse( isset($option['spacer'], $option['text']), "Option $name: spacer and text should not be combined in one option" ); $this->assertFalse( isset($option['spacer'], $option['argument']), "Option $name: spacer and argument should not be combined in one option" ); $this->assertFalse( isset($option['spacer'], $option['description']), "Option $name: spacer and description should not be combined in one option" ); $this->assertFalse( isset($option['text'], $option['argument']), "Option $name: text and argument should not be combined in one option" ); $this->assertFalse( isset($option['text'], $option['description']), "Option $name: text and description should not be combined in one option" ); if (isset($option['argument']) === true) { $this->assertArrayHasKey( 'description', $option, "Option $name: an argument should always be accompanied by a description" ); } if (isset($option['description']) === true) { $this->assertArrayHasKey( 'argument', $option, "Option $name: a description should always be accompanied by an argument" ); } }//end foreach }//end foreach }//end testQaValidCategoryOptionDefinitions() /** * Test receiving an expected exception when the shortOptions parameter is not passed a string value. * * @return void */ public function testConstructorInvalidArgumentException() { $exception = 'InvalidArgumentException'; $message = 'The $shortOptions parameter must be a string'; if (method_exists($this, 'expectException') === true) { // PHPUnit 5+. $this->expectException($exception); $this->expectExceptionMessage($message); } else { // PHPUnit 4. $this->setExpectedException($exception, $message); } new Help(new ConfigDouble(), [], []); }//end testConstructorInvalidArgumentException() /** * Test filtering of the options by requested options. * * Tests that: * - Options not explicitly requested are removed. * - Short options passed via the longOptions array are still respected. * - A category gets removed if all options are removed, even if the category still has spacers. * * @param array<string> $longOptions The long options which should be displayed. * @param string $shortOptions The short options which should be displayed. * @param array<string, int> $expected The categories expected after filtering with the number * of expected help items per category. * * @dataProvider dataOptionFiltering * * @return void */ public function testOptionFiltering($longOptions, $shortOptions, $expected) { $help = new Help(new ConfigDouble(), $longOptions, $shortOptions); $activeOptions = $this->getReflectionProperty($help, 'activeOptions'); // Simplify the value to make it comparible. foreach ($activeOptions as $category => $options) { $activeOptions[$category] = count($options); } $this->assertSame($expected, $activeOptions, 'Option count per category does not match'); }//end testOptionFiltering() /** * Data provider. * * @return array<string, array<string, string|array<string>|array<string, int>>> */ public static function dataOptionFiltering() { $allLongOptions = explode(',', Help::DEFAULT_LONG_OPTIONS); $allLongOptions[] = 'cache'; $allLongOptions[] = 'no-cache'; $allLongOptions[] = 'report'; $allLongOptions[] = 'report-file'; $allLongOptions[] = 'report-report'; $allLongOptions[] = 'runtime-set'; $allLongOptions[] = 'config-explain'; $allLongOptions[] = 'config-set'; $allLongOptions[] = 'config-delete'; $allLongOptions[] = 'config-show'; $allLongOptions[] = 'generator'; $allLongOptions[] = 'suffix'; $allShortOptions = Help::DEFAULT_SHORT_OPTIONS.'saem'; return [ 'No options' => [ 'longOptions' => [], 'shortOptions' => '', 'expected' => [], ], 'Invalid options have no influence' => [ 'longOptions' => [ 'doesnotexist', 'invalid', ], 'shortOptions' => 'bjrz', 'expected' => [], ], 'Short options passed as long options works fine' => [ 'longOptions' => [ 's', 'suffix', 'a', 'e', 'colors', ], 'shortOptions' => '', 'expected' => [ 'Rule Selection Options' => 1, 'Run Options' => 2, 'Reporting Options' => 2, ], ], 'All options' => [ 'longOptions' => $allLongOptions, 'shortOptions' => $allShortOptions, 'expected' => [ 'Scan targets' => 8, 'Rule Selection Options' => 7, 'Run Options' => 8, 'Reporting Options' => 19, 'Configuration Options' => 8, 'Miscellaneous Options' => 5, ], ], 'Default options only' => [ 'longOptions' => explode(',', Help::DEFAULT_LONG_OPTIONS), 'shortOptions' => Help::DEFAULT_SHORT_OPTIONS, 'expected' => [ 'Scan targets' => 8, 'Rule Selection Options' => 5, 'Run Options' => 4, 'Reporting Options' => 14, 'Configuration Options' => 4, 'Miscellaneous Options' => 5, ], ], 'Only one category' => [ 'longOptions' => [ 'file', 'stdin-path', 'file-list', 'filter', 'ignore', 'extensions', ], 'shortOptions' => '-l', 'expected' => [ 'Scan targets' => 8, ], ], 'All except one category' => [ 'longOptions' => array_diff($allLongOptions, ['version', 'vv', 'vvv']), 'shortOptions' => str_replace(['h', 'v'], '', $allShortOptions), 'expected' => [ 'Scan targets' => 8, 'Rule Selection Options' => 7, 'Run Options' => 8, 'Reporting Options' => 19, 'Configuration Options' => 8, ], ], ]; }//end dataOptionFiltering() /** * Test filtering of the options by requested options does not leave stray spacers at the start * or end of a category and that a category does not contain two consecutive spacers. * * {@internal Careful! This test may need updates to still test what it is supposed to test * if/when the defined options in Help::getAllOptions() change.} * * @param array<string> $longOptions The long options which should be displayed. * @param string $shortOptions The short options which should be displayed. * * @dataProvider dataOptionFilteringSpacerHandling * * @return void */ public function testOptionFilteringSpacerHandling($longOptions, $shortOptions) { $help = new Help(new ConfigDouble(), $longOptions, $shortOptions); $activeOptions = $this->getReflectionProperty($help, 'activeOptions'); $this->assertNotEmpty($activeOptions, 'Active options is empty, test is invalid'); foreach ($activeOptions as $options) { $first = reset($options); $this->assertArrayNotHasKey('spacer', $first, 'Found spacer at start of category'); $last = end($options); $this->assertArrayNotHasKey('spacer', $last, 'Found spacer at end of category'); $previousWasSpacer = false; foreach ($options as $option) { $this->assertFalse((isset($option['spacer']) && $previousWasSpacer === true), 'Consecutive spacers found'); $previousWasSpacer = isset($option['spacer']); } } }//end testOptionFilteringSpacerHandling() /** * Data provider. * * @return array<string, array<string, string|array<string>>> */ public static function dataOptionFilteringSpacerHandling() { return [ 'No spacer at start of category' => [ 'longOptions' => ['generator'], 'shortOptions' => 'ie', ], 'No spacer at end of category' => [ 'longOptions' => [ 'encoding', 'tab-width', ], 'shortOptions' => '', ], 'No consecutive spacers within category' => [ 'longOptions' => [ 'report', 'report-file', 'report-report', 'report-width', 'basepath', 'ignore-annotations', 'colors', 'no-colors', ], 'shortOptions' => 'spqm', ], ]; }//end dataOptionFilteringSpacerHandling() /** * Test that if no short/long options are passed, only usage information is displayed (CS mode). * * @param array<string> $cliArgs Command line arguments. * @param string $expectedRegex Regex to validate expected output. * * @dataProvider dataDisplayUsage * * @return void */ public function testDisplayUsageCS($cliArgs, $expectedRegex) { if (PHP_CODESNIFFER_CBF === true) { $this->markTestSkipped('This test needs CS mode to run'); } $expectedRegex = str_replace('phpc(bf|s)', 'phpcs', $expectedRegex); $this->verifyDisplayUsage($cliArgs, $expectedRegex); }//end testDisplayUsageCS() /** * Test that if no short/long options are passed, only usage information is displayed (CBF mode). * * @param array<string> $cliArgs Command line arguments. * @param string $expectedRegex Regex to validate expected output. * * @dataProvider dataDisplayUsage * @group CBF * * @return void */ public function testDisplayUsageCBF($cliArgs, $expectedRegex) { if (PHP_CODESNIFFER_CBF === false) { $this->markTestSkipped('This test needs CBF mode to run'); } $expectedRegex = str_replace('phpc(bf|s)', 'phpcbf', $expectedRegex); $this->verifyDisplayUsage($cliArgs, $expectedRegex); }//end testDisplayUsageCBF() /** * Helper method to test that if no short/long options are passed, only usage information is displayed * (and displayed correctly). * * @param array<string> $cliArgs Command line arguments. * @param string $expectedRegex Regex to validate expected output. * * @return void */ private function verifyDisplayUsage($cliArgs, $expectedRegex) { $help = new Help(new ConfigDouble($cliArgs), []); $this->expectOutputRegex($expectedRegex); $help->display(); }//end verifyDisplayUsage() /** * Data provider. * * @return array<string, array<string, string|array<string>>> */ public static function dataDisplayUsage() { return [ 'Usage without colors' => [ 'cliArgs' => ['--no-colors'], 'expectedRegex' => '`^\s*Usage:\s+phpc(bf|s) \[options\] \<file\|directory\>\s+$`', ], 'Usage with colors' => [ 'cliArgs' => ['--colors'], 'expectedRegex' => '`^\s*\\033\[33mUsage:\\033\[0m\s+phpc(bf|s) \[options\] \<file\|directory\>\s+$`', ], ]; }//end dataDisplayUsage() /** * Test the column width calculations. * * This tests the following aspects: * 1. That the report width is never less than Help::MIN_WIDTH, even when a smaller width is passed. * 2. That the first column width is calculated correctly and is based on the longest argument. * 3. That the word wrapping of the description respects the maximum report width. * 4. That if the description is being wrapped, the indent for the second line is calculated correctly. * * @param int $reportWidth Report width for the test. * @param array<string> $longOptions The long options which should be displayed. * @param string $expectedOutput Expected output. * * @dataProvider dataReportWidthCalculations * * @return void */ public function testReportWidthCalculations($reportWidth, $longOptions, $expectedOutput) { $config = new ConfigDouble(["--report-width=$reportWidth", '--no-colors']); $help = new Help($config, $longOptions); $this->invokeReflectionMethod($help, 'printCategories'); $this->expectOutputString($expectedOutput); }//end testReportWidthCalculations() /** * Data provider. * * @return array<string, array<string, int|string|array<string>>> */ public static function dataReportWidthCalculations() { $longOptions = [ 'e', 'generator', ]; // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important. return [ 'Report width small: 40; forces report width to minimum width of 60' => [ 'reportWidth' => 40, 'longOptions' => $longOptions, 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL .' -e Explain a standard by showing the'.PHP_EOL .' names of all the sniffs it'.PHP_EOL .' includes.'.PHP_EOL .' --generator=<generator> Show documentation for a standard.'.PHP_EOL .' Use either the "HTML", "Markdown"'.PHP_EOL .' or "Text" generator.'.PHP_EOL, ], 'Report width is minimum: 60 (= self::MIN_WIDTH)' => [ 'reportWidth' => Help::MIN_WIDTH, 'longOptions' => $longOptions, 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL .' -e Explain a standard by showing the'.PHP_EOL .' names of all the sniffs it'.PHP_EOL .' includes.'.PHP_EOL .' --generator=<generator> Show documentation for a standard.'.PHP_EOL .' Use either the "HTML", "Markdown"'.PHP_EOL .' or "Text" generator.'.PHP_EOL, ], 'Report width matches length for one line, not the other: 96; only one should wrap' => [ 'reportWidth' => 96, 'longOptions' => $longOptions, 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL .' -e Explain a standard by showing the names of all the sniffs it includes.'.PHP_EOL .' --generator=<generator> Show documentation for a standard. Use either the "HTML", "Markdown"'.PHP_EOL .' or "Text" generator.'.PHP_EOL, ], 'Report width matches longest line: 119; the messages should not wrap and there should be no stray new line at the end' => [ 'reportWidth' => 119, 'longOptions' => $longOptions, 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL .' -e Explain a standard by showing the names of all the sniffs it includes.'.PHP_EOL .' --generator=<generator> Show documentation for a standard. Use either the "HTML", "Markdown" or "Text" generator.'.PHP_EOL, ], ]; // phpcs:enable }//end dataReportWidthCalculations() /** * Verify that variable elements in an argument specification get colorized correctly. * * @param string $input String to colorize. * @param string $expected Expected function output. * * @dataProvider dataColorizeVariableInput * * @return void */ public function testColorizeVariableInput($input, $expected) { $help = new Help(new ConfigDouble(), []); $result = $this->invokeReflectionMethod($help, 'colorizeVariableInput', $input); $this->assertSame($expected, $result); }//end testColorizeVariableInput() /** * Data provider. * * @return array<string, array<string, string|array<string>>> */ public static function dataColorizeVariableInput() { return [ 'Empty string' => [ 'input' => '', 'expected' => '', ], 'String without variable element(s)' => [ 'input' => 'This is text', 'expected' => 'This is text', ], 'String with variable element' => [ 'input' => 'This <is> text', 'expected' => "This \033[36m<is>\033[32m text", ], 'String with multiple variable elements' => [ 'input' => '<This> is <text>', 'expected' => "\033[36m<This>\033[32m is \033[36m<text>\033[32m", ], 'String with unclosed variable element' => [ 'input' => 'This <is text', 'expected' => 'This <is text', ], 'String with nested elements' => [ 'input' => '<This <is> text>', 'expected' => "\033[36m<This <is> text>\033[32m", ], 'String with nested elements and surrounding text' => [ 'input' => 'Start <This <is> text> end', 'expected' => "Start \033[36m<This <is> text>\033[32m end", ], ]; }//end dataColorizeVariableInput() /** * Test the various option types within a category get displayed correctly. * * @param array<string, array<string, string>> $input The options to print. * @param array<string, string> $expectedRegex Regexes to validate expected output. * * @dataProvider dataPrintCategoryOptions * * @return void */ public function testPrintCategoryOptionsNoColor($input, $expectedRegex) { $config = new ConfigDouble(['--no-colors']); $help = new Help($config, []); $this->setReflectionProperty($help, 'activeOptions', ['cat' => $input]); $this->invokeReflectionMethod($help, 'setMaxOptionNameLength'); $this->invokeReflectionMethod($help, 'printCategoryOptions', $input); $this->expectOutputRegex($expectedRegex['no-color']); }//end testPrintCategoryOptionsNoColor() /** * Test the various option types within a category get displayed correctly. * * @param array<string, array<string, string>> $input The options to print. * @param array<string, string> $expectedRegex Regexes to validate expected output. * * @dataProvider dataPrintCategoryOptions * * @return void */ public function testPrintCategoryOptionsColor($input, $expectedRegex) { $config = new ConfigDouble(['--colors']); $help = new Help($config, []); $this->setReflectionProperty($help, 'activeOptions', ['cat' => $input]); $this->invokeReflectionMethod($help, 'setMaxOptionNameLength'); $this->invokeReflectionMethod($help, 'printCategoryOptions', $input); $this->expectOutputRegex($expectedRegex['color']); }//end testPrintCategoryOptionsColor() /** * Data provider. * * @return array<string, array<string, array<string, array<string, string>>|array<string, string>>> */ public static function dataPrintCategoryOptions() { $indentLength = strlen(Help::INDENT); $gutterLength = strlen(Help::GUTTER); // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important. // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found -- Test readability is more important. return [ 'Input: arg, spacer, arg; new lines in description get preserved' => [ 'input' => [ 'short-option' => [ 'argument' => '-a', 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', ], 'blank-line' => [ 'spacer' => '', ], 'long-option-multi-line-description' => [ 'argument' => '--something=<var>', 'description' => 'Proin sit amet malesuada libero, finibus bibendum tortor. Nulla vitae quam nec orci finibus pharetra.' ."\n".'Nam eget blandit dui.', ], ], 'expectedRegex' => [ 'no-color' => '`^ {'.$indentLength.'}-a {15} {'.$gutterLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' .'\R' .' {'.$indentLength.'}--something=<var> {'.$gutterLength.'}Proin sit amet malesuada libero, finibus bibendum tortor\.\R' .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nulla vitae quam nec orci finibus pharetra\.\R' .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nam eget blandit dui\.\R$`', 'color' => '`^ {'.$indentLength.'}\\033\[32m-a {15}\\033\[0m {'.$gutterLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' .'\R' .' {'.$indentLength.'}\\033\[32m--something=\\033\[36m<var>\\033\[32m\\033\[0m {'.$gutterLength.'}Proin sit amet malesuada libero, finibus bibendum tortor\.\R' .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nulla vitae quam nec orci finibus pharetra\.\R' .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nam eget blandit dui\.\R$`', ], ], 'Input: text, arg, text; multi-line text gets wrapped' => [ 'input' => [ 'single-line-text' => [ 'text' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', ], 'argument-description' => [ 'argument' => '--something', 'description' => 'Fusce dapibus sodales est eu sodales.', ], 'multi-line-text-gets-wrapped' => [ 'text' => 'Maecenas vulputate ligula vel feugiat finibus. Mauris sem dui, pretium in turpis auctor, consectetur ultrices lorem.', ], ], 'expectedRegex' => [ 'no-color' => '`^ {'.$indentLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' .' {'.$indentLength.'}--something {'.$gutterLength.'}Fusce dapibus sodales est eu sodales\.\R' .' {'.$indentLength.'}Maecenas vulputate ligula vel feugiat finibus. Mauris sem dui, pretium in\R' .' {'.$indentLength.'}turpis auctor, consectetur ultrices lorem\.\R$`', 'color' => '`^ {'.$indentLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' .' {'.$indentLength.'}\\033\[32m--something\\033\[0m {'.$gutterLength.'}Fusce dapibus sodales est eu sodales\.\R' .' {'.$indentLength.'}Maecenas vulputate ligula vel feugiat finibus. Mauris sem dui, pretium in\R' .' {'.$indentLength.'}turpis auctor, consectetur ultrices lorem\.\R$`', ], ], ]; // phpcs:enable }//end dataPrintCategoryOptions() /** * Test Helper: invoke a reflected method which is not publicly accessible. * * @param \PHP_CodeSniffer\Util\Help $help Instance of a Help object. * @param string $methodName The name of the method to invoke. * @param mixed $params Optional. Parameters to pass to the method invocation. * * @return mixed */ private function invokeReflectionMethod(Help $help, $methodName, $params=null) { $reflMethod = new ReflectionMethod($help, $methodName); (PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); if ($params === null) { $returnValue = $reflMethod->invoke($help); } else { $returnValue = $reflMethod->invoke($help, $params); } (PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); return $returnValue; }//end invokeReflectionMethod() /** * Test Helper: retrieve the value of property which is not publicly accessible. * * @param \PHP_CodeSniffer\Util\Help $help Instance of a Help object. * @param string $properyName The name of the property to retrieve. * * @return mixed */ private function getReflectionProperty(Help $help, $properyName) { $reflProperty = new ReflectionProperty($help, $properyName); (PHP_VERSION_ID < 80100) && $reflProperty->setAccessible(true); $returnValue = $reflProperty->getValue($help); (PHP_VERSION_ID < 80100) && $reflProperty->setAccessible(false); return $returnValue; }//end getReflectionProperty() /** * Test Helper: set the value of property which is not publicly accessible. * * @param \PHP_CodeSniffer\Util\Help $help Instance of a Help object. * @param string $properyName The name of the property to set. * @param mixed $value The value to set. * * @return void */ private function setReflectionProperty(Help $help, $properyName, $value) { $reflProperty = new ReflectionProperty($help, $properyName); (PHP_VERSION_ID < 80100) && $reflProperty->setAccessible(true); $reflProperty->setValue($help, $value); (PHP_VERSION_ID < 80100) && $reflProperty->setAccessible(false); }//end setReflectionProperty() }//end class