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'
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 |