Skip to content

Commit

Permalink
Start writing tests for unit test checks
Browse files Browse the repository at this point in the history
This change includes:
- detection of private providers
- detection of providers which do not exist
- detection of incorrect casing of the dataProvider declaration
- detection of providers which do not have a correct return type
- detection of providers which are not static
- detection of providers whose names start with test_

Fixes moodlehq#42
  • Loading branch information
andrewnicols committed Sep 15, 2023
1 parent 9292e56 commit b86c003
Show file tree
Hide file tree
Showing 13 changed files with 974 additions and 0 deletions.
13 changes: 13 additions & 0 deletions moodle-extra/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,17 @@
-->

<rule ref="moodle.Classes.UnitTestFormatting"/>

<!--
Detect issues with Unit Test dataProviders:
- private providers
- providers which do not exist
- providers whose name is prefixed with _test
- incorrect casing of dataProvider
- dataProviders which do not return an array or Iterable
- dataProviders which can be converted to a static method (PHPUnit 10 compatibility)
-->
<rule ref="moodle.PHPUnit.TestCaseProvider"/>

</ruleset>
209 changes: 209 additions & 0 deletions moodle/Sniffs/Classes/UnitTestFormattingSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Sniffs\Classes;

// phpcs:disable moodle.NamingConventions

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\FunctionDeclarations;
use PHPCSUtils\Utils\Scopes;

/**
* Verifies that PHP Unit Tests are well formatted.
*
* @package local_codechecker
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class UnitTestFormattingSniff implements Sniff {
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
];


/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return [
T_FUNCTION,
];
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr) {
$filename = $phpcsFile->getFilename();
if (strpos($filename, '/tests/') === false) {
// Not a unit test.
return;
}

if (strpos($filename, '/tests/behat/') !== false) {
// Ignore Behat tests.
return;
}

if (strpos($filename, '/tests/fixtures/') !== false) {
// Ignore fixtures.
return;
}

// This must be either a test (suffix _test.php) or a TestCase (suffix _testcase.php).
$istest = $this->_is_test($filename);
$istestcase = $this->_is_testcase($filename);

if (!$istest && !$istestcase) {
// Neither a test, nor a testcase.
return;
}

$scopePtr = Scopes::validDirectScope($phpcsFile, $stackPtr, Tokens::$ooScopeTokens);
if ($scopePtr === false) {
// Not an OO method.
return;
}

$this->_test_setup_teardown($phpcsFile, $stackPtr);

// If this is a function/class/interface doc block comment, skip it.
// We are only interested in inline doc block comments, which are
// not allowed.

$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_FUNCTION) {
$nextToken = $phpcsFile->findNext(
Tokens::$emptyTokens,
($stackPtr + 1),
null,
true
);
}
}

private function _test_setup_teardown(File $phpcsFile, $stackPtr): void {
// Check casing.
$functionName = FunctionDeclarations::getName($phpcsFile, $stackPtr);
$functionNameLC = \strtolower($functionName);

if ($functionNameLC === 'setup' && $functionName !== 'setUp') {
$this->_correct_setup_teardown_casing($phpcsFile, $stackPtr, $functionName, 'setUp');
}

if ($functionNameLC === 'teardown' && $functionName !== 'tearDown') {
$this->_correct_setup_teardown_casing($phpcsFile, $stackPtr, $functionName, 'tearDown');
}

if ($functionNameLC === 'setupbeforeclass' && $functionName !== 'setupBeforeClass') {
$this->_correct_setup_teardown_casing($phpcsFile, $stackPtr, $functionName, 'setupBeforeClass');
}

if ($functionNameLC === 'teardownafterclass' && $functionName !== 'tearDownAfterClass') {
$this->_correct_setup_teardown_casing($phpcsFile, $stackPtr, $functionName, 'tearDownAfterClass');
}
}

private function _correct_setup_teardown_casing(
File $phpcsFile,
$stackPtr,
string $functionName,
string $correctName
): void {
$namePtr = $this->_getNamePointer($phpcsFile, $stackPtr);
$fix = $phpcsFile->addFixableError(
"Use '{$correctName}' for PHPUnit setup/teardown methods. Found %s",
$namePtr,
'SetUpTearDownCasing',
[$functionName]
);

if ($fix === true) {
$phpcsFile->fixer->replaceToken($namePtr, $correctName);
}

}

private function _getNamePointer(
File $phpcsFile,
$stackPtr
): ?int {
$tokens = $phpcsFile->getTokens();
$stopPoint = $phpcsFile->numTokens;
$tokenCode = $tokens[$stackPtr]['code'];
if ($tokenCode === \T_FUNCTION && isset($tokens[$stackPtr]['parenthesis_opener']) === true) {
$stopPoint = $tokens[$stackPtr]['parenthesis_opener'];
} elseif (isset($tokens[$stackPtr]['scope_opener']) === true) {
$stopPoint = $tokens[$stackPtr]['scope_opener'];
}

$exclude = Tokens::$emptyTokens;
$exclude[] = \T_OPEN_PARENTHESIS;
$exclude[] = \T_OPEN_CURLY_BRACKET;
$exclude[] = \T_BITWISE_AND;
$exclude[] = \T_COLON; // Backed enums.

$nameStart = $phpcsFile->findNext($exclude, ($stackPtr + 1), $stopPoint, true);
if ($nameStart === false) {
// Live coding or parse error.
return null;
}

return $nameStart;
}

/**
* Whether the supplifed filename relates to a Test.
*
* @param string $filename
* @return bool
*/
private function _is_test(string $filename): bool {
return substr(
$filename,
strlen($filename) - strlen('_test.php')
) === '_test.php';
}

/**
* Whether the supplifed filename relates to a Testcase.
*
* @param string $filename
* @return bool
*/
private function _is_testcase(string $filename): bool {
return substr(
$filename,
strlen($filename) - strlen('_test.php')
) === '_testcase.php';
}
}
Loading

0 comments on commit b86c003

Please sign in to comment.