diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3374d5f5..cef582e2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -432,12 +432,6 @@ parameters: count: 1 path: src/Platform/TargetPlatform.php - - - message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, Closure\(array\)\: non\-empty\-array given\.$#' - identifier: argument.type - count: 1 - path: src/SelfManage/Update/FetchPieReleaseFromGitHub.php - - message: '#^Dead catch \- Php\\Pie\\SelfManage\\Verify\\GithubCliNotAvailable is never thrown in the try block\.$#' identifier: catch.neverThrown @@ -675,7 +669,7 @@ parameters: - message: '#^Parameter \#4 \$body of class Composer\\Util\\Http\\Response constructor expects string\|null, string\|false given\.$#' identifier: argument.type - count: 2 + count: 1 path: test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php - diff --git a/resources/pie-settings-schema.json b/resources/pie-settings-schema.json new file mode 100644 index 00000000..8e2fbd0b --- /dev/null +++ b/resources/pie-settings-schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/php/pie/main/resources/pie-config-schema.json", + "title": "PIE configuration file schema", + "description": "Schema for PIE tool configuration file", + "type": "object", + "properties": { + "channel": { + "type": "string", + "description": "Which update channel to use when running self-update", + "enum": [ + "nightly", + "preview", + "stable" + ] + } + } +} diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index 9a89148e..8cdf0e9c 100644 --- a/src/Command/SelfUpdateCommand.php +++ b/src/Command/SelfUpdateCommand.php @@ -4,7 +4,6 @@ namespace Php\Pie\Command; -use Composer\Semver\Semver; use Composer\Util\AuthHelper; use Composer\Util\HttpDownloader; use Php\Pie\ComposerIntegration\PieComposerFactory; @@ -12,11 +11,14 @@ use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\File\FullPathToSelf; use Php\Pie\File\SudoFilePut; +use Php\Pie\Platform; +use Php\Pie\SelfManage\Update\Channel; use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub; -use Php\Pie\SelfManage\Update\PiePharMissingFromLatestRelease; +use Php\Pie\SelfManage\Update\ReleaseIsNewer; use Php\Pie\SelfManage\Update\ReleaseMetadata; use Php\Pie\SelfManage\Verify\FailedToVerifyRelease; use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation; +use Php\Pie\Settings; use Php\Pie\Util\Emoji; use Php\Pie\Util\PieVersion; use Psr\Container\ContainerInterface; @@ -25,9 +27,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; use function file_get_contents; -use function preg_match; use function sprintf; use function unlink; @@ -37,6 +39,8 @@ )] final class SelfUpdateCommand extends Command { + private const OPTION_STABLE_UPDATE = 'stable'; + private const OPTION_PREVIEW_UPDATE = 'preview'; private const OPTION_NIGHTLY_UPDATE = 'nightly'; /** @param non-empty-string $githubApiBaseUrl */ @@ -60,6 +64,18 @@ public function configure(): void InputOption::VALUE_NONE, 'Update to the latest nightly version.', ); + $this->addOption( + self::OPTION_PREVIEW_UPDATE, + null, + InputOption::VALUE_NONE, + 'Update to the latest preview version.', + ); + $this->addOption( + self::OPTION_STABLE_UPDATE, + null, + InputOption::VALUE_NONE, + 'Update to the latest stable version.', + ); } public function execute(InputInterface $input, OutputInterface $output): int @@ -70,6 +86,22 @@ public function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } + $settings = new Settings(Platform::getPieBaseWorkingDirectory()); + $updateChannel = $settings->updateChannel(); + + if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) { + $settings->changeUpdateChannel(Channel::Nightly); + $updateChannel = Channel::Nightly; + } elseif ($input->hasOption(self::OPTION_PREVIEW_UPDATE) && $input->getOption(self::OPTION_PREVIEW_UPDATE)) { + $settings->changeUpdateChannel(Channel::Preview); + $updateChannel = Channel::Preview; + } elseif ($input->hasOption(self::OPTION_STABLE_UPDATE) && $input->getOption(self::OPTION_STABLE_UPDATE)) { + $settings->changeUpdateChannel(Channel::Stable); + $updateChannel = Channel::Stable; + } + + $output->writeln(sprintf('Updating using the %s channel.', $updateChannel->value)); + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); $composer = PieComposerFactory::createPieComposer( @@ -85,7 +117,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $fetchLatestPieRelease = new FetchPieReleaseFromGitHub($this->githubApiBaseUrl, $httpDownloader, $authHelper); $verifyPiePhar = VerifyPieReleaseUsingAttestation::factory(); - if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) { + if ($updateChannel === Channel::Nightly) { $latestRelease = new ReleaseMetadata( 'nightly', 'https://php.github.io/pie/pie-nightly.phar', @@ -94,28 +126,22 @@ public function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Downloading the latest nightly release.'); } else { try { - $latestRelease = $fetchLatestPieRelease->latestReleaseMetadata(); - } catch (PiePharMissingFromLatestRelease $piePharMissingFromLatestRelease) { - $output->writeln(sprintf('%s', $piePharMissingFromLatestRelease->getMessage())); + $latestRelease = $fetchLatestPieRelease->latestReleaseMetadata($updateChannel); + } catch (Throwable $throwable) { + $output->writeln(sprintf('%s', $throwable->getMessage())); return Command::FAILURE; } $pieVersion = PieVersion::get(); - if (preg_match('/^(?.+)@(?[a-f0-9]{7})$/', $pieVersion, $matches)) { - // Have to change the version to something the Semver library understands - $pieVersion = sprintf('dev-main#%s', $matches['hash']); - $output->writeln(sprintf( - 'It looks like you are running a nightly build; if you want to get the newest nightly, specify the --%s flag.', - self::OPTION_NIGHTLY_UPDATE, - )); - } - $output->writeln(sprintf('You are currently running PIE version %s', $pieVersion)); - if (! Semver::satisfies($latestRelease->tag, '> ' . $pieVersion)) { - $output->writeln('You already have the latest version 😍'); + if (! ReleaseIsNewer::forChannel($updateChannel, $pieVersion, $latestRelease)) { + $output->writeln(sprintf( + 'You already have the latest version for the %s channel 😍', + $updateChannel->value, + )); return Command::SUCCESS; } diff --git a/src/Platform.php b/src/Platform.php index 5ef0f6fc..90d50caf 100644 --- a/src/Platform.php +++ b/src/Platform.php @@ -55,29 +55,11 @@ private static function getUserDir(): string return rtrim(strtr($home, '\\', '/'), '/'); } - /** - * This is essentially a Composer-controlled `vendor` directory that has downloaded sources - * - * @throws RuntimeException - */ - public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): string + public static function getPieBaseWorkingDirectory(): string { - // Simple hash of the target platform so we can build against different PHP installs on the same system - $targetPlatformPath = DIRECTORY_SEPARATOR . 'php' . $targetPlatform->phpBinaryPath->majorMinorVersion() . '_' . md5(implode( - '|', - [ - $targetPlatform->operatingSystem->name, - $targetPlatform->phpBinaryPath->phpBinaryPath, - $targetPlatform->phpBinaryPath->version(), - $targetPlatform->architecture->name, - $targetPlatform->threadSafety->name, - $targetPlatform->windowsCompiler?->name ?? 'x', - ], - )); - $home = ComposerPlatform::getEnv('PIE_WORKING_DIRECTORY'); if ($home !== false && $home !== '') { - return $home . $targetPlatformPath; + return $home; } if (ComposerPlatform::isWindows()) { @@ -86,7 +68,7 @@ public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): s throw new RuntimeException('The APPDATA or PIE_WORKING_DIRECTORY environment variable must be set for PIE to run correctly'); } - return rtrim(strtr($appData, '\\', '/'), '/') . '/PIE' . $targetPlatformPath . '/'; + return rtrim(strtr($appData, '\\', '/'), '/') . '/PIE'; } $userDir = self::getUserDir(); @@ -107,12 +89,35 @@ public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): s // select first dir which exists of: $XDG_CONFIG_HOME/pie or ~/.pie foreach ($dirs as $dir) { if (Silencer::call('is_dir', $dir)) { - return $dir . $targetPlatformPath; + return $dir; } } // if none exists, we default to first defined one (XDG one if system uses it, or ~/.pie otherwise) - return $dirs[0] . $targetPlatformPath; + return $dirs[0]; + } + + /** + * This is essentially a Composer-controlled `vendor` directory that has downloaded sources + * + * @throws RuntimeException + */ + public static function getPieWorkingDirectory(TargetPlatform $targetPlatform): string + { + // Simple hash of the target platform so we can build against different PHP installs on the same system + $targetPlatformPath = DIRECTORY_SEPARATOR . 'php' . $targetPlatform->phpBinaryPath->majorMinorVersion() . '_' . md5(implode( + '|', + [ + $targetPlatform->operatingSystem->name, + $targetPlatform->phpBinaryPath->phpBinaryPath, + $targetPlatform->phpBinaryPath->version(), + $targetPlatform->architecture->name, + $targetPlatform->threadSafety->name, + $targetPlatform->windowsCompiler?->name ?? 'x', + ], + )); + + return self::getPieBaseWorkingDirectory() . $targetPlatformPath; } /** @return non-empty-string */ diff --git a/src/SelfManage/Update/Channel.php b/src/SelfManage/Update/Channel.php new file mode 100644 index 00000000..8447f0b1 --- /dev/null +++ b/src/SelfManage/Update/Channel.php @@ -0,0 +1,12 @@ +githubApiBaseUrl . self::PIE_LATEST_RELEASE_URL; + $url = $this->githubApiBaseUrl . self::PIE_RELEASES_URL; $decodedResponse = $this->httpDownloader->get( $url, @@ -46,40 +47,67 @@ public function latestReleaseMetadata(): ReleaseMetadata ], )->decodeJson(); - Assert::isArray($decodedResponse); - Assert::keyExists($decodedResponse, 'tag_name'); - Assert::stringNotEmpty($decodedResponse['tag_name']); - Assert::keyExists($decodedResponse, 'assets'); - Assert::isList($decodedResponse['assets']); + Assert::isList($decodedResponse); + Assert::allIsArray($decodedResponse); - $assetsNamedPiePhar = array_filter( + $releases = array_filter( array_map( - /** @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} */ - static function (array $asset): array { - Assert::keyExists($asset, 'name'); - Assert::stringNotEmpty($asset['name']); - Assert::keyExists($asset, 'browser_download_url'); - Assert::stringNotEmpty($asset['browser_download_url']); - - return $asset; + static function (array $releaseResponse): ReleaseMetadata|null { + Assert::keyExists($releaseResponse, 'tag_name'); + Assert::stringNotEmpty($releaseResponse['tag_name']); + Assert::keyExists($releaseResponse, 'assets'); + Assert::isList($releaseResponse['assets']); + Assert::allIsArray($releaseResponse['assets']); + + $assetsNamedPiePhar = array_filter( + array_map( + static function (array $asset): array { + Assert::keyExists($asset, 'name'); + Assert::stringNotEmpty($asset['name']); + Assert::keyExists($asset, 'browser_download_url'); + Assert::stringNotEmpty($asset['browser_download_url']); + + return $asset; + }, + $releaseResponse['assets'], + ), + static function (array $asset): bool { + return $asset['name'] === self::PIE_PHAR_NAME; + }, + ); + + if (! count($assetsNamedPiePhar)) { + return null; + } + + $firstAssetNamedPiePhar = reset($assetsNamedPiePhar); + + return new ReleaseMetadata( + $releaseResponse['tag_name'], + $firstAssetNamedPiePhar['browser_download_url'], + ); }, - $decodedResponse['assets'], + $decodedResponse, ), - static function (array $asset): bool { - return $asset['name'] === self::PIE_PHAR_NAME; + static function (ReleaseMetadata|null $releaseMetadata) use ($updateChannel): bool { + if ($releaseMetadata === null) { + return false; + } + + $stability = VersionParser::parseStability($releaseMetadata->tag); + + return ($updateChannel === Channel::Stable && $stability === 'stable') + || $updateChannel === Channel::Preview; }, ); - if (! count($assetsNamedPiePhar)) { - throw PiePharMissingFromLatestRelease::fromRelease($decodedResponse['tag_name']); - } + $first = reset($releases); - $firstAssetNamedPiePhar = reset($assetsNamedPiePhar); + if (! $first instanceof ReleaseMetadata) { + throw new RuntimeException('No PIE release found for channel ' . $updateChannel->value); + } - return new ReleaseMetadata( - $decodedResponse['tag_name'], - $firstAssetNamedPiePhar['browser_download_url'], - ); + return $first; } public function downloadContent(ReleaseMetadata $releaseMetadata): BinaryFile diff --git a/src/SelfManage/Update/PiePharMissingFromLatestRelease.php b/src/SelfManage/Update/PiePharMissingFromLatestRelease.php index 20cd5ff5..5062697c 100644 --- a/src/SelfManage/Update/PiePharMissingFromLatestRelease.php +++ b/src/SelfManage/Update/PiePharMissingFromLatestRelease.php @@ -8,6 +8,7 @@ use function sprintf; +/** @deprecated This exception is no longer thrown */ class PiePharMissingFromLatestRelease extends RuntimeException { /** @param non-empty-string $tagName */ diff --git a/src/SelfManage/Update/ReleaseIsNewer.php b/src/SelfManage/Update/ReleaseIsNewer.php new file mode 100644 index 00000000..7485ea7d --- /dev/null +++ b/src/SelfManage/Update/ReleaseIsNewer.php @@ -0,0 +1,84 @@ + Channel::Stable, + 'rc', 'beta', 'alpha' => Channel::Preview, + 'dev' => Channel::Nightly, + }; + } + + /** @param non-empty-string $currentPieVersion */ + public static function forChannel( + Channel $updateChannel, + string $currentPieVersion, + ReleaseMetadata $newRelease, + ): bool { + $newVersion = $newRelease->tag; + $newStability = self::stabilityToChannel(VersionParser::parseStability($newVersion)); + + $currentStability = self::stabilityToChannel(VersionParser::parseStability($currentPieVersion)); + + $currentIsStable = $currentStability === Channel::Stable; + $currentIsPreview = $currentStability === Channel::Preview; + $currentIsNightly = $currentStability === Channel::Nightly; + $newIsStable = $newStability === Channel::Stable; + $newIsPreview = $newStability === Channel::Preview; + $newIsNightly = $newStability === Channel::Nightly; + + switch ($updateChannel) { + case Channel::Stable: + // Do not upgrade to preview or nightly + if (! $newIsStable) { + return false; + } + + // If current is nightly/preview, any stable version is an upgrade + if ($currentIsNightly || $currentIsPreview) { + return true; + } + + return Semver::satisfies($newVersion, '> ' . $currentPieVersion); + + case Channel::Preview: + // Do not update to a nightly + if ($newIsNightly) { + return false; + } + + // If current is nightly, allow upgrade to stable/preview + if ($currentIsNightly) { + return true; + } + + // Compare versions normally for stable/preview + return Semver::satisfies($newVersion, '> ' . $currentPieVersion); + + case Channel::Nightly: + // Nightly channel: accept any newer version or nightly builds + if ($newIsNightly) { + // For nightly builds, always update to nightly (or same nightly counts as update) + return true; + } + + return Semver::satisfies($newVersion, '> ' . $currentPieVersion); + } + } +} diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 00000000..5b78fdbe --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,106 @@ +pieWorkingDirectory, DIRECTORY_SEPARATOR); + + if (! file_exists($workDir)) { + mkdir($workDir, recursive: true); + } + + return $workDir . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; + } + + /** @phpstan-assert PieSettings $settingsBlob */ + private function validateSchema(mixed $settingsBlob): void + { + JsonFile::validateJsonSchema( + self::PIE_SETTINGS_FILE_NAME, + $settingsBlob, + JsonFile::STRICT_SCHEMA, + self::PIE_SETTINGS_SCHEMA_FILE_NAME, + ); + } + + /** @phpstan-return PieSettings */ + private function read(): array + { + $pieSettingsFileName = $this->pieSettingsFullPath(); + if (! file_exists($pieSettingsFileName)) { + return []; + } + + $content = file_get_contents($pieSettingsFileName); + if ($content === false) { + return []; + } + + $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR); + + $this->validateSchema($config); + + return $config; + } + + /** @param array $config */ + private function write(array $config): void + { + $this->validateSchema($config); + + file_put_contents($this->pieSettingsFullPath(), json_encode($config, flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + } + + public function updateChannel(): Channel + { + $config = $this->read(); + + return array_key_exists('channel', $config) + ? Channel::from($config['channel']) + : Channel::Stable; + } + + public function changeUpdateChannel(Channel $channel): void + { + $config = $this->read(); + + $config['channel'] = $channel->value; + + $this->write($config); + } +} diff --git a/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php index f06642ca..d469fabd 100644 --- a/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php +++ b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php @@ -7,11 +7,12 @@ use Composer\Util\AuthHelper; use Composer\Util\Http\Response; use Composer\Util\HttpDownloader; +use Php\Pie\SelfManage\Update\Channel; use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub; -use Php\Pie\SelfManage\Update\PiePharMissingFromLatestRelease; use Php\Pie\SelfManage\Update\ReleaseMetadata; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use RuntimeException; use function file_get_contents; use function hash; @@ -21,14 +22,42 @@ #[CoversClass(FetchPieReleaseFromGitHub::class)] final class FetchPieReleaseFromGitHubTest extends TestCase { - private const TEST_GITHUB_URL = 'http://test-github-url.localhost'; + private const TEST_GITHUB_URL = 'http://test-github-url.localhost'; + private const TEST_RELEASES_LIST = [ + [ + 'tag_name' => '1.2.4-rc1', + 'assets' => [ + [ + 'name' => 'not-pie.phar', + 'browser_download_url' => self::TEST_GITHUB_URL . '/do/not/download/this', + ], + [ + 'name' => 'pie.phar', + 'browser_download_url' => self::TEST_GITHUB_URL . '/path/to/pie.phar', + ], + ], + ], + [ + 'tag_name' => '1.2.3', + 'assets' => [ + [ + 'name' => 'not-pie.phar', + 'browser_download_url' => self::TEST_GITHUB_URL . '/do/not/download/this', + ], + [ + 'name' => 'pie.phar', + 'browser_download_url' => self::TEST_GITHUB_URL . '/path/to/pie.phar', + ], + ], + ], + ]; - public function testLatestReleaseMetadata(): void + public function testLatestReleaseMetadataForStableChannel(): void { $httpDownloader = $this->createMock(HttpDownloader::class); $authHelper = $this->createMock(AuthHelper::class); - $url = self::TEST_GITHUB_URL . '/repos/php/pie/releases/latest'; + $url = self::TEST_GITHUB_URL . '/repos/php/pie/releases'; $authHelper ->method('addAuthenticationHeader') ->willReturn(['Authorization: Bearer fake-token']); @@ -49,36 +78,62 @@ public function testLatestReleaseMetadata(): void ['url' => $url], 200, [], - json_encode([ - 'tag_name' => '1.2.3', - 'assets' => [ - [ - 'name' => 'not-pie.phar', - 'browser_download_url' => self::TEST_GITHUB_URL . '/do/not/download/this', - ], - [ - 'name' => 'pie.phar', - 'browser_download_url' => self::TEST_GITHUB_URL . '/path/to/pie.phar', - ], - ], - ]), + (string) json_encode(self::TEST_RELEASES_LIST), ), ); $fetch = new FetchPieReleaseFromGitHub(self::TEST_GITHUB_URL, $httpDownloader, $authHelper); - $latestRelease = $fetch->latestReleaseMetadata(); + $latestRelease = $fetch->latestReleaseMetadata(Channel::Stable); self::assertSame('1.2.3', $latestRelease->tag); self::assertSame(self::TEST_GITHUB_URL . '/path/to/pie.phar', $latestRelease->downloadUrl); } + public function testLatestReleaseMetadataForPreviewChannel(): void + { + $httpDownloader = $this->createMock(HttpDownloader::class); + $authHelper = $this->createMock(AuthHelper::class); + + $url = self::TEST_GITHUB_URL . '/repos/php/pie/releases'; + $authHelper + ->method('addAuthenticationHeader') + ->willReturn(['Authorization: Bearer fake-token']); + $httpDownloader->expects(self::once()) + ->method('get') + ->with( + $url, + [ + 'retry-auth-failure' => true, + 'http' => [ + 'method' => 'GET', + 'header' => ['Authorization: Bearer fake-token'], + ], + ], + ) + ->willReturn( + new Response( + ['url' => $url], + 200, + [], + (string) json_encode(self::TEST_RELEASES_LIST), + ), + ); + + $fetch = new FetchPieReleaseFromGitHub(self::TEST_GITHUB_URL, $httpDownloader, $authHelper); + + $latestRelease = $fetch->latestReleaseMetadata(Channel::Preview); + + self::assertSame('1.2.4-rc1', $latestRelease->tag); + self::assertSame(self::TEST_GITHUB_URL . '/path/to/pie.phar', $latestRelease->downloadUrl); + } + public function testLatestReleaseNotHavingPiePharThrowsException(): void { $httpDownloader = $this->createMock(HttpDownloader::class); $authHelper = $this->createMock(AuthHelper::class); - $url = self::TEST_GITHUB_URL . '/repos/php/pie/releases/latest'; + $url = self::TEST_GITHUB_URL . '/repos/php/pie/releases'; $authHelper ->method('addAuthenticationHeader') ->willReturn(['Authorization: Bearer fake-token']); @@ -100,11 +155,22 @@ public function testLatestReleaseNotHavingPiePharThrowsException(): void 200, [], json_encode([ - 'tag_name' => '1.2.3', - 'assets' => [ - [ - 'name' => 'not-pie.phar', - 'browser_download_url' => self::TEST_GITHUB_URL . '/do/not/download/this', + [ + 'tag_name' => '1.2.3', + 'assets' => [ + [ + 'name' => 'not-pie.phar', + 'browser_download_url' => self::TEST_GITHUB_URL . '/do/not/download/this', + ], + ], + ], + [ + 'tag_name' => '1.2.4-rc1', + 'assets' => [ + [ + 'name' => 'not-pie.phar', + 'browser_download_url' => self::TEST_GITHUB_URL . '/do/not/download/this', + ], ], ], ]), @@ -113,8 +179,8 @@ public function testLatestReleaseNotHavingPiePharThrowsException(): void $fetch = new FetchPieReleaseFromGitHub(self::TEST_GITHUB_URL, $httpDownloader, $authHelper); - $this->expectException(PiePharMissingFromLatestRelease::class); - $fetch->latestReleaseMetadata(); + $this->expectException(RuntimeException::class); + $fetch->latestReleaseMetadata(Channel::Stable); } public function testDownloadContent(): void diff --git a/test/unit/SelfManage/Update/ReleaseIsNewerTest.php b/test/unit/SelfManage/Update/ReleaseIsNewerTest.php new file mode 100644 index 00000000..33f9bd78 --- /dev/null +++ b/test/unit/SelfManage/Update/ReleaseIsNewerTest.php @@ -0,0 +1,85 @@ + */ + public function provider(): array + { + return [ + 'stable-oldstable-to-newstable' => [Channel::Stable, '1.0.0', '1.0.1', true], + 'stable-newstable-to-oldstable' => [Channel::Stable, '1.0.1', '1.0.0', false], + 'stable-oldstable-to-newpreview' => [Channel::Stable, '1.0.0', '1.0.1-rc1', false], + 'stable-newstable-to-newpreview' => [Channel::Stable, '1.0.1', '1.0.1-rc1', false], + 'stable-stable-to-nightly' => [Channel::Stable, '1.0.0', 'dev-main', false], + 'stable-oldpreview-to-newpreview' => [Channel::Stable, '1.0.1-rc1', '1.0.1-rc2', false], + 'stable-newpreview-to-oldpreview' => [Channel::Stable, '1.0.1-rc2', '1.0.1-rc1', false], + 'stable-preview-to-oldstable' => [Channel::Stable, '1.0.1-rc1', '1.0.0', true], + 'stable-preview-to-newstable' => [Channel::Stable, '1.0.1-rc1', '1.0.1', true], + 'stable-preview-to-nightly' => [Channel::Stable, '1.0.1-rc1', 'dev-main', false], + 'stable-nightly-to-nightly' => [Channel::Stable, 'dev-main', 'dev-main', false], + 'stable-nightly-to-stable' => [Channel::Stable, 'dev-main', '1.0.0', true], + 'stable-nightly-to-preview' => [Channel::Stable, 'dev-main', '1.0.1-rc1', false], + + 'preview-oldstable-to-newstable' => [Channel::Preview, '1.0.0', '1.0.1', true], + 'preview-newstable-to-oldstable' => [Channel::Preview, '1.0.1', '1.0.0', false], + 'preview-oldstable-to-newpreview' => [Channel::Preview, '1.0.0', '1.0.1-rc1', true], + 'preview-newstable-to-newpreview' => [Channel::Preview, '1.0.1', '1.0.1-rc1', false], + 'preview-stable-to-nightly' => [Channel::Preview, '1.0.0', 'dev-main', false], + 'preview-oldpreview-to-newpreview' => [Channel::Preview, '1.0.1-rc1', '1.0.1-rc2', true], + 'preview-newpreview-to-oldpreview' => [Channel::Preview, '1.0.1-rc2', '1.0.1-rc1', false], + 'preview-preview-to-oldstable' => [Channel::Preview, '1.0.1-rc1', '1.0.0', false], + 'preview-preview-to-newstable' => [Channel::Preview, '1.0.1-rc1', '1.0.1', true], + 'preview-preview-to-nightly' => [Channel::Preview, '1.0.1-rc1', 'dev-main', false], + 'preview-nightly-to-nightly' => [Channel::Preview, 'dev-main', 'dev-main', false], + 'preview-nightly-to-stable' => [Channel::Preview, 'dev-main', '1.0.0', true], + 'preview-nightly-to-preview' => [Channel::Preview, 'dev-main', '1.0.1-rc1', true], + + 'nightly-oldstable-to-newstable' => [Channel::Nightly, '1.0.0', '1.0.1', true], + 'nightly-newstable-to-oldstable' => [Channel::Nightly, '1.0.1', '1.0.0', false], + 'nightly-oldstable-to-newpreview' => [Channel::Nightly, '1.0.0', '1.0.1-rc1', true], + 'nightly-newstable-to-newpreview' => [Channel::Nightly, '1.0.1', '1.0.1-rc1', false], + 'nightly-stable-to-nightly' => [Channel::Nightly, '1.0.0', 'dev-main', true], + 'nightly-oldpreview-to-newpreview' => [Channel::Nightly, '1.0.1-rc1', '1.0.1-rc2', true], + 'nightly-newpreview-to-oldpreview' => [Channel::Nightly, '1.0.1-rc2', '1.0.1-rc1', false], + 'nightly-preview-to-oldstable' => [Channel::Nightly, '1.0.1-rc1', '1.0.0', false], + 'nightly-preview-to-newstable' => [Channel::Nightly, '1.0.1-rc1', '1.0.1', true], + 'nightly-preview-to-nightly' => [Channel::Nightly, '1.0.1-rc1', 'dev-main', true], + 'nightly-nightly-to-nightly' => [Channel::Nightly, 'dev-main', 'dev-main', true], + 'nightly-nightly-to-stable' => [Channel::Nightly, 'dev-main', '1.0.0', false], + 'nightly-nightly-to-preview' => [Channel::Nightly, 'dev-main', '1.0.1-rc1', false], + ]; + } + + /** + * @param non-empty-string $currentPieVersion + * @param non-empty-string $newReleaseTag + */ + #[DataProvider('provider')] + public function testReleaseIsNewerForChannel( + Channel $updateChannel, + string $currentPieVersion, + string $newReleaseTag, + bool $shouldUpgrade, + ): void { + self::assertSame( + $shouldUpgrade, + ReleaseIsNewer::forChannel( + $updateChannel, + $currentPieVersion, + new ReleaseMetadata($newReleaseTag, 'ignored'), + ), + ); + } +} diff --git a/test/unit/SettingsTest.php b/test/unit/SettingsTest.php new file mode 100644 index 00000000..db28700e --- /dev/null +++ b/test/unit/SettingsTest.php @@ -0,0 +1,74 @@ +expectException(JsonValidationException::class); + $settings->updateChannel(); + } + + public function testNewSettingsJsonCanBeCreated(): void + { + $workingDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_settings_test', true) . DIRECTORY_SEPARATOR; + + $settings = new Settings($workingDir); + self::assertSame(Channel::Stable, $settings->updateChannel()); + + $settings->changeUpdateChannel(Channel::Preview); + self::assertSame(Channel::Preview, $settings->updateChannel()); + + self::assertJsonStringEqualsJsonString( + '{"channel": "preview"}', + (string) file_get_contents($workingDir . 'pie-settings.json'), + ); + + (new Filesystem())->remove($workingDir); + } + + public function testExistingSettingsCanBeUpdated(): void + { + $workingDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_settings_test', true) . DIRECTORY_SEPARATOR; + mkdir($workingDir, recursive: true); + file_put_contents($workingDir . 'pie-settings.json', '{"channel": "stable"}'); + + $settings = new Settings($workingDir); + self::assertSame(Channel::Stable, $settings->updateChannel()); + + $settings->changeUpdateChannel(Channel::Nightly); + self::assertSame(Channel::Nightly, $settings->updateChannel()); + + self::assertJsonStringEqualsJsonString( + '{"channel": "nightly"}', + (string) file_get_contents($workingDir . 'pie-settings.json'), + ); + + (new Filesystem())->remove($workingDir); + } +}