AbstractIntegrationTestCase.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Tests\Test;
  13. use PhpCsFixer\Cache\NullCacheManager;
  14. use PhpCsFixer\Differ\UnifiedDiffer;
  15. use PhpCsFixer\Error\Error;
  16. use PhpCsFixer\Error\ErrorsManager;
  17. use PhpCsFixer\FileRemoval;
  18. use PhpCsFixer\Fixer\FixerInterface;
  19. use PhpCsFixer\FixerFactory;
  20. use PhpCsFixer\Linter\CachingLinter;
  21. use PhpCsFixer\Linter\Linter;
  22. use PhpCsFixer\Linter\LinterInterface;
  23. use PhpCsFixer\Linter\ProcessLinter;
  24. use PhpCsFixer\PhpunitConstraintIsIdenticalString\Constraint\IsIdenticalString;
  25. use PhpCsFixer\Runner\Runner;
  26. use PhpCsFixer\Tests\TestCase;
  27. use PhpCsFixer\Tokenizer\Tokens;
  28. use PhpCsFixer\WhitespacesFixerConfig;
  29. use Symfony\Component\Filesystem\Exception\IOException;
  30. use Symfony\Component\Filesystem\Filesystem;
  31. use Symfony\Component\Finder\Finder;
  32. use Symfony\Component\Finder\SplFileInfo;
  33. /**
  34. * Integration test base class.
  35. *
  36. * This test searches for '.test' fixture files in the given directory.
  37. * Each fixture file will be parsed and tested against the expected result.
  38. *
  39. * Fixture files have the following format:
  40. *
  41. * --TEST--
  42. * Example test description.
  43. * --RULESET--
  44. * {"@PSR2": true, "strict": true}
  45. * --CONFIG--
  46. * {"indent": " ", "lineEnding": "\n"}
  47. * --SETTINGS--
  48. * {"key": "value"} # optional extension point for custom IntegrationTestCase class
  49. * --REQUIREMENTS--
  50. * {"php": 70400, "php<": 80000}
  51. * --EXPECT--
  52. * Expected code after fixing
  53. * --INPUT--
  54. * Code to fix
  55. *
  56. **************
  57. * IMPORTANT! *
  58. **************
  59. *
  60. * Some sections (like `--CONFIG--`) may be omitted. The required sections are:
  61. * - `--TEST--`
  62. * - `--RULESET--`
  63. * - `--EXPECT--` (works as input too if `--INPUT--` is not provided, that means no changes are expected)
  64. *
  65. * The `--REQUIREMENTS--` section can define additional constraints for running (or not) the test.
  66. * You can use these fields to fine-tune run conditions for test cases:
  67. * - `php` represents minimum PHP version test should be run on. Defaults to current running PHP version (no effect).
  68. * - `php<` represents maximum PHP version test should be run on. Defaults to PHP's maximum integer value (no effect).
  69. * - `os` represents operating system(s) test should be run on. Supported operating systems: Linux, Darwin and Windows.
  70. * By default test is run on all supported operating systems.
  71. *
  72. * @internal
  73. */
  74. abstract class AbstractIntegrationTestCase extends TestCase
  75. {
  76. /**
  77. * @var null|LinterInterface
  78. */
  79. protected $linter;
  80. /**
  81. * @var null|FileRemoval
  82. */
  83. private static $fileRemoval;
  84. public static function setUpBeforeClass(): void
  85. {
  86. parent::setUpBeforeClass();
  87. $tmpFile = static::getTempFile();
  88. self::$fileRemoval = new FileRemoval();
  89. self::$fileRemoval->observe($tmpFile);
  90. if (!is_file($tmpFile)) {
  91. $dir = \dirname($tmpFile);
  92. if (!is_dir($dir)) {
  93. $fs = new Filesystem();
  94. $fs->mkdir($dir, 0766);
  95. }
  96. }
  97. }
  98. public static function tearDownAfterClass(): void
  99. {
  100. parent::tearDownAfterClass();
  101. $tmpFile = static::getTempFile();
  102. self::$fileRemoval->delete($tmpFile);
  103. self::$fileRemoval = null;
  104. }
  105. protected function setUp(): void
  106. {
  107. parent::setUp();
  108. $this->linter = $this->getLinter();
  109. }
  110. protected function tearDown(): void
  111. {
  112. parent::tearDown();
  113. $this->linter = null;
  114. }
  115. /**
  116. * @dataProvider provideIntegrationCases
  117. *
  118. * @see doTest()
  119. *
  120. * @large
  121. *
  122. * @group legacy
  123. */
  124. public function testIntegration(IntegrationCase $case): void
  125. {
  126. foreach ($case->getSettings()['deprecations'] as $deprecation) {
  127. $this->expectDeprecation($deprecation);
  128. }
  129. $this->doTest($case);
  130. // run the test again with the `expected` part, this should always stay the same
  131. $this->doTest(
  132. new IntegrationCase(
  133. $case->getFileName(),
  134. $case->getTitle().' "--EXPECT-- part run"',
  135. $case->getSettings(),
  136. $case->getRequirements(),
  137. $case->getConfig(),
  138. $case->getRuleset(),
  139. $case->getExpectedCode(),
  140. null
  141. )
  142. );
  143. }
  144. /**
  145. * Creates test data by parsing '.test' files.
  146. *
  147. * @return iterable<string, array{IntegrationCase}>
  148. */
  149. public static function provideIntegrationCases(): iterable
  150. {
  151. $dir = static::getFixturesDir();
  152. $fixturesDir = realpath($dir);
  153. if (!is_dir($fixturesDir)) {
  154. throw new \UnexpectedValueException(\sprintf('Given fixture dir "%s" is not a directory.', \is_string($fixturesDir) ? $fixturesDir : $dir));
  155. }
  156. $factory = static::createIntegrationCaseFactory();
  157. /** @var SplFileInfo $file */
  158. foreach (Finder::create()->files()->in($fixturesDir) as $file) {
  159. if ('test' !== $file->getExtension()) {
  160. continue;
  161. }
  162. $relativePath = substr($file->getPathname(), \strlen(realpath(__DIR__.'/../../')) + 1);
  163. yield $relativePath => [$factory->create($file)];
  164. }
  165. }
  166. protected static function createIntegrationCaseFactory(): IntegrationCaseFactoryInterface
  167. {
  168. return new IntegrationCaseFactory();
  169. }
  170. /**
  171. * Returns the full path to directory which contains the tests.
  172. */
  173. protected static function getFixturesDir(): string
  174. {
  175. throw new \BadMethodCallException('Method "getFixturesDir" must be overridden by the extending class.');
  176. }
  177. /**
  178. * Returns the full path to the temporary file where the test will write to.
  179. */
  180. protected static function getTempFile(): string
  181. {
  182. throw new \BadMethodCallException('Method "getTempFile" must be overridden by the extending class.');
  183. }
  184. /**
  185. * Applies the given fixers on the input and checks the result.
  186. *
  187. * It will write the input to a temp file. The file will be fixed by a Fixer instance
  188. * configured with the given fixers. The result is compared with the expected output.
  189. * It checks if no errors were reported during the fixing.
  190. */
  191. protected function doTest(IntegrationCase $case): void
  192. {
  193. $phpLowerLimit = $case->getRequirement('php');
  194. if (\PHP_VERSION_ID < $phpLowerLimit) {
  195. self::markTestSkipped(\sprintf('PHP %d (or later) is required for "%s", current "%d".', $phpLowerLimit, $case->getFileName(), \PHP_VERSION_ID));
  196. }
  197. $phpUpperLimit = $case->getRequirement('php<');
  198. if (\PHP_VERSION_ID >= $phpUpperLimit) {
  199. self::markTestSkipped(\sprintf('PHP lower than %d is required for "%s", current "%d".', $phpUpperLimit, $case->getFileName(), \PHP_VERSION_ID));
  200. }
  201. if (!\in_array(PHP_OS_FAMILY, $case->getRequirement('os'), true)) {
  202. self::markTestSkipped(
  203. \sprintf(
  204. 'Unsupported OS (%s) for "%s", allowed are: %s.',
  205. PHP_OS,
  206. $case->getFileName(),
  207. implode(', ', $case->getRequirement('os'))
  208. )
  209. );
  210. }
  211. $input = $case->getInputCode();
  212. $expected = $case->getExpectedCode();
  213. $input = $case->hasInputCode() ? $input : $expected;
  214. $tmpFile = static::getTempFile();
  215. if (false === @file_put_contents($tmpFile, $input)) {
  216. throw new IOException(\sprintf('Failed to write to tmp. file "%s".', $tmpFile));
  217. }
  218. $errorsManager = new ErrorsManager();
  219. $fixers = self::createFixers($case);
  220. $runner = new Runner(
  221. new \ArrayIterator([new \SplFileInfo($tmpFile)]),
  222. $fixers,
  223. new UnifiedDiffer(),
  224. null,
  225. $errorsManager,
  226. $this->linter,
  227. false,
  228. new NullCacheManager()
  229. );
  230. Tokens::clearCache();
  231. $result = $runner->fix();
  232. $changed = array_pop($result);
  233. if (!$errorsManager->isEmpty()) {
  234. $errors = $errorsManager->getExceptionErrors();
  235. self::assertEmpty($errors, \sprintf('Errors reported during fixing of file "%s": %s', $case->getFileName(), $this->implodeErrors($errors)));
  236. $errors = $errorsManager->getInvalidErrors();
  237. self::assertEmpty($errors, \sprintf('Errors reported during linting before fixing file "%s": %s.', $case->getFileName(), $this->implodeErrors($errors)));
  238. $errors = $errorsManager->getLintErrors();
  239. self::assertEmpty($errors, \sprintf('Errors reported during linting after fixing file "%s": %s.', $case->getFileName(), $this->implodeErrors($errors)));
  240. }
  241. if (!$case->hasInputCode()) {
  242. self::assertEmpty(
  243. $changed,
  244. \sprintf(
  245. "Expected no changes made to test \"%s\" in \"%s\".\nFixers applied:\n%s.\nDiff.:\n%s.",
  246. $case->getTitle(),
  247. $case->getFileName(),
  248. null === $changed ? '[None]' : implode(',', $changed['appliedFixers']),
  249. null === $changed ? '[None]' : $changed['diff']
  250. )
  251. );
  252. return;
  253. }
  254. self::assertNotEmpty($changed, \sprintf('Expected changes made to test "%s" in "%s".', $case->getTitle(), $case->getFileName()));
  255. $fixedInputCode = file_get_contents($tmpFile);
  256. self::assertThat(
  257. $fixedInputCode,
  258. new IsIdenticalString($expected),
  259. \sprintf(
  260. "Expected changes do not match result for \"%s\" in \"%s\".\nFixers applied:\n%s.",
  261. $case->getTitle(),
  262. $case->getFileName(),
  263. implode(',', $changed['appliedFixers'])
  264. )
  265. );
  266. if (1 < \count($fixers)) {
  267. $tmpFile = static::getTempFile();
  268. if (false === @file_put_contents($tmpFile, $input)) {
  269. throw new IOException(\sprintf('Failed to write to tmp. file "%s".', $tmpFile));
  270. }
  271. $runner = new Runner(
  272. new \ArrayIterator([new \SplFileInfo($tmpFile)]),
  273. array_reverse($fixers),
  274. new UnifiedDiffer(),
  275. null,
  276. $errorsManager,
  277. $this->linter,
  278. false,
  279. new NullCacheManager()
  280. );
  281. Tokens::clearCache();
  282. $runner->fix();
  283. $fixedInputCodeWithReversedFixers = file_get_contents($tmpFile);
  284. self::assertRevertedOrderFixing($case, $fixedInputCode, $fixedInputCodeWithReversedFixers);
  285. }
  286. }
  287. protected static function assertRevertedOrderFixing(IntegrationCase $case, string $fixedInputCode, string $fixedInputCodeWithReversedFixers): void
  288. {
  289. // If output is different depends on rules order - we need to verify that the rules are ordered by priority.
  290. // If not, any order is valid.
  291. if ($fixedInputCode !== $fixedInputCodeWithReversedFixers) {
  292. self::assertGreaterThan(
  293. 1,
  294. \count(array_unique(array_map(
  295. static fn (FixerInterface $fixer): int => $fixer->getPriority(),
  296. self::createFixers($case)
  297. ))),
  298. \sprintf(
  299. 'Rules priorities are not differential enough. If rules would be used in reverse order then final output would be different than the expected one. For that, different priorities must be set up for used rules to ensure stable order of them. In "%s".',
  300. $case->getFileName()
  301. )
  302. );
  303. }
  304. }
  305. /**
  306. * @return list<FixerInterface>
  307. */
  308. private static function createFixers(IntegrationCase $case): array
  309. {
  310. $config = $case->getConfig();
  311. return (new FixerFactory())
  312. ->registerBuiltInFixers()
  313. ->useRuleSet($case->getRuleset())
  314. ->setWhitespacesConfig(
  315. new WhitespacesFixerConfig($config['indent'], $config['lineEnding'])
  316. )
  317. ->getFixers()
  318. ;
  319. }
  320. /**
  321. * @param list<Error> $errors
  322. */
  323. private function implodeErrors(array $errors): string
  324. {
  325. $errorStr = '';
  326. foreach ($errors as $error) {
  327. $source = $error->getSource();
  328. $errorStr .= \sprintf("%d: %s%s\n", $error->getType(), $error->getFilePath(), null === $source ? '' : ' '.$source->getMessage()."\n\n".$source->getTraceAsString());
  329. }
  330. return $errorStr;
  331. }
  332. private function getLinter(): LinterInterface
  333. {
  334. static $linter = null;
  335. if (null === $linter) {
  336. $linter = new CachingLinter(
  337. getenv('FAST_LINT_TEST_CASES') ? new Linter() : new ProcessLinter()
  338. );
  339. }
  340. return $linter;
  341. }
  342. }