LiveCodeTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Test\Php;
  7. use Magento\Framework\App\Utility\Files;
  8. use Magento\Framework\Component\ComponentRegistrar;
  9. use Magento\TestFramework\CodingStandard\Tool\CodeMessDetector;
  10. use Magento\TestFramework\CodingStandard\Tool\CodeSniffer;
  11. use Magento\TestFramework\CodingStandard\Tool\CodeSniffer\Wrapper;
  12. use Magento\TestFramework\CodingStandard\Tool\CopyPasteDetector;
  13. use PHPMD\TextUI\Command;
  14. /**
  15. * Set of tests for static code analysis, e.g. code style, code complexity, copy paste detecting, etc.
  16. */
  17. class LiveCodeTest extends \PHPUnit\Framework\TestCase
  18. {
  19. /**
  20. * @var string
  21. */
  22. protected static $reportDir = '';
  23. /**
  24. * @var string
  25. */
  26. protected static $pathToSource = '';
  27. /**
  28. * Setup basics for all tests
  29. *
  30. * @return void
  31. */
  32. public static function setUpBeforeClass()
  33. {
  34. self::$pathToSource = BP;
  35. self::$reportDir = self::$pathToSource . '/dev/tests/static/report';
  36. if (!is_dir(self::$reportDir)) {
  37. mkdir(self::$reportDir);
  38. }
  39. }
  40. /**
  41. * Returns base folder for suite scope
  42. *
  43. * @return string
  44. */
  45. private static function getBaseFilesFolder()
  46. {
  47. return __DIR__;
  48. }
  49. /**
  50. * Returns base directory for whitelisted files
  51. *
  52. * @return string
  53. */
  54. private static function getChangedFilesBaseDir()
  55. {
  56. return __DIR__ . '/..';
  57. }
  58. /**
  59. * Returns whitelist based on blacklist and git changed files
  60. *
  61. * @param array $fileTypes
  62. * @param string $changedFilesBaseDir
  63. * @param string $baseFilesFolder
  64. * @param string $whitelistFile
  65. * @return array
  66. */
  67. public static function getWhitelist(
  68. $fileTypes = ['php'],
  69. $changedFilesBaseDir = '',
  70. $baseFilesFolder = '',
  71. $whitelistFile = '/_files/whitelist/common.txt'
  72. ) {
  73. $changedFiles = self::getChangedFilesList($changedFilesBaseDir);
  74. if (empty($changedFiles)) {
  75. return [];
  76. }
  77. $globPatternsFolder = ('' !== $baseFilesFolder) ? $baseFilesFolder : self::getBaseFilesFolder();
  78. try {
  79. $directoriesToCheck = Files::init()->readLists($globPatternsFolder . $whitelistFile);
  80. } catch (\Exception $e) {
  81. // no directories matched white list
  82. return [];
  83. }
  84. $targetFiles = self::filterFiles($changedFiles, $fileTypes, $directoriesToCheck);
  85. return $targetFiles;
  86. }
  87. /**
  88. * This method loads list of changed files.
  89. *
  90. * List may be generated by:
  91. * - dev/tests/static/get_github_changes.php utility (allow to generate diffs between branches),
  92. * - CLI command "git diff --name-only > dev/tests/static/testsuite/Magento/Test/_files/changed_files_local.txt",
  93. *
  94. * If no generated changed files list found "git diff" will be used to find not committed changed
  95. * (tests should be invoked from target gir repo).
  96. *
  97. * Note: "static" modifier used for compatibility with legacy implementation of self::getWhitelist method
  98. *
  99. * @param string $changedFilesBaseDir Base dir with previously generated list files
  100. * @return string[] List of changed files
  101. */
  102. private static function getChangedFilesList($changedFilesBaseDir)
  103. {
  104. return self::getFilesFromListFile(
  105. $changedFilesBaseDir,
  106. 'changed_files*',
  107. function () {
  108. // if no list files, probably, this is the dev environment
  109. @exec('git diff --name-only', $changedFiles);
  110. @exec('git diff --cached --name-only', $addedFiles);
  111. $changedFiles = array_unique(array_merge($changedFiles, $addedFiles));
  112. return $changedFiles;
  113. }
  114. );
  115. }
  116. /**
  117. * This method loads list of added files.
  118. *
  119. * @param string $changedFilesBaseDir
  120. * @return string[]
  121. */
  122. private static function getAddedFilesList($changedFilesBaseDir)
  123. {
  124. return self::getFilesFromListFile(
  125. $changedFilesBaseDir,
  126. 'changed_files*.added.*',
  127. function () {
  128. // if no list files, probably, this is the dev environment
  129. @exec('git diff --cached --name-only', $addedFiles);
  130. return $addedFiles;
  131. }
  132. );
  133. }
  134. /**
  135. * Read files from generated lists.
  136. *
  137. * @param string $listsBaseDir
  138. * @param string $listFilePattern
  139. * @param callable $noListCallback
  140. * @return string[]
  141. */
  142. private static function getFilesFromListFile($listsBaseDir, $listFilePattern, $noListCallback)
  143. {
  144. $filesDefinedInList = [];
  145. $globFilesListPattern = ($listsBaseDir ?: self::getChangedFilesBaseDir())
  146. . '/_files/' . $listFilePattern;
  147. $listFiles = glob($globFilesListPattern);
  148. if (count($listFiles)) {
  149. foreach ($listFiles as $listFile) {
  150. $filesDefinedInList = array_merge(
  151. $filesDefinedInList,
  152. file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)
  153. );
  154. }
  155. } else {
  156. $filesDefinedInList = call_user_func($noListCallback);
  157. }
  158. array_walk(
  159. $filesDefinedInList,
  160. function (&$file) {
  161. $file = BP . '/' . $file;
  162. }
  163. );
  164. $filesDefinedInList = array_values(array_unique($filesDefinedInList));
  165. return $filesDefinedInList;
  166. }
  167. /**
  168. * Filter list of files.
  169. *
  170. * File removed from list:
  171. * - if it not exists,
  172. * - if allowed types are specified and file has another type (extension),
  173. * - if allowed directories specified and file not located in one of them.
  174. *
  175. * Note: "static" modifier used for compatibility with legacy implementation of self::getWhitelist method
  176. *
  177. * @param string[] $files List of file paths to filter
  178. * @param string[] $allowedFileTypes List of allowed file extensions (pass empty array to allow all)
  179. * @param string[] $allowedDirectories List of allowed directories (pass empty array to allow all)
  180. * @return string[] Filtered file paths
  181. */
  182. private static function filterFiles(array $files, array $allowedFileTypes, array $allowedDirectories)
  183. {
  184. if (empty($allowedFileTypes)) {
  185. $fileHasAllowedType = function () {
  186. return true;
  187. };
  188. } else {
  189. $fileHasAllowedType = function ($file) use ($allowedFileTypes) {
  190. return in_array(pathinfo($file, PATHINFO_EXTENSION), $allowedFileTypes);
  191. };
  192. }
  193. if (empty($allowedDirectories)) {
  194. $fileIsInAllowedDirectory = function () {
  195. return true;
  196. };
  197. } else {
  198. $allowedDirectories = array_map('realpath', $allowedDirectories);
  199. usort($allowedDirectories, function ($dir1, $dir2) {
  200. return strlen($dir1) - strlen($dir2);
  201. });
  202. $fileIsInAllowedDirectory = function ($file) use ($allowedDirectories) {
  203. foreach ($allowedDirectories as $directory) {
  204. if (strpos($file, $directory) === 0) {
  205. return true;
  206. }
  207. }
  208. return false;
  209. };
  210. }
  211. $filtered = array_filter(
  212. $files,
  213. function ($file) use ($fileHasAllowedType, $fileIsInAllowedDirectory) {
  214. $file = realpath($file);
  215. if (false === $file) {
  216. return false;
  217. }
  218. return $fileHasAllowedType($file) && $fileIsInAllowedDirectory($file);
  219. }
  220. );
  221. return $filtered;
  222. }
  223. /**
  224. * Retrieves full list of codebase paths without any files/folders filtered out
  225. *
  226. * @return array
  227. */
  228. private function getFullWhitelist()
  229. {
  230. try {
  231. return Files::init()->readLists(__DIR__ . '/_files/whitelist/common.txt');
  232. } catch (\Exception $e) {
  233. // nothing is whitelisted
  234. return [];
  235. }
  236. }
  237. /**
  238. * Test code quality using phpcs
  239. */
  240. public function testCodeStyle()
  241. {
  242. $isFullScan = defined('TESTCODESTYLE_IS_FULL_SCAN') && TESTCODESTYLE_IS_FULL_SCAN === '1';
  243. $reportFile = self::$reportDir . '/phpcs_report.txt';
  244. if (!file_exists($reportFile)) {
  245. touch($reportFile);
  246. }
  247. $codeSniffer = new CodeSniffer('Magento', $reportFile, new Wrapper());
  248. $result = $codeSniffer->run($isFullScan ? $this->getFullWhitelist() : self::getWhitelist(['php', 'phtml']));
  249. $report = file_get_contents($reportFile);
  250. $this->assertEquals(
  251. 0,
  252. $result,
  253. "PHP Code Sniffer detected {$result} violation(s): " . PHP_EOL . $report
  254. );
  255. }
  256. /**
  257. * Test code quality using phpmd
  258. */
  259. public function testCodeMess()
  260. {
  261. $reportFile = self::$reportDir . '/phpmd_report.txt';
  262. $codeMessDetector = new CodeMessDetector(realpath(__DIR__ . '/_files/phpmd/ruleset.xml'), $reportFile);
  263. if (!$codeMessDetector->canRun()) {
  264. $this->markTestSkipped('PHP Mess Detector is not available.');
  265. }
  266. $result = $codeMessDetector->run(self::getWhitelist(['php']));
  267. $output = "";
  268. if (file_exists($reportFile)) {
  269. $output = file_get_contents($reportFile);
  270. }
  271. $this->assertEquals(
  272. Command::EXIT_SUCCESS,
  273. $result,
  274. "PHP Code Mess has found error(s):" . PHP_EOL . $output
  275. );
  276. // delete empty reports
  277. if (file_exists($reportFile)) {
  278. unlink($reportFile);
  279. }
  280. }
  281. /**
  282. * Test code quality using phpcpd
  283. */
  284. public function testCopyPaste()
  285. {
  286. $reportFile = self::$reportDir . '/phpcpd_report.xml';
  287. $copyPasteDetector = new CopyPasteDetector($reportFile);
  288. if (!$copyPasteDetector->canRun()) {
  289. $this->markTestSkipped('PHP Copy/Paste Detector is not available.');
  290. }
  291. $blackList = [];
  292. foreach (glob(__DIR__ . '/_files/phpcpd/blacklist/*.txt') as $list) {
  293. $blackList = array_merge($blackList, file($list, FILE_IGNORE_NEW_LINES));
  294. }
  295. $copyPasteDetector->setBlackList($blackList);
  296. $result = $copyPasteDetector->run([BP]);
  297. $output = "";
  298. if (file_exists($reportFile)) {
  299. $output = file_get_contents($reportFile);
  300. }
  301. $this->assertTrue(
  302. $result,
  303. "PHP Copy/Paste Detector has found error(s):" . PHP_EOL . $output
  304. );
  305. }
  306. /**
  307. * Tests whitelisted files for strict type declarations.
  308. */
  309. public function testStrictTypes()
  310. {
  311. $changedFiles = self::getAddedFilesList('');
  312. try {
  313. $blackList = Files::init()->readLists(
  314. self::getBaseFilesFolder() . '/_files/blacklist/strict_type.txt'
  315. );
  316. } catch (\Exception $e) {
  317. // nothing matched black list
  318. $blackList = [];
  319. }
  320. $toBeTestedFiles = array_diff(
  321. self::filterFiles($changedFiles, ['php'], []),
  322. $blackList
  323. );
  324. $filesMissingStrictTyping = [];
  325. foreach ($toBeTestedFiles as $fileName) {
  326. $file = file_get_contents($fileName);
  327. if (strstr($file, 'strict_types=1') === false) {
  328. $filesMissingStrictTyping[] = $fileName;
  329. }
  330. }
  331. $this->assertEquals(
  332. 0,
  333. count($filesMissingStrictTyping),
  334. "Following files are missing strict type declaration:"
  335. . PHP_EOL
  336. . implode(PHP_EOL, $filesMissingStrictTyping)
  337. );
  338. }
  339. }