diff --git a/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php b/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php new file mode 100644 index 00000000..ea8c69b7 --- /dev/null +++ b/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php @@ -0,0 +1,279 @@ +. + +namespace MoodleCodeSniffer\moodle\Sniffs\PHPUnit; + +// phpcs:disable moodle.NamingConventions + +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Tokens; +use MoodleCodeSniffer\moodle\Util\MoodleUtil; + +/** + * Checks that a test file has the @coversxxx annotations properly defined. + * + * @package local_codechecker + * @copyright 2022 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class TestCaseCoversSniff implements Sniff { + + /** + * Register for open tag (only process once per file). + */ + public function register() { + return array(T_OPEN_TAG); + } + + /** + * Processes php files and perform various checks with file. + * + * @param File $file The file being scanned. + * @param int $pointer The position in the stack. + */ + public function process(File $file, $pointer) { + + // Before starting any check, let's look for various things. + + // Get the moodle branch being analysed. + $moodleBranch = MoodleUtil::getMoodleBranch($file); + + // We have all we need from core, let's start processing the file. + + // Get the file tokens, for ease of use. + $tokens = $file->getTokens(); + + // In various places we are going to ignore class/method prefixes (private, abstract...) + // and whitespace, create an array for all them. + $skipTokens = Tokens::$methodPrefixes + [T_WHITESPACE => T_WHITESPACE]; + + // We only want to do this once per file. + $prevopentag = $file->findPrevious(T_OPEN_TAG, $pointer - 1); + if ($prevopentag !== false) { + return; + } + + // If we aren't checking Moodle 4.0dev (400) and up, nothing to check. + if (isset($moodleBranch) && $moodleBranch < 400) { + // Make and exception for codechecker phpunit tests, so they are run always. + if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { + return; + } + } + + // If the file isn't under tests directory, nothing to check. + if (strpos($file->getFilename(), '/tests/') === false) { + return; + } + + // If the file isn't called, _test.php, nothing to check. + $fileName = basename($file->getFilename()); + if (substr($fileName, -9) !== '_test.php') { + // Make an exception for codechecker own phpunit fixtures here, allowing any name for them. + if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { + return; + } + } + + // Iterate over all the classes (hopefully only one, but that's not this sniff problem). + $cStart = $pointer; + while ($cStart = $file->findNext(T_CLASS, $cStart + 1)) { + $class = $file->getDeclarationName($cStart); + $classCovers = false; // To control when the class has a @covers tag. + $classCoversNothing = false; // To control when the class has a @coversNothing tag. + + // Only if the class is extending something. + // TODO: We could add a list of valid classes once we have a class-map available. + if (!$file->findNext(T_EXTENDS, $cStart + 1, $tokens[$cStart]['scope_opener'])) { + continue; + } + + // Ignore non ended "_test|_testcase" classes. + if (substr($class, -5) !== '_test' && substr($class, -9) != '_testcase') { + continue; + } + + // Let's see if the class has any phpdoc block (first non skip token must be end of phpdoc comment). + $docPointer = $file->findPrevious($skipTokens, $cStart - 1, null, true); + + // Found a phpdoc block, let's look for @covers, @coversNothing and @coversDefaultClass tags. + if ($tokens[$docPointer]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + $docStart = $tokens[$docPointer]['comment_opener']; + while ($docPointer) { // Let's look upwards, until the beginning of the phpdoc block. + $docPointer = $file->findPrevious(T_DOC_COMMENT_TAG, $docPointer - 1, $docStart); + if ($docPointer) { + $docTag = trim($tokens[$docPointer]['content']); + switch ($docTag) { + case '@covers': + $classCovers = $docPointer; + // Validate basic syntax (FQCN or ::). + $this->checkCoversTagsSyntax($file, $docPointer, '@covers'); + break; + case '@coversNothing': + $classCoversNothing = $docPointer; + // Validate basic syntax (empty). + $this->checkCoversTagsSyntax($file, $docPointer, '@coversNothing'); + break; + case '@coversDefaultClass': + // Validate basic syntax (FQCN). + $this->checkCoversTagsSyntax($file, $docPointer, '@coversDefaultClass'); + break; + } + } + } + } + + // Both @covers and @coversNothing, that's a mistake. 2 errors. + if ($classCovers && $classCoversNothing) { + $file->addError('Class %s has both @covers and @coversNothing tags, good contradiction', + $classCovers, 'ContradictoryClass', [$class]); + $file->addError('Class %s has both @covers and @coversNothing tags, good contradiction', + $classCoversNothing, 'ContradictoryClass', [$class]); + } + + // Iterate over all the methods in the class. + $mStart = $cStart; + while ($mStart = $file->findNext(T_FUNCTION, $mStart + 1, $tokens[$cStart]['scope_closer'])) { + $method = $file->getDeclarationName($mStart); + $methodCovers = false; // To control when the method has a @covers tag. + $methodCoversNothing = false; // To control when the method has a @coversNothing tag. + + // Ignore non test_xxxx() methods. + if (strpos($method, 'test_') !== 0) { + continue; + } + + // Let's see if the method has any phpdoc block (first non skip token must be end of phpdoc comment). + $docPointer = $file->findPrevious($skipTokens, $mStart - 1, null, true); + + // Found a phpdoc block, let's look for @covers and @coversNothing tags. + if ($tokens[$docPointer]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + $docStart = $tokens[$docPointer]['comment_opener']; + while ($docPointer) { // Let's look upwards, until the beginning of the phpdoc block. + $docPointer = $file->findPrevious(T_DOC_COMMENT_TAG, $docPointer - 1, $docStart); + if ($docPointer) { + $docTag = trim($tokens[$docPointer]['content']); + switch ($docTag) { + case '@covers': + $methodCovers = $docPointer; + // Validate basic syntax (FQCN or ::). + $this->checkCoversTagsSyntax($file, $docPointer, '@covers'); + break; + case '@coversNothing': + $methodCoversNothing = $docPointer; + // Validate basic syntax (empty). + $this->checkCoversTagsSyntax($file, $docPointer, '@coversNothing'); + break; + case '@coversDefaultClass': + // Not allowed in methods. + $file->addError('Method %s() has @coversDefaultClass tag, only allowed in classes', + $docPointer, 'DefaultClassNotAllowed', [$method]); + break; + } + } + } + } + + // No @covers or @coversNothing at any level, that's a missing one. + if (!$classCovers && !$classCoversNothing && !$methodCovers && !$methodCoversNothing) { + $file->addWarning('Test method %s() is missing any coverage information, own or at class level', + $mStart, 'Missing', [$method]); + } + + // Both @covers and @coversNothing, that's a mistake. 2 errors. + if ($methodCovers && $methodCoversNothing) { + $file->addError('Method %s() has both @covers and @coversNothing tags, good contradiction', + $methodCovers, 'ContradictoryMethod', [$method]); + $file->addError('Method %s() has both @covers and @coversNothing tags, good contradiction', + $methodCoversNothing, 'ContradictoryMethod', [$method]); + } + + // Found @coversNothing at class, and @covers at method, strange. Warning. + if ($classCoversNothing && $methodCovers) { + $file->addWarning('Class %s has @coversNothing, but there are methods covering stuff', + $classCoversNothing, 'ContradictoryMixed', [$class]); + $file->addWarning('Test method %s() is covering stuff, but class has @coversNothing', + $methodCovers, 'ContradictoryMixed', [$method]); + } + + // Found @coversNothing at class and method, redundant. Warning. + if ($classCoversNothing && $methodCoversNothing) { + $file->addWarning('Test method %s() has @coversNothing, but class also has it, redundant', + $methodCoversNothing, 'Redundant', [$method]); + } + + // Advance until the end of the method, if possible, to find the next one quicker. + $mStart = $tokens[$mStart]['scope_closer'] ?? $pointer + 1; + } + } + } + + /** + * Perform a basic syntax cheking of the values of the @coversXXX tags. + * + * @param File $file The file being scanned + * @param int $pointer pointer to the token that contains the tag. Calculations are based on that. + * @param string $tag $coversXXX tag to be checked. Verifications are different based on that. + * @return void + */ + protected function checkCoversTagsSyntax(File $file, int $pointer, string $tag) { + // Get the file tokens, for ease of use. + $tokens = $file->getTokens(); + + if ($tag === '@coversNothing') { + // Check that there isn't whitespace and string. + if ($tokens[$pointer + 1]['code'] === T_DOC_COMMENT_WHITESPACE && + $tokens[$pointer + 2]['code'] === T_DOC_COMMENT_STRING) { + $file->addError('Wrong %s annotation, it must be empty', + $pointer, 'NotEmpty', [$tag]); + } + } + + if ($tag === '@covers' || $tag === '@coversDefaultClass') { + // Check that there is whitespace and string. + if ($tokens[$pointer + 1]['code'] !== T_DOC_COMMENT_WHITESPACE || + $tokens[$pointer + 2]['code'] !== T_DOC_COMMENT_STRING) { + $file->addError('Wrong %s annotation, it must contain some value', + $pointer, 'Empty', [$tag]); + // No value, nothing else to check. + return; + } + } + + if ($tag === '@coversDefaultClass') { + // Check that value begins with \ (FQCN). + if (strpos($tokens[$pointer + 2]['content'], '\\') !== 0) { + $file->addError('Wrong %s annotation, it must be FQCN (\\ prefixed)', + $pointer, 'NoFQCN', [$tag]); + } + // Check that value does not contain :: (method). + if (strpos($tokens[$pointer + 2]['content'], '::') !== false) { + $file->addError('Wrong %s annotation, cannot point to a method (contains ::)', + $pointer, 'WrongMethod', [$tag]); + } + } + + if ($tag === '@covers') { + // Check value begins with \ (FQCN) or :: (method). + if (strpos($tokens[$pointer + 2]['content'], '\\') !== 0 && + strpos($tokens[$pointer + 2]['content'], '::') !== 0) { + $file->addError('Wrong %s annotation, it must be FQCN (\\ prefixed) or point to method (:: prefixed)', + $pointer, 'NoFQCNOrMethod', [$tag]); + } + } + } +} diff --git a/moodle/tests/fixtures/phpunit/testcasecovers_contradiction.php b/moodle/tests/fixtures/phpunit/testcasecovers_contradiction.php new file mode 100644 index 00000000..4e7af727 --- /dev/null +++ b/moodle/tests/fixtures/phpunit/testcasecovers_contradiction.php @@ -0,0 +1,18 @@ +verify_cs_results(); } + /** + * Test the moodle.Commenting.InlineComment sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Commenting\InlineCommentSniff + */ public function test_moodle_commenting_inlinecomment() { // Define the standard, sniff and fixture to use. @@ -122,10 +132,14 @@ public function test_moodle_commenting_inlinecomment() { } /** + * Test the moodle.Commenting.InlineComment sniff. + * * Note that, while this test continues passing, because * we load the .js file manually, now the moodle standard * by default enforces --extensions=php, so no .js file * will be inspected by default ever. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Commenting\InlineCommentSniff */ public function test_moodle_commenting_inlinecomment_js() { @@ -151,6 +165,11 @@ public function test_moodle_commenting_inlinecomment_js() { $this->verify_cs_results(); } + /** + * Test the moodle.ControlStructures.ControlSignature sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\ControlStructures\ControlSignatureSniff + */ public function test_moodle_controlstructures_controlsignature() { // Define the standard, sniff and fixture to use. @@ -173,6 +192,11 @@ public function test_moodle_controlstructures_controlsignature() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.LineLength sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\LineLengthSniff + */ public function test_moodle_files_linelength() { // Define the standard, sniff and fixture to use. @@ -197,6 +221,11 @@ public function test_moodle_files_linelength() { $this->verify_cs_results(); } + /** + * Test the Generic.Files.LineEndings sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineEndingsSniff + */ public function test_generic_files_lineendings() { // Define the standard, sniff and fixture to use. @@ -217,6 +246,11 @@ public function test_generic_files_lineendings() { $this->verify_cs_results(); } + /** + * Test the Generic.Files.EndFileNewline sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Files\EndFileNewlineSniff + */ public function test_generic_files_endfilenewline() { // Define the standard, sniff and fixture to use. @@ -237,6 +271,11 @@ public function test_generic_files_endfilenewline() { $this->verify_cs_results(); } + /** + * Test the Generic.WhiteSpace.DisallowTabIndent sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Whitespace\DisallowTabIndentSniff + */ public function test_generic_whitespace_disalowtabindent() { // Define the standard, sniff and fixture to use. @@ -259,6 +298,11 @@ public function test_generic_whitespace_disalowtabindent() { $this->verify_cs_results(); } + /** + * Test the Generic.Functions.OpeningFunctionBraceKernighanRitchie sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Functions\OpeningFunctionBraceKernighanRitchieSniff + */ public function test_generic_functions_openingfunctionbracekerninghanritchie() { // Define the standard, sniff and fixture to use. @@ -286,6 +330,11 @@ public function test_generic_functions_openingfunctionbracekerninghanritchie() { $this->verify_cs_results(); } + /** + * Test the Generic.Classes.OpeningBraceSameLine sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Classes\OpeningBraceSameLineSniff + */ public function test_generic_classes_openingclassbrace() { // Define the standard, sniff and fixture to use. @@ -311,6 +360,11 @@ public function test_generic_classes_openingclassbrace() { $this->verify_cs_results(); } + /** + * Test the Generic.WhiteSpace.ScopeIndent sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\ScopeIndentSniff + */ public function test_generic_whitespace_scopeindent() { // Define the standard, sniff and fixture to use. @@ -333,6 +387,11 @@ public function test_generic_whitespace_scopeindent() { $this->verify_cs_results(); } + /** + * Test the moodle.PHP.DeprecatedFunctions sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\PHP\DeprecatedFunctionsSniff + */ public function test_moodle_php_deprecatedfunctions() { // Define the standard, sniff and fixture to use. @@ -355,6 +414,11 @@ public function test_moodle_php_deprecatedfunctions() { $this->verify_cs_results(); } + /** + * Test the moodle.PHP.ForbiddenFunctions sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\PHP\ForbiddenFunctionsSniff + */ public function test_moodle_php_forbiddenfunctions() { // Define the standard, sniff and fixture to use. @@ -384,6 +448,11 @@ public function test_moodle_php_forbiddenfunctions() { $this->verify_cs_results(); } + /** + * Test the moodle.PHP.ForbiddenGlobalUse snifff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\PHP\ForbiddenGlobalUseSniff + */ public function test_moodle_php_forbidden_global_use() { // Define the standard, sniff and fixture to use. $this->set_standard('moodle'); @@ -463,6 +532,11 @@ public function test_moodle_php_forbidden_global_use() { $this->verify_cs_results(); } + /** + * Test the moodle.PHP.ForbiddenTokens sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\PHP\ForbiddenTokensSniff + */ public function test_moodle_php_forbiddentokens() { // Define the standard, sniff and fixture to use. @@ -486,6 +560,11 @@ public function test_moodle_php_forbiddentokens() { $this->verify_cs_results(); } + /** + * Test the moodle.Strings.ForbiddenStrings sniff. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Strings\ForbiddenStringsSniff + */ public function test_moodle_strings_forbiddenstrings() { // Define the standard, sniff and fixture to use. @@ -522,6 +601,8 @@ public function test_moodle_strings_forbiddenstrings() { /** * Test external sniff incorporated to moodle standard. + * + * @covers \PHPCompatibility\Sniffs\FunctionUse\RemovedFunctionsSniff */ public function test_phpcompatibility_php_deprecatedfunctions() { @@ -545,6 +626,8 @@ public function test_phpcompatibility_php_deprecatedfunctions() { /** * Test call time pass by reference. + * + * @covers \PHPCompatibility\Sniffs\Syntax\ForbiddenCallTimePassByReferenceSniff */ public function test_phpcompatibility_php_forbiddencalltimepassbyreference() { @@ -568,6 +651,8 @@ public function test_phpcompatibility_php_forbiddencalltimepassbyreference() { /** * Test variable naming standards + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\NamingConventions\ValidVariableNameSniff */ public function test_moodle_namingconventions_variablename() { @@ -605,6 +690,8 @@ public function test_moodle_namingconventions_variablename() { /** * Test operator spacing standards + * + * @covers \PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\OperatorSpacingSniff */ public function test_squiz_operator_spacing() { @@ -685,6 +772,8 @@ public function test_squiz_operator_spacing() { /** * Test object operator spacing standards + * + * @covers \PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ObjectOperatorSpacingSniff */ public function test_squiz_object_operator_spacing() { @@ -706,6 +795,8 @@ public function test_squiz_object_operator_spacing() { /** * Test object operator indentation standards + * + * @covers \PHP_CodeSniffer\Standards\PEAR\Sniffs\WhiteSpace\ObjectOperatorIndentSniff */ public function test_pear_object_operator_indent() { @@ -742,6 +833,8 @@ public function test_pear_object_operator_indent() { /** * Test variable naming standards + * + * @covers \PHP_CodeSniffer\Standards\Squiz\Sniffs\PHP\CommentedOutCodeSniff */ public function test_squid_php_commentedoutcode() { @@ -764,6 +857,11 @@ public function test_squid_php_commentedoutcode() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.RequireLogin sniff detects problems. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\RequireLoginSniff + */ public function test_moodle_files_requirelogin_problem() { $this->set_standard('moodle'); $this->set_sniff('moodle.Files.RequireLogin'); @@ -777,6 +875,11 @@ public function test_moodle_files_requirelogin_problem() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.RequireLogin sniff with login. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\RequireLoginSniff + */ public function test_moodle_files_requirelogin_require_login_ok() { $this->set_standard('moodle'); $this->set_sniff('moodle.Files.RequireLogin'); @@ -788,6 +891,11 @@ public function test_moodle_files_requirelogin_require_login_ok() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.RequireLogin sniff with require_course_login(). + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\RequireLoginSniff + */ public function test_moodle_files_requirelogin_require_course_login_ok() { $this->set_standard('moodle'); $this->set_sniff('moodle.Files.RequireLogin'); @@ -799,6 +907,11 @@ public function test_moodle_files_requirelogin_require_course_login_ok() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.RequireLogin sniff with external page setup. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\RequireLoginSniff + */ public function test_moodle_files_requirelogin_admin_externalpage_setup_ok() { $this->set_standard('moodle'); $this->set_sniff('moodle.Files.RequireLogin'); @@ -810,6 +923,11 @@ public function test_moodle_files_requirelogin_admin_externalpage_setup_ok() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.RequireLogin in a CLI script. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\RequireLoginSniff + */ public function test_moodle_files_requirelogin_cliscript_ok() { $this->set_standard('moodle'); $this->set_sniff('moodle.Files.RequireLogin'); @@ -821,6 +939,11 @@ public function test_moodle_files_requirelogin_cliscript_ok() { $this->verify_cs_results(); } + /** + * Test the moodle.Files.RequireLogin sniff in a no moodle cookies script. + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\Files\RequireLoginSniff + */ public function test_moodle_files_requirelogin_nomoodlecookies_ok() { $this->set_standard('moodle'); $this->set_sniff('moodle.Files.RequireLogin'); diff --git a/moodle/tests/phpunit_testcasecovers_test.php b/moodle/tests/phpunit_testcasecovers_test.php new file mode 100644 index 00000000..a2081399 --- /dev/null +++ b/moodle/tests/phpunit_testcasecovers_test.php @@ -0,0 +1,151 @@ +. + +namespace local_codechecker; + +use MoodleCodeSniffer\moodle\Util\MoodleUtil; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../tests/local_codechecker_testcase.php'); +require_once(__DIR__ . '/../Util/MoodleUtil.php'); + +// phpcs:disable moodle.NamingConventions + +/** + * Test the TestCaseCoversSniff sniff. + * + * @package local_codechecker + * @category test + * @copyright 2022 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \MoodleCodeSniffer\moodle\Sniffs\PHPUnit\TestCaseCoversSniff + */ +class phpunit_testcasecovers_test extends local_codechecker_testcase { + + /** + * Data provider for self::test_phpunit_testcasecovers + */ + public function provider_phpunit_testcasecovers() { + return [ + 'Correct' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_correct.php', + 'errors' => [], + 'warnings' => [], + ], + 'Contradiction' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_contradiction.php', + 'errors' => [ + 7 => 'contradiction_test has both', + 8 => 'TestCaseCovers.ContradictoryClass', + 12 => 'test_something() has both', + 13 => 'TestCaseCovers.ContradictoryMethod', + ], + 'warnings' => [ + 8 => 1, + 12 => 1, + 13 => 1, + ], + ], + 'Missing' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_missing.php', + 'errors' => [], + 'warnings' => [ + 8 => 'test_something() is missing any coverage information', + ], + ], + 'Mixed' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_mixed.php', + 'errors' => [], + 'warnings' => [ + 7 => 'contradictionmixed_test has @coversNothing, but there are methods covering stuff', + 11 => 'TestCaseCovers.ContradictoryMixed', + ], + ], + 'Redundant' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_redundant.php', + 'errors' => [], + 'warnings' => [ + 11 => 'has @coversNothing, but class also has it, redundant', + ], + ], + 'Skipped' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_skipped.php', + 'errors' => [], + 'warnings' => [], + ], + 'Covers' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_covers.php', + 'errors' => [ + 9 => 'it must be FQCN (\\ prefixed) or point to method (:: prefixed)', + 10 => 'it must contain some value', + 17 => 'TestCaseCovers.NoFQCNOrMethod', + 18 => 'TestCaseCovers.Empty', + ], + 'warnings' => [], + ], + 'CoversDefaultClass' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_coversdefaultclass.php', + 'errors' => [ + 8 => 'Wrong @coversDefaultClass annotation, it must be FQCN (\\ prefixed)', + 9 => 'TestCaseCovers.WrongMethod', + 10 => '@coversDefaultClass annotation, it must contain some value', + 14 => 'test_something() has @coversDefaultClass tag', + 15 => 'TestCaseCovers.DefaultClassNotAllowed', + 16 => 'TestCaseCovers.DefaultClassNotAllowed', + ], + 'warnings' => [], + ], + 'CoversNothing' => [ + 'fixture' => 'fixtures/phpunit/testcasecovers_coversnothing.php', + 'errors' => [ + 7 => '@coversNothing annotation, it must be empty', + 11 => 'TestCaseCovers.NotEmpty', + ], + 'warnings' => [ + 11 => 'has @coversNothing, but class also has it, redundant', + ], + ], + ]; + } + + /** + * Test the moodle.PHPUnit.TestCaseCovers sniff + * + * @param string $fixture relative path to fixture to use. + * @param array $errors array of errors expected. + * @param array $warnings array of warnings expected. + * @dataProvider provider_phpunit_testcasecovers + */ + public function test_phpunit_testcasecovers(string $fixture, array $errors, array $warnings) { + + // Define the standard, sniff and fixture to use. + $this->set_standard('moodle'); + $this->set_sniff('moodle.PHPUnit.TestCaseCovers'); + $this->set_fixture(__DIR__ . '/' . $fixture); + + // Define expected results (errors and warnings). Format, array of: + // - line => number of problems, or + // - line => array of contents for message / source problem matching. + // - line => string of contents for message / source problem matching (only 1). + $this->set_errors($errors); + $this->set_warnings($warnings); + + // Let's do all the hard work! + $this->verify_cs_results(); + } +} diff --git a/tests/behat/ui.feature b/tests/behat/ui.feature index 5b07ed6b..d5c6bdf1 100644 --- a/tests/behat/ui.feature +++ b/tests/behat/ui.feature @@ -40,9 +40,9 @@ Feature: Codechecker UI works as expected Examples: | path | exclude | seen | notseen | - | local/codechecker/moodle/tests | */tests/fixtures/* | Files found: 5 | Invalid path | + | local/codechecker/moodle/tests | */tests/fixtures/* | Files found: 6 | Invalid path | | local/codechecker/moodle/tests | */tests/fixtures/* | moodlestandard_test.php | Invalid path | - | local/codechecker/moodle/tests/ | *PHPC*, *moodle_* | Files found: 52 | Invalid path | + | local/codechecker/moodle/tests/ | *PHPC*, *moodle_* | Files found: 62 | Invalid path | | local/codechecker/moodle/tests/ | *PHPC*, *moodle_* | Line 1 of the opening comment | moodle_php | | local/codechecker/moodle/tests/ | *PHPC*, *moodle_* | Inline comments must end | /phpcompat | | local/codechecker/moodle/tests/ | *PHPC*, *moodle_* | Inline comments must end | /phpcompat |