AbstractIntegrationTestCase.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Tests\Test;
  12. use PhpCsFixer\Cache\NullCacheManager;
  13. use PhpCsFixer\Differ\SebastianBergmannDiffer;
  14. use PhpCsFixer\Error\Error;
  15. use PhpCsFixer\Error\ErrorsManager;
  16. use PhpCsFixer\FileRemoval;
  17. use PhpCsFixer\Fixer\FixerInterface;
  18. use PhpCsFixer\FixerFactory;
  19. use PhpCsFixer\Linter\Linter;
  20. use PhpCsFixer\Linter\LinterInterface;
  21. use PhpCsFixer\Runner\Runner;
  22. use PhpCsFixer\Tests\TestCase;
  23. use PhpCsFixer\Tokenizer\Tokens;
  24. use PhpCsFixer\WhitespacesFixerConfig;
  25. use Prophecy\Argument;
  26. use Symfony\Component\Filesystem\Exception\IOException;
  27. use Symfony\Component\Filesystem\Filesystem;
  28. use Symfony\Component\Finder\Finder;
  29. /**
  30. * Integration test base class.
  31. *
  32. * This test searches for '.test' fixture files in the given directory.
  33. * Each fixture file will be parsed and tested against the expected result.
  34. *
  35. * Fixture files have the following format:
  36. *
  37. * --TEST--
  38. * Example test description.
  39. * --RULESET--
  40. * {"@PSR2": true, "strict": true}
  41. * --CONFIG--*
  42. * {"indent": " ", "lineEnding": "\n"}
  43. * --SETTINGS--*
  44. * {"key": "value"} # optional extension point for custom IntegrationTestCase class
  45. * --REQUIREMENTS--*
  46. * {"php": 50600**}
  47. * --EXPECT--
  48. * Expected code after fixing
  49. * --INPUT--*
  50. * Code to fix
  51. *
  52. * * Section or any line in it may be omitted.
  53. * ** PHP minimum version. Default to current running php version (no effect).
  54. *
  55. * @author SpacePossum
  56. *
  57. * @internal
  58. */
  59. abstract class AbstractIntegrationTestCase extends TestCase
  60. {
  61. /**
  62. * @var LinterInterface
  63. */
  64. protected $linter;
  65. /**
  66. * @var FileRemoval
  67. */
  68. private static $fileRemoval;
  69. public static function setUpBeforeClass()
  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()
  84. {
  85. parent::tearDownAfterClass();
  86. $tmpFile = static::getTempFile();
  87. self::$fileRemoval->delete($tmpFile);
  88. }
  89. protected function setUp()
  90. {
  91. parent::setUp();
  92. $this->linter = $this->getLinter();
  93. // @todo remove at 3.0 together with env var itself
  94. if (getenv('PHP_CS_FIXER_TEST_USE_LEGACY_TOKENIZER')) {
  95. Tokens::setLegacyMode(true);
  96. }
  97. }
  98. protected function tearDown()
  99. {
  100. parent::tearDown();
  101. // @todo remove at 3.0
  102. Tokens::setLegacyMode(false);
  103. }
  104. /**
  105. * @dataProvider provideIntegrationCases
  106. *
  107. * @see doTest()
  108. *
  109. * @param IntegrationCase $case
  110. */
  111. public function testIntegration(IntegrationCase $case)
  112. {
  113. $this->doTest($case);
  114. }
  115. /**
  116. * Creates test data by parsing '.test' files.
  117. *
  118. * @return IntegrationCase[][]
  119. */
  120. public function provideIntegrationCases()
  121. {
  122. $fixturesDir = realpath(static::getFixturesDir());
  123. if (!is_dir($fixturesDir)) {
  124. throw new \UnexpectedValueException(sprintf('Given fixture dir "%s" is not a directory.', $fixturesDir));
  125. }
  126. $factory = static::createIntegrationCaseFactory();
  127. $tests = [];
  128. foreach (Finder::create()->files()->in($fixturesDir) as $file) {
  129. if ('test' !== $file->getExtension()) {
  130. continue;
  131. }
  132. $tests[] = [
  133. $factory->create($file),
  134. ];
  135. }
  136. return $tests;
  137. }
  138. /**
  139. * @return IntegrationCaseFactoryInterface
  140. */
  141. protected static function createIntegrationCaseFactory()
  142. {
  143. return new IntegrationCaseFactory();
  144. }
  145. /**
  146. * Returns the full path to directory which contains the tests.
  147. *
  148. * @return string
  149. */
  150. protected static function getFixturesDir()
  151. {
  152. throw new \BadMethodCallException('Method "getFixturesDir" must be overridden by the extending class.');
  153. }
  154. /**
  155. * Returns the full path to the temporary file where the test will write to.
  156. *
  157. * @return string
  158. */
  159. protected static function getTempFile()
  160. {
  161. throw new \BadMethodCallException('Method "getTempFile" must be overridden by the extending class.');
  162. }
  163. /**
  164. * Applies the given fixers on the input and checks the result.
  165. *
  166. * It will write the input to a temp file. The file will be fixed by a Fixer instance
  167. * configured with the given fixers. The result is compared with the expected output.
  168. * It checks if no errors were reported during the fixing.
  169. *
  170. * @param IntegrationCase $case
  171. */
  172. protected function doTest(IntegrationCase $case)
  173. {
  174. if (PHP_VERSION_ID < $case->getRequirement('php')) {
  175. $this->markTestSkipped(sprintf('PHP %d (or later) is required for "%s", current "%d".', $case->getRequirement('php'), $case->getFileName(), PHP_VERSION_ID));
  176. }
  177. $input = $case->getInputCode();
  178. $expected = $case->getExpectedCode();
  179. $input = $case->hasInputCode() ? $input : $expected;
  180. $tmpFile = static::getTempFile();
  181. if (false === @file_put_contents($tmpFile, $input)) {
  182. throw new IOException(sprintf('Failed to write to tmp. file "%s".', $tmpFile));
  183. }
  184. $errorsManager = new ErrorsManager();
  185. $fixers = static::createFixers($case);
  186. $runner = new Runner(
  187. new \ArrayIterator([new \SplFileInfo($tmpFile)]),
  188. $fixers,
  189. new SebastianBergmannDiffer(),
  190. null,
  191. $errorsManager,
  192. $this->linter,
  193. false,
  194. new NullCacheManager()
  195. );
  196. Tokens::clearCache();
  197. $result = $runner->fix();
  198. $changed = array_pop($result);
  199. if (!$errorsManager->isEmpty()) {
  200. $errors = $errorsManager->getExceptionErrors();
  201. $this->assertEmpty($errors, sprintf('Errors reported during fixing of file "%s": %s', $case->getFileName(), $this->implodeErrors($errors)));
  202. $errors = $errorsManager->getInvalidErrors();
  203. $this->assertEmpty($errors, sprintf('Errors reported during linting before fixing file "%s": %s.', $case->getFileName(), $this->implodeErrors($errors)));
  204. $errors = $errorsManager->getLintErrors();
  205. $this->assertEmpty($errors, sprintf('Errors reported during linting after fixing file "%s": %s.', $case->getFileName(), $this->implodeErrors($errors)));
  206. }
  207. if (!$case->hasInputCode()) {
  208. $this->assertEmpty(
  209. $changed,
  210. sprintf(
  211. "Expected no changes made to test \"%s\" in \"%s\".\nFixers applied:\n%s.\nDiff.:\n%s.",
  212. $case->getTitle(),
  213. $case->getFileName(),
  214. null === $changed ? '[None]' : implode(',', $changed['appliedFixers']),
  215. null === $changed ? '[None]' : $changed['diff']
  216. )
  217. );
  218. return;
  219. }
  220. $this->assertNotEmpty($changed, sprintf('Expected changes made to test "%s" in "%s".', $case->getTitle(), $case->getFileName()));
  221. $fixedInputCode = file_get_contents($tmpFile);
  222. $this->assertThat(
  223. $fixedInputCode,
  224. self::createIsIdenticalStringConstraint($expected),
  225. sprintf(
  226. "Expected changes do not match result for \"%s\" in \"%s\".\nFixers applied:\n%s.",
  227. $case->getTitle(),
  228. $case->getFileName(),
  229. null === $changed ? '[None]' : implode(',', $changed['appliedFixers'])
  230. )
  231. );
  232. if (1 < count($fixers)) {
  233. $tmpFile = static::getTempFile();
  234. if (false === @file_put_contents($tmpFile, $input)) {
  235. throw new IOException(sprintf('Failed to write to tmp. file "%s".', $tmpFile));
  236. }
  237. $runner = new Runner(
  238. new \ArrayIterator([new \SplFileInfo($tmpFile)]),
  239. array_reverse($fixers),
  240. new SebastianBergmannDiffer(),
  241. null,
  242. $errorsManager,
  243. $this->linter,
  244. false,
  245. new NullCacheManager()
  246. );
  247. Tokens::clearCache();
  248. $runner->fix();
  249. $fixedInputCodeWithReversedFixers = file_get_contents($tmpFile);
  250. $this->assertRevertedOrderFixing($case, $fixedInputCode, $fixedInputCodeWithReversedFixers);
  251. }
  252. // run the test again with the `expected` part, this should always stay the same
  253. $this->testIntegration(
  254. new IntegrationCase(
  255. $case->getFileName(),
  256. $case->getTitle().' "--EXPECT-- part run"',
  257. $case->getSettings(),
  258. $case->getRequirements(),
  259. $case->getConfig(),
  260. $case->getRuleset(),
  261. $case->getExpectedCode(),
  262. null
  263. )
  264. );
  265. }
  266. /**
  267. * @param IntegrationCase $case
  268. * @param string $fixedInputCode
  269. * @param string $fixedInputCodeWithReversedFixers
  270. */
  271. protected static function assertRevertedOrderFixing(IntegrationCase $case, $fixedInputCode, $fixedInputCodeWithReversedFixers)
  272. {
  273. // If output is different depends on rules order - we need to verify that the rules are ordered by priority.
  274. // If not, any order is valid.
  275. if ($fixedInputCode !== $fixedInputCodeWithReversedFixers) {
  276. static::assertGreaterThan(
  277. 1,
  278. count(array_unique(array_map(
  279. static function (FixerInterface $fixer) {
  280. return $fixer->getPriority();
  281. },
  282. static::createFixers($case)
  283. ))),
  284. sprintf(
  285. '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".',
  286. $case->getFileName()
  287. )
  288. );
  289. }
  290. }
  291. /**
  292. * @param IntegrationCase $case
  293. *
  294. * @return FixerInterface[]
  295. */
  296. private static function createFixers(IntegrationCase $case)
  297. {
  298. $config = $case->getConfig();
  299. return FixerFactory::create()
  300. ->registerBuiltInFixers()
  301. ->useRuleSet($case->getRuleset())
  302. ->setWhitespacesConfig(
  303. new WhitespacesFixerConfig($config['indent'], $config['lineEnding'])
  304. )
  305. ->getFixers();
  306. }
  307. /**
  308. * @param Error[] $errors
  309. *
  310. * @return string
  311. */
  312. private function implodeErrors(array $errors)
  313. {
  314. $errorStr = '';
  315. foreach ($errors as $error) {
  316. $source = $error->getSource();
  317. $errorStr .= sprintf("%d: %s%s\n", $error->getType(), $error->getFilePath(), null === $source ? '' : ' '.$source->getMessage()."\n\n".$source->getTraceAsString());
  318. }
  319. return $errorStr;
  320. }
  321. /**
  322. * @return LinterInterface
  323. */
  324. private function getLinter()
  325. {
  326. static $linter = null;
  327. if (null === $linter) {
  328. if (getenv('SKIP_LINT_TEST_CASES')) {
  329. $linterProphecy = $this->prophesize(\PhpCsFixer\Linter\LinterInterface::class);
  330. $linterProphecy
  331. ->lintSource(Argument::type('string'))
  332. ->willReturn($this->prophesize(\PhpCsFixer\Linter\LintingResultInterface::class)->reveal());
  333. $linterProphecy
  334. ->lintFile(Argument::type('string'))
  335. ->willReturn($this->prophesize(\PhpCsFixer\Linter\LintingResultInterface::class)->reveal());
  336. $linterProphecy
  337. ->isAsync()
  338. ->willReturn(false);
  339. $linter = $linterProphecy->reveal();
  340. } else {
  341. $linter = new Linter();
  342. }
  343. }
  344. return $linter;
  345. }
  346. /**
  347. * @todo Remove me when this class will end up in dedicated package.
  348. *
  349. * @param string $expected
  350. */
  351. private static function createIsIdenticalStringConstraint($expected)
  352. {
  353. $candidates = array_filter([
  354. 'PhpCsFixer\PhpunitConstraintIsIdenticalString\Constraint\IsIdenticalString',
  355. 'PHPUnit\Framework\Constraint\IsIdentical',
  356. 'PHPUnit_Framework_Constraint_IsIdentical',
  357. ], function ($className) { return class_exists($className); });
  358. if (empty($candidates)) {
  359. throw new \RuntimeException('PHPUnit not installed?!');
  360. }
  361. $candidate = array_shift($candidates);
  362. return new $candidate($expected);
  363. }
  364. }