AbstractIntegrationTestCase.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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. * --EXPECT--
  50. * Expected code after fixing
  51. * --INPUT--*
  52. * Code to fix
  53. *
  54. * * Section or any line in it may be omitted.
  55. * ** PHP minimum version. Default to current running php version (no effect).
  56. *
  57. * @internal
  58. */
  59. abstract class AbstractIntegrationTestCase extends TestCase
  60. {
  61. /**
  62. * @var null|LinterInterface
  63. */
  64. protected $linter;
  65. /**
  66. * @var null|FileRemoval
  67. */
  68. private static $fileRemoval;
  69. public static function setUpBeforeClass(): void
  70. {
  71. parent::setUpBeforeClass();
  72. $tmpFile = static::getTempFile();
  73. self::$fileRemoval = new FileRemoval();
  74. self::$fileRemoval->observe($tmpFile);
  75. if (!is_file($tmpFile)) {
  76. $dir = \dirname($tmpFile);
  77. if (!is_dir($dir)) {
  78. $fs = new Filesystem();
  79. $fs->mkdir($dir, 0766);
  80. }
  81. }
  82. }
  83. public static function tearDownAfterClass(): void
  84. {
  85. parent::tearDownAfterClass();
  86. $tmpFile = static::getTempFile();
  87. self::$fileRemoval->delete($tmpFile);
  88. self::$fileRemoval = null;
  89. }
  90. protected function setUp(): void
  91. {
  92. parent::setUp();
  93. $this->linter = $this->getLinter();
  94. }
  95. protected function tearDown(): void
  96. {
  97. parent::tearDown();
  98. $this->linter = null;
  99. }
  100. /**
  101. * @dataProvider provideIntegrationCases
  102. *
  103. * @see doTest()
  104. *
  105. * @large
  106. *
  107. * @group legacy
  108. */
  109. public function testIntegration(IntegrationCase $case): void
  110. {
  111. foreach ($case->getSettings()['deprecations'] as $deprecation) {
  112. $this->expectDeprecation($deprecation);
  113. }
  114. $this->doTest($case);
  115. }
  116. /**
  117. * Creates test data by parsing '.test' files.
  118. *
  119. * @return IntegrationCase[][]
  120. */
  121. public static function provideIntegrationCases(): array
  122. {
  123. $dir = static::getFixturesDir();
  124. $fixturesDir = realpath($dir);
  125. if (!is_dir($fixturesDir)) {
  126. throw new \UnexpectedValueException(sprintf('Given fixture dir "%s" is not a directory.', \is_string($fixturesDir) ? $fixturesDir : $dir));
  127. }
  128. $factory = static::createIntegrationCaseFactory();
  129. $tests = [];
  130. /** @var SplFileInfo $file */
  131. foreach (Finder::create()->files()->in($fixturesDir) as $file) {
  132. if ('test' !== $file->getExtension()) {
  133. continue;
  134. }
  135. $tests[$file->getPathname()] = [
  136. $factory->create($file),
  137. ];
  138. }
  139. return $tests;
  140. }
  141. protected static function createIntegrationCaseFactory(): IntegrationCaseFactoryInterface
  142. {
  143. return new IntegrationCaseFactory();
  144. }
  145. /**
  146. * Returns the full path to directory which contains the tests.
  147. */
  148. protected static function getFixturesDir(): string
  149. {
  150. throw new \BadMethodCallException('Method "getFixturesDir" must be overridden by the extending class.');
  151. }
  152. /**
  153. * Returns the full path to the temporary file where the test will write to.
  154. */
  155. protected static function getTempFile(): string
  156. {
  157. throw new \BadMethodCallException('Method "getTempFile" must be overridden by the extending class.');
  158. }
  159. /**
  160. * Applies the given fixers on the input and checks the result.
  161. *
  162. * It will write the input to a temp file. The file will be fixed by a Fixer instance
  163. * configured with the given fixers. The result is compared with the expected output.
  164. * It checks if no errors were reported during the fixing.
  165. */
  166. protected function doTest(IntegrationCase $case): void
  167. {
  168. if (\PHP_VERSION_ID < $case->getRequirement('php')) {
  169. static::markTestSkipped(sprintf('PHP %d (or later) is required for "%s", current "%d".', $case->getRequirement('php'), $case->getFileName(), \PHP_VERSION_ID));
  170. }
  171. $input = $case->getInputCode();
  172. $expected = $case->getExpectedCode();
  173. $input = $case->hasInputCode() ? $input : $expected;
  174. $tmpFile = static::getTempFile();
  175. if (false === @file_put_contents($tmpFile, $input)) {
  176. throw new IOException(sprintf('Failed to write to tmp. file "%s".', $tmpFile));
  177. }
  178. $errorsManager = new ErrorsManager();
  179. $fixers = static::createFixers($case);
  180. $runner = new Runner(
  181. new \ArrayIterator([new \SplFileInfo($tmpFile)]),
  182. $fixers,
  183. new UnifiedDiffer(),
  184. null,
  185. $errorsManager,
  186. $this->linter,
  187. false,
  188. new NullCacheManager()
  189. );
  190. Tokens::clearCache();
  191. $result = $runner->fix();
  192. $changed = array_pop($result);
  193. if (!$errorsManager->isEmpty()) {
  194. $errors = $errorsManager->getExceptionErrors();
  195. static::assertEmpty($errors, sprintf('Errors reported during fixing of file "%s": %s', $case->getFileName(), $this->implodeErrors($errors)));
  196. $errors = $errorsManager->getInvalidErrors();
  197. static::assertEmpty($errors, sprintf('Errors reported during linting before fixing file "%s": %s.', $case->getFileName(), $this->implodeErrors($errors)));
  198. $errors = $errorsManager->getLintErrors();
  199. static::assertEmpty($errors, sprintf('Errors reported during linting after fixing file "%s": %s.', $case->getFileName(), $this->implodeErrors($errors)));
  200. }
  201. if (!$case->hasInputCode()) {
  202. static::assertEmpty(
  203. $changed,
  204. sprintf(
  205. "Expected no changes made to test \"%s\" in \"%s\".\nFixers applied:\n%s.\nDiff.:\n%s.",
  206. $case->getTitle(),
  207. $case->getFileName(),
  208. null === $changed ? '[None]' : implode(',', $changed['appliedFixers']),
  209. null === $changed ? '[None]' : $changed['diff']
  210. )
  211. );
  212. return;
  213. }
  214. static::assertNotEmpty($changed, sprintf('Expected changes made to test "%s" in "%s".', $case->getTitle(), $case->getFileName()));
  215. $fixedInputCode = file_get_contents($tmpFile);
  216. static::assertThat(
  217. $fixedInputCode,
  218. new IsIdenticalString($expected),
  219. sprintf(
  220. "Expected changes do not match result for \"%s\" in \"%s\".\nFixers applied:\n%s.",
  221. $case->getTitle(),
  222. $case->getFileName(),
  223. implode(',', $changed['appliedFixers'])
  224. )
  225. );
  226. if (1 < \count($fixers)) {
  227. $tmpFile = static::getTempFile();
  228. if (false === @file_put_contents($tmpFile, $input)) {
  229. throw new IOException(sprintf('Failed to write to tmp. file "%s".', $tmpFile));
  230. }
  231. $runner = new Runner(
  232. new \ArrayIterator([new \SplFileInfo($tmpFile)]),
  233. array_reverse($fixers),
  234. new UnifiedDiffer(),
  235. null,
  236. $errorsManager,
  237. $this->linter,
  238. false,
  239. new NullCacheManager()
  240. );
  241. Tokens::clearCache();
  242. $runner->fix();
  243. $fixedInputCodeWithReversedFixers = file_get_contents($tmpFile);
  244. static::assertRevertedOrderFixing($case, $fixedInputCode, $fixedInputCodeWithReversedFixers);
  245. }
  246. // run the test again with the `expected` part, this should always stay the same
  247. $this->testIntegration(
  248. new IntegrationCase(
  249. $case->getFileName(),
  250. $case->getTitle().' "--EXPECT-- part run"',
  251. $case->getSettings(),
  252. $case->getRequirements(),
  253. $case->getConfig(),
  254. $case->getRuleset(),
  255. $case->getExpectedCode(),
  256. null
  257. )
  258. );
  259. }
  260. protected static function assertRevertedOrderFixing(IntegrationCase $case, string $fixedInputCode, string $fixedInputCodeWithReversedFixers): void
  261. {
  262. // If output is different depends on rules order - we need to verify that the rules are ordered by priority.
  263. // If not, any order is valid.
  264. if ($fixedInputCode !== $fixedInputCodeWithReversedFixers) {
  265. static::assertGreaterThan(
  266. 1,
  267. \count(array_unique(array_map(
  268. static function (FixerInterface $fixer): int {
  269. return $fixer->getPriority();
  270. },
  271. static::createFixers($case)
  272. ))),
  273. sprintf(
  274. '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".',
  275. $case->getFileName()
  276. )
  277. );
  278. }
  279. }
  280. /**
  281. * @return FixerInterface[]
  282. */
  283. private static function createFixers(IntegrationCase $case): array
  284. {
  285. $config = $case->getConfig();
  286. return (new FixerFactory())
  287. ->registerBuiltInFixers()
  288. ->useRuleSet($case->getRuleset())
  289. ->setWhitespacesConfig(
  290. new WhitespacesFixerConfig($config['indent'], $config['lineEnding'])
  291. )
  292. ->getFixers()
  293. ;
  294. }
  295. /**
  296. * @param Error[] $errors
  297. */
  298. private function implodeErrors(array $errors): string
  299. {
  300. $errorStr = '';
  301. foreach ($errors as $error) {
  302. $source = $error->getSource();
  303. $errorStr .= sprintf("%d: %s%s\n", $error->getType(), $error->getFilePath(), null === $source ? '' : ' '.$source->getMessage()."\n\n".$source->getTraceAsString());
  304. }
  305. return $errorStr;
  306. }
  307. private function getLinter(): LinterInterface
  308. {
  309. static $linter = null;
  310. if (null === $linter) {
  311. $linter = new CachingLinter(
  312. getenv('FAST_LINT_TEST_CASES') ? new Linter() : new ProcessLinter()
  313. );
  314. }
  315. return $linter;
  316. }
  317. }