437 lines
14 KiB
PHP
Executable File
437 lines
14 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* Copyright © Magento, Inc. All rights reserved.
|
|
* See COPYING.txt for license details.
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
namespace Magento\Test\Php;
|
|
|
|
use Magento\Framework\App\Utility\Files;
|
|
use Magento\TestFramework\CodingStandard\Tool\CodeMessDetector;
|
|
use Magento\TestFramework\CodingStandard\Tool\CodeSniffer;
|
|
use Magento\TestFramework\CodingStandard\Tool\CodeSniffer\Wrapper;
|
|
use Magento\TestFramework\CodingStandard\Tool\CopyPasteDetector;
|
|
use Magento\TestFramework\CodingStandard\Tool\PhpCompatibility;
|
|
use Magento\TestFramework\CodingStandard\Tool\PhpStan;
|
|
use Magento\TestFramework\Utility\AddedFiles;
|
|
use Magento\TestFramework\Utility\FilesSearch;
|
|
use PHPMD\TextUI\Command;
|
|
|
|
/**
|
|
* Set of tests for static code analysis, e.g. code style, code complexity, copy paste detecting, etc.
|
|
*/
|
|
class LiveCodeTest extends \PHPUnit\Framework\TestCase
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $reportDir = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $pathToSource = '';
|
|
|
|
/**
|
|
* Setup basics for all tests
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function setUpBeforeClass(): void
|
|
{
|
|
self::$pathToSource = BP;
|
|
self::$reportDir = self::$pathToSource . '/dev/tests/static/report';
|
|
if (!is_dir(self::$reportDir)) {
|
|
mkdir(self::$reportDir);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns base folder for suite scope
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function getBaseFilesFolder()
|
|
{
|
|
return __DIR__;
|
|
}
|
|
|
|
/**
|
|
* Returns base directory for whitelisted files
|
|
*
|
|
* @return string
|
|
*/
|
|
private static function getChangedFilesBaseDir()
|
|
{
|
|
return __DIR__ . '/..';
|
|
}
|
|
|
|
/**
|
|
* Returns whitelist based on blacklist and git changed files
|
|
*
|
|
* @param array $fileTypes
|
|
* @param string $changedFilesBaseDir
|
|
* @param string $baseFilesFolder
|
|
* @param string $whitelistFile
|
|
* @return array
|
|
*/
|
|
public static function getWhitelist(
|
|
$fileTypes = ['php'],
|
|
$changedFilesBaseDir = '',
|
|
$baseFilesFolder = '',
|
|
$whitelistFile = '/_files/whitelist/common.txt'
|
|
) {
|
|
$changedFiles = self::getChangedFilesList($changedFilesBaseDir);
|
|
if (empty($changedFiles)) {
|
|
return [];
|
|
}
|
|
|
|
$globPatternsFolder = ('' !== $baseFilesFolder) ? $baseFilesFolder : self::getBaseFilesFolder();
|
|
try {
|
|
$directoriesToCheck = Files::init()->readLists($globPatternsFolder . $whitelistFile);
|
|
} catch (\Exception $e) {
|
|
// no directories matched white list
|
|
return [];
|
|
}
|
|
$targetFiles = self::filterFiles($changedFiles, $fileTypes, $directoriesToCheck);
|
|
return $targetFiles;
|
|
}
|
|
|
|
/**
|
|
* This method loads list of changed files.
|
|
*
|
|
* List may be generated by:
|
|
* - dev/tests/static/get_github_changes.php utility (allow to generate diffs between branches),
|
|
* - CLI command "git diff --name-only > dev/tests/static/testsuite/Magento/Test/_files/changed_files_local.txt",
|
|
*
|
|
* If no generated changed files list found "git diff" will be used to find not committed changed
|
|
* (tests should be invoked from target gir repo).
|
|
*
|
|
* Note: "static" modifier used for compatibility with legacy implementation of self::getWhitelist method
|
|
*
|
|
* @param string $changedFilesBaseDir Base dir with previously generated list files
|
|
* @return string[] List of changed files
|
|
*/
|
|
private static function getChangedFilesList($changedFilesBaseDir)
|
|
{
|
|
return FilesSearch::getFilesFromListFile(
|
|
$changedFilesBaseDir ?: self::getChangedFilesBaseDir(),
|
|
'changed_files*',
|
|
function () {
|
|
// if no list files, probably, this is the dev environment
|
|
// phpcs:disable Generic.PHP.NoSilencedErrors,Magento2.Security.InsecureFunction
|
|
@exec('git diff --name-only', $changedFiles);
|
|
@exec('git diff --cached --name-only', $addedFiles);
|
|
// phpcs:enable
|
|
$changedFiles = array_unique(array_merge($changedFiles, $addedFiles));
|
|
return $changedFiles;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filter list of files.
|
|
*
|
|
* File removed from list:
|
|
* - if it not exists,
|
|
* - if allowed types are specified and file has another type (extension),
|
|
* - if allowed directories specified and file not located in one of them.
|
|
*
|
|
* Note: "static" modifier used for compatibility with legacy implementation of self::getWhitelist method
|
|
*
|
|
* @param string[] $files List of file paths to filter
|
|
* @param string[] $allowedFileTypes List of allowed file extensions (pass empty array to allow all)
|
|
* @param string[] $allowedDirectories List of allowed directories (pass empty array to allow all)
|
|
* @return string[] Filtered file paths
|
|
*/
|
|
private static function filterFiles(array $files, array $allowedFileTypes, array $allowedDirectories)
|
|
{
|
|
if (empty($allowedFileTypes)) {
|
|
$fileHasAllowedType = function () {
|
|
return true;
|
|
};
|
|
} else {
|
|
$fileHasAllowedType = function ($file) use ($allowedFileTypes) {
|
|
return in_array(pathinfo($file, PATHINFO_EXTENSION), $allowedFileTypes);
|
|
};
|
|
}
|
|
|
|
if (empty($allowedDirectories)) {
|
|
$fileIsInAllowedDirectory = function () {
|
|
return true;
|
|
};
|
|
} else {
|
|
$allowedDirectories = array_map('realpath', $allowedDirectories);
|
|
usort(
|
|
$allowedDirectories,
|
|
function ($dir1, $dir2) {
|
|
return strlen($dir1) - strlen($dir2);
|
|
}
|
|
);
|
|
$fileIsInAllowedDirectory = function ($file) use ($allowedDirectories) {
|
|
foreach ($allowedDirectories as $directory) {
|
|
if (strpos($file, $directory) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
|
|
$filtered = array_filter(
|
|
$files,
|
|
function ($file) use ($fileHasAllowedType, $fileIsInAllowedDirectory) {
|
|
$file = realpath($file);
|
|
if (false === $file) {
|
|
return false;
|
|
}
|
|
return $fileHasAllowedType($file) && $fileIsInAllowedDirectory($file);
|
|
}
|
|
);
|
|
|
|
return $filtered;
|
|
}
|
|
|
|
/**
|
|
* Retrieves full list of codebase paths without any files/folders filtered out
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getFullWhitelist()
|
|
{
|
|
try {
|
|
return Files::init()->readLists(__DIR__ . '/_files/whitelist/common.txt');
|
|
} catch (\Exception $e) {
|
|
// nothing is whitelisted
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether a full scan was requested.
|
|
*
|
|
* This can be set in the `phpunit.xml` used to run these test cases, by setting the constant
|
|
* `TESTCODESTYLE_IS_FULL_SCAN` to `1`, e.g.:
|
|
* ```xml
|
|
* <php>
|
|
* <!-- TESTCODESTYLE_IS_FULL_SCAN - specify if full scan should be performed for test code style test -->
|
|
* <const name="TESTCODESTYLE_IS_FULL_SCAN" value="0"/>
|
|
* </php>
|
|
* ```
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function isFullScan(): bool
|
|
{
|
|
return defined('TESTCODESTYLE_IS_FULL_SCAN') && TESTCODESTYLE_IS_FULL_SCAN === '1';
|
|
}
|
|
|
|
/**
|
|
* Test code quality using phpcs
|
|
*/
|
|
public function testCodeStyle()
|
|
{
|
|
$reportFile = self::$reportDir . '/phpcs_report.txt';
|
|
if (!file_exists($reportFile)) {
|
|
touch($reportFile);
|
|
}
|
|
$codeSniffer = new CodeSniffer('Magento', $reportFile, new Wrapper());
|
|
$fileList = $this->isFullScan() ? $this->getFullWhitelist() : self::getWhitelist(['php', 'phtml']);
|
|
$ignoreList = Files::init()->readLists(__DIR__ . '/_files/phpcs/ignorelist/*.txt');
|
|
if ($ignoreList) {
|
|
$ignoreListPattern = sprintf('#(%s)#i', implode('|', $ignoreList));
|
|
$fileList = array_filter(
|
|
$fileList,
|
|
function ($path) use ($ignoreListPattern) {
|
|
return !preg_match($ignoreListPattern, $path);
|
|
}
|
|
);
|
|
}
|
|
|
|
$result = $codeSniffer->run($fileList);
|
|
$report = file_get_contents($reportFile);
|
|
$this->assertEquals(
|
|
0,
|
|
$result,
|
|
"PHP Code Sniffer detected {$result} violation(s): " . PHP_EOL . $report
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test code quality using phpmd
|
|
*/
|
|
public function testCodeMess()
|
|
{
|
|
$reportFile = self::$reportDir . '/phpmd_report.txt';
|
|
$codeMessDetector = new CodeMessDetector(realpath(__DIR__ . '/_files/phpmd/ruleset.xml'), $reportFile);
|
|
|
|
if (!$codeMessDetector->canRun()) {
|
|
$this->markTestSkipped('PHP Mess Detector is not available.');
|
|
}
|
|
$fileList = self::getWhitelist(['php']);
|
|
$ignoreList = Files::init()->readLists(__DIR__ . '/_files/phpmd/ignorelist/*.txt');
|
|
if ($ignoreList) {
|
|
$ignoreListPattern = sprintf('#(%s)#i', implode('|', $ignoreList));
|
|
$fileList = array_filter(
|
|
$fileList,
|
|
function ($path) use ($ignoreListPattern) {
|
|
return !preg_match($ignoreListPattern, $path);
|
|
}
|
|
);
|
|
}
|
|
|
|
$result = $codeMessDetector->run($fileList);
|
|
|
|
$output = "";
|
|
if (file_exists($reportFile)) {
|
|
$output = file_get_contents($reportFile);
|
|
}
|
|
|
|
$this->assertEquals(
|
|
Command::EXIT_SUCCESS,
|
|
$result,
|
|
"PHP Code Mess has found error(s):" . PHP_EOL . $output
|
|
);
|
|
|
|
// delete empty reports
|
|
if (file_exists($reportFile)) {
|
|
unlink($reportFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test code quality using phpcpd
|
|
*/
|
|
public function testCopyPaste()
|
|
{
|
|
$reportFile = self::$reportDir . '/phpcpd_report.xml';
|
|
$copyPasteDetector = new CopyPasteDetector($reportFile);
|
|
|
|
if (!$copyPasteDetector->canRun()) {
|
|
$this->markTestSkipped('PHP Copy/Paste Detector is not available.');
|
|
}
|
|
|
|
$blackList = [];
|
|
foreach (glob(__DIR__ . '/_files/phpcpd/blacklist/*.txt') as $list) {
|
|
$blackList[] = file($list, FILE_IGNORE_NEW_LINES);
|
|
}
|
|
$blackList = array_merge([], ...$blackList);
|
|
|
|
$copyPasteDetector->setBlackList($blackList);
|
|
|
|
$result = $copyPasteDetector->run([BP]);
|
|
|
|
$output = file_exists($reportFile) ? file_get_contents($reportFile) : '';
|
|
|
|
$this->assertTrue(
|
|
$result,
|
|
"PHP Copy/Paste Detector has found error(s):" . PHP_EOL . $output
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Tests whitelisted files for strict type declarations.
|
|
*/
|
|
public function testStrictTypes()
|
|
{
|
|
$changedFiles = AddedFiles::getAddedFilesList(self::getChangedFilesBaseDir());
|
|
|
|
try {
|
|
$blackList = Files::init()->readLists(
|
|
self::getBaseFilesFolder() . '/_files/blacklist/strict_type.txt'
|
|
);
|
|
} catch (\Exception $e) {
|
|
// nothing matched black list
|
|
$blackList = [];
|
|
}
|
|
|
|
$toBeTestedFiles = array_diff(
|
|
self::filterFiles($changedFiles, ['php'], []),
|
|
$blackList
|
|
);
|
|
|
|
$filesMissingStrictTyping = [];
|
|
foreach ($toBeTestedFiles as $fileName) {
|
|
$file = file_get_contents($fileName);
|
|
if (strstr($file, 'strict_types=1') === false) {
|
|
$filesMissingStrictTyping[] = $fileName;
|
|
}
|
|
}
|
|
|
|
$this->assertCount(
|
|
0,
|
|
$filesMissingStrictTyping,
|
|
"Following files are missing strict type declaration:"
|
|
. PHP_EOL
|
|
. implode(PHP_EOL, $filesMissingStrictTyping)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test code quality using PHPStan
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function testPhpStan()
|
|
{
|
|
$reportFile = self::$reportDir . '/phpstan_report.txt';
|
|
$confFile = __DIR__ . '/_files/phpstan/phpstan.neon';
|
|
|
|
if (!file_exists($reportFile)) {
|
|
touch($reportFile);
|
|
}
|
|
|
|
$fileList = self::getWhitelist(['php']);
|
|
$blackList = Files::init()->readLists(__DIR__ . '/_files/phpstan/blacklist/*.txt');
|
|
if ($blackList) {
|
|
$blackListPattern = sprintf('#(%s)#i', implode('|', $blackList));
|
|
$fileList = array_filter(
|
|
$fileList,
|
|
function ($path) use ($blackListPattern) {
|
|
return !preg_match($blackListPattern, $path);
|
|
}
|
|
);
|
|
}
|
|
|
|
$phpStan = new PhpStan($confFile, $reportFile);
|
|
$exitCode = $phpStan->run($fileList);
|
|
$report = file_get_contents($reportFile);
|
|
|
|
$errorMessage = empty($report) ?
|
|
'PHPStan command run failed.' : 'PHPStan detected violation(s):' . PHP_EOL . $report;
|
|
$this->assertEquals(0, $exitCode, $errorMessage);
|
|
}
|
|
|
|
/**
|
|
* Tests whitelisted fixtures for reuse other fixtures.
|
|
*/
|
|
public function testFixtureReuse()
|
|
{
|
|
$changedFiles = self::getWhitelist(['php']);
|
|
$toBeTestedFiles = self::filterFiles($changedFiles, ['php'], []);
|
|
|
|
$filesWithIncorrectReuse = [];
|
|
foreach ($toBeTestedFiles as $fileName) {
|
|
//check only _files and Fixtures directory
|
|
if (!preg_match('/integration.+\/(_files|Fixtures)/', $fileName)) {
|
|
continue;
|
|
}
|
|
$file = str_replace(["\n", "\r"], '', file_get_contents($fileName));
|
|
if (preg_match('/(?<![\=\s*])\b(require|require_once|include)\b/', $file)) {
|
|
$filesWithIncorrectReuse[] = $fileName;
|
|
}
|
|
}
|
|
|
|
$this->assertEquals(
|
|
0,
|
|
count($filesWithIncorrectReuse),
|
|
"The following files incorrectly reuse fixtures:"
|
|
. PHP_EOL
|
|
. implode(PHP_EOL, $filesWithIncorrectReuse)
|
|
. PHP_EOL
|
|
. 'Please use Magento\TestFramework\Workaround\Override\Fixture\Resolver::requireDataFixture'
|
|
);
|
|
}
|
|
}
|