From e53ee1cbab887f84cb587168a08ad157f070f93c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 7 Oct 2025 20:16:55 +0100 Subject: [PATCH 1/7] Added basic settings fetching to fetch channel update --- src/Command/SelfUpdateCommand.php | 7 ++++ src/Platform.php | 51 ++++++++++++++------------- src/SelfManage/Update/Channel.php | 12 +++++++ src/Settings.php | 57 +++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 src/SelfManage/Update/Channel.php create mode 100644 src/Settings.php diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index 9a89148..08f88c5 100644 --- a/src/Command/SelfUpdateCommand.php +++ b/src/Command/SelfUpdateCommand.php @@ -12,11 +12,13 @@ use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\File\FullPathToSelf; use Php\Pie\File\SudoFilePut; +use Php\Pie\Platform; use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub; use Php\Pie\SelfManage\Update\PiePharMissingFromLatestRelease; 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; @@ -70,6 +72,11 @@ public function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } + $settings = new Settings(Platform::getPieBaseWorkingDirectory()); + $updateChannel = $settings->updateChannel(); + + // @todo change channel based on args + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); $composer = PieComposerFactory::createPieComposer( diff --git a/src/Platform.php b/src/Platform.php index 5ef0f6f..90d50ca 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 0000000..a0fd99f --- /dev/null +++ b/src/SelfManage/Update/Channel.php @@ -0,0 +1,12 @@ +pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; + if (! file_exists($pieSettingsFileName)) { + return []; + } + + $content = file_get_contents($pieSettingsFileName); + if ($content === false) { + return []; + } + + $config = json_decode($content, true); + + // @todo schema validation + + return is_array($config) ? $config : []; + } + + public function updateChannel(): Channel + { + $config = $this->read(); + + return array_key_exists('channel', $config) + ? Channel::from($config['channel']) + : Channel::Stable; + } +} From 7b586360f1d57c9d6291848617affb4dd9873c3e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 7 Oct 2025 20:37:46 +0100 Subject: [PATCH 2/7] Update and write channel settings --- src/Command/SelfUpdateCommand.php | 29 +++++++++++++++++++++++- src/Settings.php | 37 ++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index 08f88c5..9dfa397 100644 --- a/src/Command/SelfUpdateCommand.php +++ b/src/Command/SelfUpdateCommand.php @@ -13,6 +13,7 @@ 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\ReleaseMetadata; @@ -39,6 +40,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 */ @@ -62,6 +65,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 @@ -75,7 +90,18 @@ public function execute(InputInterface $input, OutputInterface $output): int $settings = new Settings(Platform::getPieBaseWorkingDirectory()); $updateChannel = $settings->updateChannel(); - // @todo change channel based on args + if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) { + $settings->changeUpdateChannel(Channel::Nightly); + $updateChannel = Channel::Nightly; + } else if ($input->hasOption(self::OPTION_PREVIEW_UPDATE) && $input->getOption(self::OPTION_PREVIEW_UPDATE)) { + $settings->changeUpdateChannel(Channel::Preview); + $updateChannel = Channel::Preview; + } else if ($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); @@ -92,6 +118,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $fetchLatestPieRelease = new FetchPieReleaseFromGitHub($this->githubApiBaseUrl, $httpDownloader, $authHelper); $verifyPiePhar = VerifyPieReleaseUsingAttestation::factory(); + // @todo use $updateChannel to decide where to update if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) { $latestRelease = new ReleaseMetadata( 'nightly', diff --git a/src/Settings.php b/src/Settings.php index 87fb685..c7339f5 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -9,11 +9,22 @@ use function array_key_exists; use function file_exists; use function file_get_contents; +use function file_put_contents; use function is_array; use function json_decode; +use function json_encode; use const DIRECTORY_SEPARATOR; +use const JSON_PRETTY_PRINT; +use const JSON_THROW_ON_ERROR; +/** + * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks + * + * @phpstan-type PieSettings = array{ + * channel?: non-empty-string, + * } + */ class Settings { private const PIE_SETTINGS_FILE_NAME = 'pie-settings.json'; @@ -22,11 +33,7 @@ public function __construct(private readonly string $pieWorkingDirectory) { } - /** - * @return array{ - * channel?: non-empty-string, - * } - */ + /** @phpstan-return PieSettings */ private function read(): array { $pieSettingsFileName = $this->pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; @@ -39,13 +46,22 @@ private function read(): array return []; } - $config = json_decode($content, true); + $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR); // @todo schema validation return is_array($config) ? $config : []; } + /** @param PieSettings $config */ + private function write(array $config): void + { + // @todo schema validation + + $pieSettingsFileName = $this->pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; + file_put_contents($pieSettingsFileName, json_encode($config, flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + } + public function updateChannel(): Channel { $config = $this->read(); @@ -54,4 +70,13 @@ public function updateChannel(): Channel ? Channel::from($config['channel']) : Channel::Stable; } + + public function changeUpdateChannel(Channel $channel): void + { + $config = $this->read(); + + $config['channel'] = $channel->value; + + $this->write($config); + } } From ac9d8421ebcb88091defccd67553127a8452770e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 7 Oct 2025 21:20:50 +0100 Subject: [PATCH 3/7] Pass update channel for self update --- src/Command/SelfUpdateCommand.php | 24 +++++++------------ src/SelfManage/Update/FetchPieRelease.php | 2 +- .../Update/FetchPieReleaseFromGitHub.php | 3 ++- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index 9dfa397..f42a124 100644 --- a/src/Command/SelfUpdateCommand.php +++ b/src/Command/SelfUpdateCommand.php @@ -93,10 +93,10 @@ public function execute(InputInterface $input, OutputInterface $output): int if ($input->hasOption(self::OPTION_NIGHTLY_UPDATE) && $input->getOption(self::OPTION_NIGHTLY_UPDATE)) { $settings->changeUpdateChannel(Channel::Nightly); $updateChannel = Channel::Nightly; - } else if ($input->hasOption(self::OPTION_PREVIEW_UPDATE) && $input->getOption(self::OPTION_PREVIEW_UPDATE)) { + } elseif ($input->hasOption(self::OPTION_PREVIEW_UPDATE) && $input->getOption(self::OPTION_PREVIEW_UPDATE)) { $settings->changeUpdateChannel(Channel::Preview); $updateChannel = Channel::Preview; - } else if ($input->hasOption(self::OPTION_STABLE_UPDATE) && $input->getOption(self::OPTION_STABLE_UPDATE)) { + } elseif ($input->hasOption(self::OPTION_STABLE_UPDATE) && $input->getOption(self::OPTION_STABLE_UPDATE)) { $settings->changeUpdateChannel(Channel::Stable); $updateChannel = Channel::Stable; } @@ -118,8 +118,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $fetchLatestPieRelease = new FetchPieReleaseFromGitHub($this->githubApiBaseUrl, $httpDownloader, $authHelper); $verifyPiePhar = VerifyPieReleaseUsingAttestation::factory(); - // @todo use $updateChannel to decide where to update - 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', @@ -128,7 +127,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Downloading the latest nightly release.'); } else { try { - $latestRelease = $fetchLatestPieRelease->latestReleaseMetadata(); + $latestRelease = $fetchLatestPieRelease->latestReleaseMetadata($updateChannel); } catch (PiePharMissingFromLatestRelease $piePharMissingFromLatestRelease) { $output->writeln(sprintf('%s', $piePharMissingFromLatestRelease->getMessage())); @@ -137,19 +136,14 @@ public function execute(InputInterface $input, OutputInterface $output): int $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)); + // @todo test going from preview->stable, nightly->preview, nightly->stable, stable->preview if (! Semver::satisfies($latestRelease->tag, '> ' . $pieVersion)) { - $output->writeln('You already have the latest version 😍'); + $output->writeln(sprintf( + 'You already have the latest version for the %s channel 😍', + $updateChannel->value, + )); return Command::SUCCESS; } diff --git a/src/SelfManage/Update/FetchPieRelease.php b/src/SelfManage/Update/FetchPieRelease.php index 3ec78c7..57852bb 100644 --- a/src/SelfManage/Update/FetchPieRelease.php +++ b/src/SelfManage/Update/FetchPieRelease.php @@ -10,7 +10,7 @@ interface FetchPieRelease { /** @throws PiePharMissingFromLatestRelease */ - public function latestReleaseMetadata(): ReleaseMetadata; + public function latestReleaseMetadata(Channel $updateChannel): ReleaseMetadata; /** Download the given pie.phar and return the filename (should be a temp file) */ public function downloadContent(ReleaseMetadata $releaseMetadata): BinaryFile; diff --git a/src/SelfManage/Update/FetchPieReleaseFromGitHub.php b/src/SelfManage/Update/FetchPieReleaseFromGitHub.php index efba02a..d7dec09 100644 --- a/src/SelfManage/Update/FetchPieReleaseFromGitHub.php +++ b/src/SelfManage/Update/FetchPieReleaseFromGitHub.php @@ -31,8 +31,9 @@ public function __construct( ) { } - public function latestReleaseMetadata(): ReleaseMetadata + public function latestReleaseMetadata(Channel $updateChannel): ReleaseMetadata { + // @todo update to preview or stable $url = $this->githubApiBaseUrl . self::PIE_LATEST_RELEASE_URL; $decodedResponse = $this->httpDownloader->get( From 1139a8e711d574d6945c3de05a757e79453d10a1 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 9 Oct 2025 14:18:27 +0100 Subject: [PATCH 4/7] Also fetch RC releases if they are latest and --preview channel --- .../Update/FetchPieReleaseFromGitHub.php | 85 ++++++++----- .../PiePharMissingFromLatestRelease.php | 1 + .../Update/FetchPieReleaseFromGitHubTest.php | 116 ++++++++++++++---- 3 files changed, 148 insertions(+), 54 deletions(-) diff --git a/src/SelfManage/Update/FetchPieReleaseFromGitHub.php b/src/SelfManage/Update/FetchPieReleaseFromGitHub.php index d7dec09..edd2a7e 100644 --- a/src/SelfManage/Update/FetchPieReleaseFromGitHub.php +++ b/src/SelfManage/Update/FetchPieReleaseFromGitHub.php @@ -4,6 +4,7 @@ namespace Php\Pie\SelfManage\Update; +use Composer\Package\Version\VersionParser; use Composer\Util\AuthHelper; use Composer\Util\HttpDownloader; use Php\Pie\File\BinaryFile; @@ -21,8 +22,8 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class FetchPieReleaseFromGitHub implements FetchPieRelease { - private const PIE_PHAR_NAME = 'pie.phar'; - private const PIE_LATEST_RELEASE_URL = '/repos/php/pie/releases/latest'; + private const PIE_PHAR_NAME = 'pie.phar'; + private const PIE_RELEASES_URL = '/repos/php/pie/releases'; public function __construct( private readonly string $githubApiBaseUrl, @@ -33,8 +34,7 @@ public function __construct( public function latestReleaseMetadata(Channel $updateChannel): ReleaseMetadata { - // @todo update to preview or stable - $url = $this->githubApiBaseUrl . self::PIE_LATEST_RELEASE_URL; + $url = $this->githubApiBaseUrl . self::PIE_RELEASES_URL; $decodedResponse = $this->httpDownloader->get( $url, @@ -47,40 +47,67 @@ public function latestReleaseMetadata(Channel $updateChannel): 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 20cd5ff..5062697 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/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php index f06642c..1ee6e3a 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; @@ -22,13 +23,41 @@ final class FetchPieReleaseFromGitHubTest extends TestCase { 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 From 943fe195520b0dfc59b513eb6238b4175c240559 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Sat, 11 Oct 2025 09:08:24 +0100 Subject: [PATCH 5/7] Improve determining if we should upgrade or not based on selected update channel --- src/Command/SelfUpdateCommand.php | 6 +- src/SelfManage/Update/Channel.php | 2 +- src/SelfManage/Update/ReleaseIsNewer.php | 84 ++++++++++++++++++ .../Update/FetchPieReleaseFromGitHubTest.php | 2 +- .../SelfManage/Update/ReleaseIsNewerTest.php | 85 +++++++++++++++++++ 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 src/SelfManage/Update/ReleaseIsNewer.php create mode 100644 test/unit/SelfManage/Update/ReleaseIsNewerTest.php diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index f42a124..18a94c4 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; @@ -16,6 +15,7 @@ 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; @@ -30,7 +30,6 @@ use Symfony\Component\Console\Output\OutputInterface; use function file_get_contents; -use function preg_match; use function sprintf; use function unlink; @@ -138,8 +137,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $output->writeln(sprintf('You are currently running PIE version %s', $pieVersion)); - // @todo test going from preview->stable, nightly->preview, nightly->stable, stable->preview - if (! Semver::satisfies($latestRelease->tag, '> ' . $pieVersion)) { + if (! ReleaseIsNewer::forChannel($updateChannel, $pieVersion, $latestRelease)) { $output->writeln(sprintf( 'You already have the latest version for the %s channel 😍', $updateChannel->value, diff --git a/src/SelfManage/Update/Channel.php b/src/SelfManage/Update/Channel.php index a0fd99f..8447f0b 100644 --- a/src/SelfManage/Update/Channel.php +++ b/src/SelfManage/Update/Channel.php @@ -6,7 +6,7 @@ enum Channel: string { - case Stable = 'stable'; + case Stable = 'stable'; case Preview = 'preview'; case Nightly = 'nightly'; } diff --git a/src/SelfManage/Update/ReleaseIsNewer.php b/src/SelfManage/Update/ReleaseIsNewer.php new file mode 100644 index 0000000..7485ea7 --- /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/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php index 1ee6e3a..d469fab 100644 --- a/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php +++ b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php @@ -22,7 +22,7 @@ #[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', diff --git a/test/unit/SelfManage/Update/ReleaseIsNewerTest.php b/test/unit/SelfManage/Update/ReleaseIsNewerTest.php new file mode 100644 index 0000000..33f9bd7 --- /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'), + ), + ); + } +} From ded16e607c31cd8ff930c19652d374ae4810d939 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Sat, 11 Oct 2025 11:44:00 +0100 Subject: [PATCH 6/7] Validate schema for PIE settings file --- phpstan-baseline.neon | 8 +-- resources/pie-settings-schema.json | 18 +++++++ src/Settings.php | 42 +++++++++++---- test/unit/SettingsTest.php | 84 ++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 resources/pie-settings-schema.json create mode 100644 test/unit/SettingsTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3374d5f..cef582e 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 0000000..8e2fbd0 --- /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/Settings.php b/src/Settings.php index c7339f5..5b78fdb 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -4,15 +4,17 @@ namespace Php\Pie; +use Composer\Json\JsonFile; use Php\Pie\SelfManage\Update\Channel; use function array_key_exists; use function file_exists; use function file_get_contents; use function file_put_contents; -use function is_array; use function json_decode; use function json_encode; +use function mkdir; +use function rtrim; use const DIRECTORY_SEPARATOR; use const JSON_PRETTY_PRINT; @@ -27,16 +29,39 @@ */ class Settings { - private const PIE_SETTINGS_FILE_NAME = 'pie-settings.json'; + private const PIE_SETTINGS_SCHEMA_FILE_NAME = __DIR__ . '/../resources/pie-settings-schema.json'; + private const PIE_SETTINGS_FILE_NAME = 'pie-settings.json'; public function __construct(private readonly string $pieWorkingDirectory) { } + private function pieSettingsFullPath(): string + { + $workDir = rtrim($this->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->pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; + $pieSettingsFileName = $this->pieSettingsFullPath(); if (! file_exists($pieSettingsFileName)) { return []; } @@ -48,18 +73,17 @@ private function read(): array $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR); - // @todo schema validation + $this->validateSchema($config); - return is_array($config) ? $config : []; + return $config; } - /** @param PieSettings $config */ + /** @param array $config */ private function write(array $config): void { - // @todo schema validation + $this->validateSchema($config); - $pieSettingsFileName = $this->pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; - file_put_contents($pieSettingsFileName, json_encode($config, flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + file_put_contents($this->pieSettingsFullPath(), json_encode($config, flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } public function updateChannel(): Channel diff --git a/test/unit/SettingsTest.php b/test/unit/SettingsTest.php new file mode 100644 index 0000000..282931e --- /dev/null +++ b/test/unit/SettingsTest.php @@ -0,0 +1,84 @@ +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::assertSame( + trim(<<<'JSON' +{ + "channel": "preview" +} +JSON), + str_replace("\r\n", "\n", (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::Preview); + self::assertSame(Channel::Preview, $settings->updateChannel()); + + self::assertSame( + trim(<<<'JSON' +{ + "channel": "preview" +} +JSON), + str_replace("\r\n", "\n", (string) file_get_contents($workingDir . 'pie-settings.json')), + ); + + (new Filesystem())->remove($workingDir); + } +} From 19be08a5179e8a5f4964f6b07ff5c0f265b4e187 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 13 Oct 2025 19:35:54 +0100 Subject: [PATCH 7/7] Catch all when fetching latest release --- src/Command/SelfUpdateCommand.php | 6 +++--- test/unit/SettingsTest.php | 26 ++++++++------------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index 18a94c4..8cdf0e9 100644 --- a/src/Command/SelfUpdateCommand.php +++ b/src/Command/SelfUpdateCommand.php @@ -14,7 +14,6 @@ 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; @@ -28,6 +27,7 @@ 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 sprintf; @@ -127,8 +127,8 @@ public function execute(InputInterface $input, OutputInterface $output): int } else { try { $latestRelease = $fetchLatestPieRelease->latestReleaseMetadata($updateChannel); - } catch (PiePharMissingFromLatestRelease $piePharMissingFromLatestRelease) { - $output->writeln(sprintf('%s', $piePharMissingFromLatestRelease->getMessage())); + } catch (Throwable $throwable) { + $output->writeln(sprintf('%s', $throwable->getMessage())); return Command::FAILURE; } diff --git a/test/unit/SettingsTest.php b/test/unit/SettingsTest.php index 282931e..db28700 100644 --- a/test/unit/SettingsTest.php +++ b/test/unit/SettingsTest.php @@ -14,9 +14,7 @@ use function file_get_contents; use function file_put_contents; use function mkdir; -use function str_replace; use function sys_get_temp_dir; -use function trim; use function uniqid; use const DIRECTORY_SEPARATOR; @@ -46,13 +44,9 @@ public function testNewSettingsJsonCanBeCreated(): void $settings->changeUpdateChannel(Channel::Preview); self::assertSame(Channel::Preview, $settings->updateChannel()); - self::assertSame( - trim(<<<'JSON' -{ - "channel": "preview" -} -JSON), - str_replace("\r\n", "\n", (string) file_get_contents($workingDir . 'pie-settings.json')), + self::assertJsonStringEqualsJsonString( + '{"channel": "preview"}', + (string) file_get_contents($workingDir . 'pie-settings.json'), ); (new Filesystem())->remove($workingDir); @@ -67,16 +61,12 @@ public function testExistingSettingsCanBeUpdated(): void $settings = new Settings($workingDir); self::assertSame(Channel::Stable, $settings->updateChannel()); - $settings->changeUpdateChannel(Channel::Preview); - self::assertSame(Channel::Preview, $settings->updateChannel()); + $settings->changeUpdateChannel(Channel::Nightly); + self::assertSame(Channel::Nightly, $settings->updateChannel()); - self::assertSame( - trim(<<<'JSON' -{ - "channel": "preview" -} -JSON), - str_replace("\r\n", "\n", (string) file_get_contents($workingDir . 'pie-settings.json')), + self::assertJsonStringEqualsJsonString( + '{"channel": "nightly"}', + (string) file_get_contents($workingDir . 'pie-settings.json'), ); (new Filesystem())->remove($workingDir);