SelfUpdateCommandTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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\Console\Command;
  13. use org\bovigo\vfs\vfsStream;
  14. use org\bovigo\vfs\vfsStreamDirectory;
  15. use org\bovigo\vfs\vfsStreamException;
  16. use org\bovigo\vfs\vfsStreamWrapper;
  17. use PhpCsFixer\Console\Application;
  18. use PhpCsFixer\Console\Command\SelfUpdateCommand;
  19. use PhpCsFixer\Console\SelfUpdate\GithubClientInterface;
  20. use PhpCsFixer\Console\SelfUpdate\NewVersionChecker;
  21. use PhpCsFixer\Console\SelfUpdate\NewVersionCheckerInterface;
  22. use PhpCsFixer\PharCheckerInterface;
  23. use PhpCsFixer\Tests\TestCase;
  24. use PhpCsFixer\ToolInfoInterface;
  25. use Symfony\Component\Console\Command\Command;
  26. use Symfony\Component\Console\Tester\CommandTester;
  27. /**
  28. * @internal
  29. *
  30. * @covers \PhpCsFixer\Console\Command\SelfUpdateCommand
  31. */
  32. final class SelfUpdateCommandTest extends TestCase
  33. {
  34. /**
  35. * @var null|vfsStreamDirectory
  36. */
  37. private $root;
  38. protected function setUp(): void
  39. {
  40. parent::setUp();
  41. $this->root = vfsStream::setup();
  42. file_put_contents($this->getToolPath(), 'Current PHP CS Fixer.');
  43. file_put_contents($this->root->url().'/'.self::getNewMinorReleaseVersion().'.phar', 'New minor version of PHP CS Fixer.');
  44. file_put_contents($this->root->url().'/'.self::getNewMajorReleaseVersion().'.phar', 'New major version of PHP CS Fixer.');
  45. }
  46. protected function tearDown(): void
  47. {
  48. parent::tearDown();
  49. $this->root = null;
  50. try {
  51. vfsStreamWrapper::unregister();
  52. } catch (vfsStreamException $exception) {
  53. // ignored
  54. }
  55. }
  56. /**
  57. * @dataProvider provideCommandNameCases
  58. */
  59. public function testCommandName(string $name): void
  60. {
  61. $command = new SelfUpdateCommand(
  62. $this->createNewVersionCheckerDouble(),
  63. $this->createToolInfoDouble(),
  64. $this->createPharCheckerDouble(),
  65. );
  66. $application = new Application();
  67. $application->add($command);
  68. self::assertSame($command, $application->find($name));
  69. }
  70. /**
  71. * @return iterable<array{string}>
  72. */
  73. public static function provideCommandNameCases(): iterable
  74. {
  75. yield ['self-update'];
  76. yield ['selfupdate'];
  77. }
  78. /**
  79. * @param array<string, bool|string> $input
  80. *
  81. * @dataProvider provideExecuteCases
  82. */
  83. public function testExecute(
  84. string $latestVersion,
  85. ?string $latestMinorVersion,
  86. array $input,
  87. bool $decorated,
  88. string $expectedFileContents,
  89. string $expectedDisplay
  90. ): void {
  91. $versionChecker = $this->createNewVersionCheckerDouble($latestVersion, $latestMinorVersion);
  92. $command = new SelfUpdateCommand(
  93. $versionChecker,
  94. $this->createToolInfoDouble(),
  95. $this->createPharCheckerDouble(),
  96. );
  97. $commandTester = $this->execute($command, $input, $decorated);
  98. self::assertSame($expectedFileContents, file_get_contents($this->getToolPath()));
  99. self::assertDisplay($expectedDisplay, $commandTester);
  100. self::assertSame(0, $commandTester->getStatusCode());
  101. }
  102. public static function provideExecuteCases(): iterable
  103. {
  104. $currentVersion = Application::VERSION;
  105. $minorRelease = self::getNewMinorReleaseVersion();
  106. $majorRelease = self::getNewMajorReleaseVersion();
  107. $major = self::getNewMajorVersion();
  108. $currentContents = 'Current PHP CS Fixer.';
  109. $minorContents = 'New minor version of PHP CS Fixer.';
  110. $majorContents = 'New major version of PHP CS Fixer.';
  111. $upToDateDisplay = "\033[32mPHP CS Fixer is already up-to-date.\033[39m\n";
  112. $newMinorDisplay = "\033[32mPHP CS Fixer updated\033[39m (\033[33m{$currentVersion}\033[39m -> \033[33m{$minorRelease}\033[39m)\n";
  113. $newMajorDisplay = "\033[32mPHP CS Fixer updated\033[39m (\033[33m{$currentVersion}\033[39m -> \033[33m{$majorRelease}\033[39m)\n";
  114. $majorInfoNoMinorDisplay = <<<OUTPUT
  115. \033[32mA new major version of PHP CS Fixer is available\033[39m (\033[33m{$majorRelease}\033[39m)
  116. \033[32mBefore upgrading please read\033[39m https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/{$majorRelease}/UPGRADE-v{$major}.md
  117. \033[32mIf you are ready to upgrade run this command with\033[39m \033[33m-f\033[39m
  118. \033[32mChecking for new minor/patch version...\033[39m
  119. \033[32mNo minor update for PHP CS Fixer.\033[39m
  120. OUTPUT;
  121. $majorInfoNewMinorDisplay = <<<OUTPUT
  122. \033[32mA new major version of PHP CS Fixer is available\033[39m (\033[33m{$majorRelease}\033[39m)
  123. \033[32mBefore upgrading please read\033[39m https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/{$majorRelease}/UPGRADE-v{$major}.md
  124. \033[32mIf you are ready to upgrade run this command with\033[39m \033[33m-f\033[39m
  125. \033[32mChecking for new minor/patch version...\033[39m
  126. \033[32mPHP CS Fixer updated\033[39m (\033[33m{$currentVersion}\033[39m -> \033[33m{$minorRelease}\033[39m)
  127. OUTPUT;
  128. // no new version available
  129. yield [Application::VERSION, Application::VERSION, [], true, $currentContents, $upToDateDisplay];
  130. yield [Application::VERSION, Application::VERSION, [], false, $currentContents, $upToDateDisplay];
  131. yield [Application::VERSION, Application::VERSION, ['--force' => true], true, $currentContents, $upToDateDisplay];
  132. yield [Application::VERSION, Application::VERSION, ['-f' => true], false, $currentContents, $upToDateDisplay];
  133. // new minor version available
  134. yield [$minorRelease, $minorRelease, [], true, $minorContents, $newMinorDisplay];
  135. yield [$minorRelease, $minorRelease, ['--force' => true], true, $minorContents, $newMinorDisplay];
  136. yield [$minorRelease, $minorRelease, ['-f' => true], true, $minorContents, $newMinorDisplay];
  137. yield [$minorRelease, $minorRelease, [], false, $minorContents, $newMinorDisplay];
  138. yield [$minorRelease, $minorRelease, ['--force' => true], false, $minorContents, $newMinorDisplay];
  139. yield [$minorRelease, $minorRelease, ['-f' => true], false, $minorContents, $newMinorDisplay];
  140. // new major version available
  141. yield [$majorRelease, Application::VERSION, [], true, $currentContents, $majorInfoNoMinorDisplay];
  142. yield [$majorRelease, Application::VERSION, [], false, $currentContents, $majorInfoNoMinorDisplay];
  143. yield [$majorRelease, Application::VERSION, ['--force' => true], true, $majorContents, $newMajorDisplay];
  144. yield [$majorRelease, Application::VERSION, ['-f' => true], false, $majorContents, $newMajorDisplay];
  145. // new minor version and new major version available
  146. yield [$majorRelease, $minorRelease, [], true, $minorContents, $majorInfoNewMinorDisplay];
  147. yield [$majorRelease, $minorRelease, [], false, $minorContents, $majorInfoNewMinorDisplay];
  148. yield [$majorRelease, $minorRelease, ['--force' => true], true, $majorContents, $newMajorDisplay];
  149. yield [$majorRelease, $minorRelease, ['-f' => true], false, $majorContents, $newMajorDisplay];
  150. // weird/unexpected versions
  151. yield ['v0.1.0', 'v0.1.0', [], true, $currentContents, $upToDateDisplay];
  152. yield ['v0.1.0', 'v0.1.0', [], false, $currentContents, $upToDateDisplay];
  153. yield ['v0.1.0', 'v0.1.0', ['--force' => true], true, $currentContents, $upToDateDisplay];
  154. yield ['v0.1.0', 'v0.1.0', ['-f' => true], false, $currentContents, $upToDateDisplay];
  155. yield ['v0.1.0', null, [], true, $currentContents, $upToDateDisplay];
  156. yield ['v0.1.0', null, [], false, $currentContents, $upToDateDisplay];
  157. yield ['v0.1.0', null, ['--force' => true], true, $currentContents, $upToDateDisplay];
  158. yield ['v0.1.0', null, ['-f' => true], false, $currentContents, $upToDateDisplay];
  159. yield ['v0.1.0', Application::VERSION, [], true, $currentContents, $upToDateDisplay];
  160. yield ['v0.1.0', Application::VERSION, [], false, $currentContents, $upToDateDisplay];
  161. yield ['v0.1.0', Application::VERSION, ['--force' => true], true, $currentContents, $upToDateDisplay];
  162. yield ['v0.1.0', Application::VERSION, ['-f' => true], false, $currentContents, $upToDateDisplay];
  163. yield [Application::VERSION, 'v0.1.0', [], true, $currentContents, $upToDateDisplay];
  164. yield [Application::VERSION, 'v0.1.0', [], false, $currentContents, $upToDateDisplay];
  165. yield [Application::VERSION, 'v0.1.0', ['--force' => true], true, $currentContents, $upToDateDisplay];
  166. yield [Application::VERSION, 'v0.1.0', ['-f' => true], false, $currentContents, $upToDateDisplay];
  167. }
  168. /**
  169. * @param array<string, bool|string> $input
  170. *
  171. * @dataProvider provideExecuteWhenNotAbleToGetLatestVersionsCases
  172. */
  173. public function testExecuteWhenNotAbleToGetLatestVersions(
  174. bool $latestMajorVersionSuccess,
  175. bool $latestMinorVersionSuccess,
  176. array $input,
  177. bool $decorated
  178. ): void {
  179. $versionChecker = $this->createNewVersionCheckerDouble(
  180. self::getNewMajorReleaseVersion(),
  181. self::getNewMinorReleaseVersion(),
  182. $latestMajorVersionSuccess,
  183. $latestMinorVersionSuccess,
  184. );
  185. $command = new SelfUpdateCommand(
  186. $versionChecker,
  187. $this->createToolInfoDouble(),
  188. $this->createPharCheckerDouble(),
  189. );
  190. $commandTester = $this->execute($command, $input, $decorated);
  191. self::assertDisplay(
  192. "\033[37;41mUnable to determine newest version: Foo.\033[39;49m\n",
  193. $commandTester
  194. );
  195. self::assertSame(1, $commandTester->getStatusCode());
  196. }
  197. public static function provideExecuteWhenNotAbleToGetLatestVersionsCases(): iterable
  198. {
  199. yield [false, false, [], true];
  200. yield [false, false, ['--force' => true], true];
  201. yield [false, false, ['-f' => true], true];
  202. yield [false, false, [], false];
  203. yield [false, false, ['--force' => true], false];
  204. yield [false, false, ['-f' => true], false];
  205. yield [true, false, [], true];
  206. yield [true, false, ['--force' => true], true];
  207. yield [true, false, ['-f' => true], true];
  208. yield [true, false, [], false];
  209. yield [true, false, ['--force' => true], false];
  210. yield [true, false, ['-f' => true], false];
  211. yield [false, true, [], true];
  212. yield [false, true, ['--force' => true], true];
  213. yield [false, true, ['-f' => true], true];
  214. yield [false, true, [], false];
  215. yield [false, true, ['--force' => true], false];
  216. yield [false, true, ['-f' => true], false];
  217. }
  218. /**
  219. * @param array<string, bool|string> $input
  220. *
  221. * @dataProvider provideExecuteWhenNotInstalledAsPharCases
  222. */
  223. public function testExecuteWhenNotInstalledAsPhar(array $input, bool $decorated): void
  224. {
  225. $command = new SelfUpdateCommand(
  226. $this->createNewVersionCheckerDouble(),
  227. $this->createToolInfoDouble(false),
  228. $this->createPharCheckerDouble(),
  229. );
  230. $commandTester = $this->execute($command, $input, $decorated);
  231. self::assertDisplay(
  232. "\033[37;41mSelf-update is available only for PHAR version.\033[39;49m\n",
  233. $commandTester
  234. );
  235. self::assertSame(1, $commandTester->getStatusCode());
  236. }
  237. public static function provideExecuteWhenNotInstalledAsPharCases(): iterable
  238. {
  239. yield [[], true];
  240. yield [['--force' => true], true];
  241. yield [['-f' => true], true];
  242. yield [[], false];
  243. yield [['--force' => true], false];
  244. yield [['-f' => true], false];
  245. }
  246. /**
  247. * @param array<string, bool|string> $input
  248. */
  249. private function execute(Command $command, array $input, bool $decorated): CommandTester
  250. {
  251. $application = new Application();
  252. $application->add($command);
  253. $input = ['command' => $command->getName()] + $input;
  254. $commandTester = new CommandTester($command);
  255. $realPath = $_SERVER['argv'][0];
  256. $_SERVER['argv'][0] = $this->getToolPath();
  257. $commandTester->execute($input, ['decorated' => $decorated]);
  258. $_SERVER['argv'][0] = $realPath;
  259. return $commandTester;
  260. }
  261. private static function assertDisplay(string $expectedDisplay, CommandTester $commandTester): void
  262. {
  263. if (!$commandTester->getOutput()->isDecorated()) {
  264. $expectedDisplay = preg_replace("/\033\\[(\\d+;)*\\d+m/", '', $expectedDisplay);
  265. }
  266. self::assertSame(
  267. $expectedDisplay,
  268. $commandTester->getDisplay(true)
  269. );
  270. }
  271. private function createToolInfoDouble(bool $isInstalledAsPhar = true): ToolInfoInterface
  272. {
  273. return new class($this->root, $isInstalledAsPhar) implements ToolInfoInterface {
  274. private vfsStreamDirectory $directory;
  275. private bool $isInstalledAsPhar;
  276. public function __construct(vfsStreamDirectory $directory, bool $isInstalledAsPhar)
  277. {
  278. $this->directory = $directory;
  279. $this->isInstalledAsPhar = $isInstalledAsPhar;
  280. }
  281. public function getComposerInstallationDetails(): array
  282. {
  283. throw new \LogicException('Not implemented.');
  284. }
  285. public function getComposerVersion(): string
  286. {
  287. throw new \LogicException('Not implemented.');
  288. }
  289. public function getVersion(): string
  290. {
  291. throw new \LogicException('Not implemented.');
  292. }
  293. public function isInstalledAsPhar(): bool
  294. {
  295. return $this->isInstalledAsPhar;
  296. }
  297. public function isInstalledByComposer(): bool
  298. {
  299. throw new \LogicException('Not implemented.');
  300. }
  301. public function isRunInsideDocker(): bool
  302. {
  303. return false;
  304. }
  305. public function getPharDownloadUri(string $version): string
  306. {
  307. return \sprintf('%s/%s.phar', $this->directory->url(), $version);
  308. }
  309. };
  310. }
  311. private function getToolPath(): string
  312. {
  313. return "{$this->root->url()}/php-cs-fixer";
  314. }
  315. private static function getCurrentMajorVersion(): int
  316. {
  317. return (int) preg_replace('/^v?(\d+).*$/', '$1', Application::VERSION);
  318. }
  319. private static function getNewMinorReleaseVersion(): string
  320. {
  321. return self::getCurrentMajorVersion().'.999.0';
  322. }
  323. private static function getNewMajorVersion(): int
  324. {
  325. return self::getCurrentMajorVersion() + 1;
  326. }
  327. private static function getNewMajorReleaseVersion(): string
  328. {
  329. return self::getNewMajorVersion().'.0.0';
  330. }
  331. private function createNewVersionCheckerDouble(
  332. string $latestVersion = Application::VERSION,
  333. ?string $latestMinorVersion = Application::VERSION,
  334. bool $latestMajorVersionSuccess = true,
  335. bool $latestMinorVersionSuccess = true
  336. ): NewVersionCheckerInterface {
  337. return new class($latestVersion, $latestMinorVersion, $latestMajorVersionSuccess, $latestMinorVersionSuccess) implements NewVersionCheckerInterface {
  338. private string $latestVersion;
  339. private ?string $latestMinorVersion;
  340. private bool $latestMajorVersionSuccess;
  341. private bool $latestMinorVersionSuccess;
  342. public function __construct(
  343. string $latestVersion,
  344. ?string $latestMinorVersion,
  345. bool $latestMajorVersionSuccess = true,
  346. bool $latestMinorVersionSuccess = true
  347. ) {
  348. $this->latestVersion = $latestVersion;
  349. $this->latestMinorVersion = $latestMinorVersion;
  350. $this->latestMajorVersionSuccess = $latestMajorVersionSuccess;
  351. $this->latestMinorVersionSuccess = $latestMinorVersionSuccess;
  352. }
  353. public function getLatestVersion(): string
  354. {
  355. if ($this->latestMajorVersionSuccess) {
  356. return $this->latestVersion;
  357. }
  358. throw new \RuntimeException('Foo.');
  359. }
  360. public function getLatestVersionOfMajor(int $majorVersion): ?string
  361. {
  362. TestCase::assertSame((int) preg_replace('/^v?(\d+).*$/', '$1', Application::VERSION), $majorVersion);
  363. if ($this->latestMinorVersionSuccess) {
  364. return $this->latestMinorVersion;
  365. }
  366. throw new \RuntimeException('Foo.');
  367. }
  368. public function compareVersions(string $versionA, string $versionB): int
  369. {
  370. return (new NewVersionChecker(
  371. new class implements GithubClientInterface {
  372. public function getTags(): array
  373. {
  374. throw new \LogicException('Not implemented.');
  375. }
  376. }
  377. ))->compareVersions($versionA, $versionB);
  378. }
  379. };
  380. }
  381. private function createPharCheckerDouble(): PharCheckerInterface
  382. {
  383. return new class implements PharCheckerInterface {
  384. public function checkFileValidity(string $filename): ?string
  385. {
  386. return null;
  387. }
  388. };
  389. }
  390. }