From 66bf738a33ee4edc09f1d516563784d2c5985378 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Nov 2025 17:34:44 +0000 Subject: [PATCH 01/12] Increase timeout for running pie itself --- test/behaviour/CliContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 9d151c5a..52b304a9 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -39,7 +39,7 @@ public function runPieCommand(array $command): void { $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, 'bin/pie'], $command); - $proc = (new Process($pieCommand))->mustRun(); + $proc = (new Process($pieCommand, timeout: 120))->mustRun(); $this->output = $proc->getOutput(); $this->exitCode = $proc->getExitCode(); From 73e06fd114828ec86d96b0171a9ca1c7a727305b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 5 Nov 2025 17:06:02 +0000 Subject: [PATCH 02/12] Expand feature descriptions for better coverage of new features --- features/bundled-php-extensions.feature | 7 +++++++ features/install-extensions.feature | 15 ++++++++++++-- features/install-in-php-project.feature | 7 +++++++ features/install-in-pie-project.feature | 7 +++++++ features/manage-repositories.feature | 2 ++ features/platform-dependencies.feature | 13 ++++++++++++ features/self-update.feature | 27 +++++++++++++++++++++++++ features/uninstall-extensions.feature | 1 + 8 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 features/bundled-php-extensions.feature create mode 100644 features/install-in-php-project.feature create mode 100644 features/install-in-pie-project.feature create mode 100644 features/platform-dependencies.feature create mode 100644 features/self-update.feature diff --git a/features/bundled-php-extensions.feature b/features/bundled-php-extensions.feature new file mode 100644 index 00000000..cbc3a856 --- /dev/null +++ b/features/bundled-php-extensions.feature @@ -0,0 +1,7 @@ +Feature: Extensions for a PHP project can be installed with PIE + + # pie install php/sodium + Example: An extension normally bundled with PHP can be installed + Given I have libsodium on my system + When I install the sodium extension with PIE + Then the extension should have been installed and enabled diff --git a/features/install-extensions.feature b/features/install-extensions.feature index 35076040..cbae8880 100644 --- a/features/install-extensions.feature +++ b/features/install-extensions.feature @@ -1,9 +1,11 @@ Feature: Extensions can be installed with PIE + # pie download Example: The latest version of an extension can be downloaded When I run a command to download the latest version of an extension Then the latest version should have been downloaded + # pie download : Scenario Outline: A version matching the requested constraint can be downloaded When I run a command to download version "" of an extension Then version "" should have been downloaded @@ -13,11 +15,13 @@ Feature: Extensions can be installed with PIE | 2.0.5 | 2.0.5 | | ^2.0 | 2.0.5 | + # pie download :dev-main @non-windows Example: An in-development version can be downloaded on non-Windows systems When I run a command to download version "dev-main" of an extension Then version "dev-main" should have been downloaded + # pie build Example: An extension can be built When I run a command to build an extension Then the extension should have been built @@ -27,10 +31,17 @@ Feature: Extensions can be installed with PIE When I run a command to build an extension Then the extension should have been built + # pie build --with-some-options=foo Example: An extension can be built with configure options When I run a command to build an extension with configure options Then the extension should have been built with options - Example: An extension can be installed - When I run a command to install an extension + # pie install --skip-enable-extension + Example: An extension can be installed without enabling + When I run a command to install an extension without enabling it Then the extension should have been installed + + # pie install + Example: An extension can be installed and enabled + When I run a command to install an extension + Then the extension should have been installed and enabled diff --git a/features/install-in-php-project.feature b/features/install-in-php-project.feature new file mode 100644 index 00000000..3d3abb8c --- /dev/null +++ b/features/install-in-php-project.feature @@ -0,0 +1,7 @@ +Feature: Extensions for a PHP project can be installed with PIE + + # pie install + Example: PIE running in a PHP project suggests missing dependencies + Given I am in a PHP project that has missing extensions + When I run a command to install the extensions + Then I should see all the extensions are now installed diff --git a/features/install-in-pie-project.feature b/features/install-in-pie-project.feature new file mode 100644 index 00000000..2bfcdcf5 --- /dev/null +++ b/features/install-in-pie-project.feature @@ -0,0 +1,7 @@ +Feature: A PIE extension can be installed with PIE + + # pie install + Example: Running PIE in a PIE project will install that PIE extension + Given I am in a PIE project + When I run a command to install the extension + Then the extension should have been installed and enabled diff --git a/features/manage-repositories.feature b/features/manage-repositories.feature index 6fd69b50..0c54d201 100644 --- a/features/manage-repositories.feature +++ b/features/manage-repositories.feature @@ -1,10 +1,12 @@ Feature: Package repositories can be managed with PIE + # pie repository:add ... Example: A package repository can be added Given no repositories have previously been added When I add a package repository Then I should see the package repository can be used by PIE + # pie repository:remove ... Example: A package repository can be removed Given I have previously added a package repository When I remove the package repository diff --git a/features/platform-dependencies.feature b/features/platform-dependencies.feature new file mode 100644 index 00000000..f9dbcf98 --- /dev/null +++ b/features/platform-dependencies.feature @@ -0,0 +1,13 @@ +Feature: Platform dependencies are checked when installing + + # pie info + Example: Extension platform dependencies are listed as dependencies + Given I do not have libsodium on my system + When I display information about the sodium extension with PIE + Then the information should show that libsodium is a missing dependency + + # pie install + Example: Extension platform dependencies will warn the extension is missing a dependency + Given I do not have libsodium on my system + When I install the sodium extension with PIE + Then the extension fails to install due to the missing library diff --git a/features/self-update.feature b/features/self-update.feature new file mode 100644 index 00000000..0a6a65b8 --- /dev/null +++ b/features/self-update.feature @@ -0,0 +1,27 @@ +Feature: PIE can update itself and verify it is authentic + + # pie self-update + Example: PIE can update itself + Given I have an old version of PIE + When I update PIE to the latest stable version + Then I should see I have been updated to the latest version + + # pie self-verify + Example: PIE can verify its authenticity with gh + Given I have a pie.phar built on PHP's GitHub + And I have the gh cli command + When I verify my PIE installation + Then I should see it is verified + + # pie self-verify + Example: PIE can verify its authenticity with openssl + Given I have a pie.phar built on PHP's GitHub + And I do not have the gh cli command + When I verify my PIE installation + Then I should see it is verified + + # pie self-verify + Example: PIE will alert when its authenticity is not verified + Given I have a pie.phar built on a nasty hacker's machine + When I verify my PIE installation + Then I should see it has failed verification diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature index 1167897e..850d5729 100644 --- a/features/uninstall-extensions.feature +++ b/features/uninstall-extensions.feature @@ -1,5 +1,6 @@ Feature: Extensions can be uninstalled with PIE + # pie uninstall Example: An extension can be uninstalled Given an extension was previously installed When I run a command to uninstall an extension From 1c1b37e859421108eb9a567cee1f142497e47d8d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 6 Nov 2025 15:49:40 +0000 Subject: [PATCH 03/12] Separate install vs install and enable Behat steps --- test/behaviour/CliContext.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 52b304a9..d9e4a0e6 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -119,6 +119,12 @@ public function iRunACommandToInstallAnExtension(): void $this->runPieCommand(['install', 'asgrim/example-pie-extension']); } + #[When('I run a command to install an extension without enabling it')] + public function iRunACommandToInstallAnExtensionWithoutEnabling(): void + { + $this->runPieCommand(['install', 'asgrim/example-pie-extension', '--skip-enable-extension']); + } + #[When('I run a command to uninstall an extension')] public function iRunACommandToUninstallAnExtension(): void { @@ -148,6 +154,22 @@ public function theExtensionShouldHaveBeenInstalled(): void { $this->assertCommandSuccessful(); + Assert::contains($this->output, 'Extension has NOT been automatically enabled.'); + + if (Platform::isWindows()) { + Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#'); + + return; + } + + Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/example_pie_extension.so#'); + } + + #[Then('the extension should have been installed and enabled')] + public function theExtensionShouldHaveBeenInstalledAndEnabled(): void + { + $this->assertCommandSuccessful(); + Assert::contains($this->output, 'Extension is enabled and loaded'); if (Platform::isWindows()) { From 772ae3bf07bb6078858a47ba18fdb5484a9f50a6 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 6 Nov 2025 16:07:25 +0000 Subject: [PATCH 04/12] Add Behat context for bundled-php-extensions.feature --- features/uninstall-extensions.feature | 2 +- test/behaviour/CliContext.php | 37 ++++++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature index 850d5729..93002d4f 100644 --- a/features/uninstall-extensions.feature +++ b/features/uninstall-extensions.feature @@ -2,6 +2,6 @@ Feature: Extensions can be uninstalled with PIE # pie uninstall Example: An extension can be uninstalled - Given an extension was previously installed + Given an extension was previously installed and enabled When I run a command to uninstall an extension Then the extension should not be installed anymore diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index d9e4a0e6..12964305 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -22,6 +22,8 @@ class CliContext implements Context /** @var list */ private array $phpArguments = []; + private string $theExtension = 'example_pie_extension'; + #[When('I run a command to download the latest version of an extension')] public function iRunACommandToDownloadTheLatestVersionOfAnExtension(): void { @@ -113,22 +115,25 @@ public function theExtensionShouldHaveBeenBuiltWithOptions(): void } #[When('I run a command to install an extension')] - #[Given('an extension was previously installed')] + #[Given('an extension was previously installed and enabled')] public function iRunACommandToInstallAnExtension(): void { $this->runPieCommand(['install', 'asgrim/example-pie-extension']); + $this->theExtension = 'example_pie_extension'; } #[When('I run a command to install an extension without enabling it')] public function iRunACommandToInstallAnExtensionWithoutEnabling(): void { $this->runPieCommand(['install', 'asgrim/example-pie-extension', '--skip-enable-extension']); + $this->theExtension = 'example_pie_extension'; } #[When('I run a command to uninstall an extension')] public function iRunACommandToUninstallAnExtension(): void { $this->runPieCommand(['uninstall', 'asgrim/example-pie-extension']); + $this->theExtension = 'example_pie_extension'; } #[Then('the extension should not be installed anymore')] @@ -137,12 +142,12 @@ public function theExtensionShouldNotBeInstalled(): void $this->assertCommandSuccessful(); if (Platform::isWindows()) { - Assert::regex($this->output, '#👋 Removed extension: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#'); + Assert::regex($this->output, '#👋 Removed extension: [-\\\_:.a-zA-Z0-9]+\\\php_' . $this->theExtension . '.dll#'); } else { - Assert::regex($this->output, '#👋 Removed extension: [-_.a-zA-Z0-9/]+/example_pie_extension.so#'); + Assert::regex($this->output, '#👋 Removed extension: [-_.a-zA-Z0-9/]+/' . $this->theExtension . '.so#'); } - $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";'])) + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";'])) ->mustRun() ->getOutput(); @@ -157,12 +162,12 @@ public function theExtensionShouldHaveBeenInstalled(): void Assert::contains($this->output, 'Extension has NOT been automatically enabled.'); if (Platform::isWindows()) { - Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#'); + Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_' . $this->theExtension . '.dll#'); return; } - Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/example_pie_extension.so#'); + Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/' . $this->theExtension . '.so#'); } #[Then('the extension should have been installed and enabled')] @@ -173,14 +178,14 @@ public function theExtensionShouldHaveBeenInstalledAndEnabled(): void Assert::contains($this->output, 'Extension is enabled and loaded'); if (Platform::isWindows()) { - Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#'); + Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_' . $this->theExtension . '.dll#'); return; } - Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/example_pie_extension.so#'); + Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/' . $this->theExtension . '.so#'); - $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";'])) + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";'])) ->mustRun() ->getOutput(); @@ -231,4 +236,18 @@ public function iShouldSeeThePackageRepositoryIsNotUsedByPie(): void Assert::notNull($this->output); Assert::notContains($this->output, 'Path repository (' . __DIR__ . ')'); } + + #[Given('I have libsodium on my system')] + public function iHaveLibsodiumOnMySystem(): void + { + (new Process(['apt-get', 'update'], timeout: 120))->mustRun(); + (new Process(['apt-get', '-y', 'install', 'libsodium-dev'], timeout: 120))->mustRun(); + } + + #[When('I install the sodium extension with PIE')] + public function iInstallTheSodiumExtensionWithPie(): void + { + $this->runPieCommand(['install', '--force', 'php/sodium']); + $this->theExtension = 'sodium'; + } } From b28712456f569152605d5a2296773cce55fb31d5 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 6 Nov 2025 17:39:02 +0000 Subject: [PATCH 05/12] Implement platform dependencies feature Behat tests --- test/behaviour/CliContext.php | 45 +++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 12964305..62a59c24 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -16,9 +16,10 @@ class CliContext implements Context { - private const PHP_BINARY = 'php'; - private string|null $output = null; - private int|null $exitCode = null; + private const PHP_BINARY = 'php'; + private string|null $output = null; + private string|null $errorOutput = null; + private int|null $exitCode = null; /** @var list */ private array $phpArguments = []; @@ -41,10 +42,12 @@ public function runPieCommand(array $command): void { $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, 'bin/pie'], $command); - $proc = (new Process($pieCommand, timeout: 120))->mustRun(); + $proc = new Process($pieCommand, timeout: 120); + $proc->run(); - $this->output = $proc->getOutput(); - $this->exitCode = $proc->getExitCode(); + $this->output = $proc->getOutput(); + $this->errorOutput = $proc->getErrorOutput(); + $this->exitCode = $proc->getExitCode(); } /** @phpstan-assert !null $this->output */ @@ -247,7 +250,35 @@ public function iHaveLibsodiumOnMySystem(): void #[When('I install the sodium extension with PIE')] public function iInstallTheSodiumExtensionWithPie(): void { - $this->runPieCommand(['install', '--force', 'php/sodium']); + $this->runPieCommand(['install', 'php/sodium']); $this->theExtension = 'sodium'; } + + #[Given('I do not have libsodium on my system')] + public function iDoNotHaveLibsodiumOnMySystem(): void + { + (new Process(['apt-get', '-y', '-m', 'remove', 'libsodium*'], timeout: 120))->run(); + } + + #[When('I display information about the sodium extension with PIE')] + public function iDisplayInformationAboutTheSodiumExtensionWithPie(): void + { + $this->runPieCommand(['info', 'php/sodium']); + $this->theExtension = 'sodium'; + } + + #[Then('the information should show that libsodium is a missing dependency')] + public function theInformationShouldShowThatLibsodiumIsAMissingDependency(): void + { + Assert::notNull($this->output); + Assert::contains($this->output, 'lib-sodium: * 🚫 (not installed)'); + } + + #[Then('the extension fails to install due to the missing library')] + public function theExtensionFailsToInstallDueToTheMissingLibrary(): void + { + Assert::notSame(0, $this->exitCode); + Assert::notNull($this->errorOutput); + Assert::regex($this->errorOutput, '#Cannot use php/sodium\'s latest version .* as it requires lib-sodium .* which is missing from your platform.#'); + } } From 74e35f453ce83ee491631f3fbe7426d919e39ff8 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 10 Nov 2025 17:46:48 +0000 Subject: [PATCH 06/12] Implemented feature tests for PHP project pie install --- .github/workflows/continuous-integration.yml | 8 +- features/bundled-php-extensions.feature | 7 +- test/assets/example-php-project/.gitignore | 1 + test/assets/example-php-project/composer.json | 9 ++ test/assets/example-php-project/composer.lock | 21 +++++ test/behaviour/CliContext.php | 84 ++++++++++++++++--- 6 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 test/assets/example-php-project/.gitignore create mode 100644 test/assets/example-php-project/composer.json create mode 100644 test/assets/example-php-project/composer.lock diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0b8e919d..8bb363fe 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -157,10 +157,14 @@ jobs: - uses: ramsey/composer-install@v3 - name: Run Behat on Windows if: matrix.operating-system == 'windows-latest' - run: vendor/bin/behat --tags="~@non-windows" + run: vendor/bin/behat --no-snippets --tags="~@non-windows" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run Behat on non-Windows if: matrix.operating-system != 'windows-latest' - run: sudo vendor/bin/behat + run: sudo vendor/bin/behat --no-snippets + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} coding-standards: runs-on: ubuntu-latest diff --git a/features/bundled-php-extensions.feature b/features/bundled-php-extensions.feature index cbc3a856..2a6f0d50 100644 --- a/features/bundled-php-extensions.feature +++ b/features/bundled-php-extensions.feature @@ -1,7 +1,12 @@ -Feature: Extensions for a PHP project can be installed with PIE +Feature: Bundled PHP extensions can be installed # pie install php/sodium Example: An extension normally bundled with PHP can be installed Given I have libsodium on my system When I install the sodium extension with PIE Then the extension should have been installed and enabled + + Example: A bundled extension installed with PIE can be uninstalled + Given I have the sodium extension installed with PIE + When I run a command to uninstall an extension + Then the extension should not be installed anymore diff --git a/test/assets/example-php-project/.gitignore b/test/assets/example-php-project/.gitignore new file mode 100644 index 00000000..22d0d82f --- /dev/null +++ b/test/assets/example-php-project/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/test/assets/example-php-project/composer.json b/test/assets/example-php-project/composer.json new file mode 100644 index 00000000..d4a25d50 --- /dev/null +++ b/test/assets/example-php-project/composer.json @@ -0,0 +1,9 @@ +{ + "name": "php-pie-test-project/php-pie-test-project", + "description": "Example PHP project for test cases", + "type": "project", + "require": { + "php": "^8.0", + "ext-example_pie_extension": "^2.0" + } +} diff --git a/test/assets/example-php-project/composer.lock b/test/assets/example-php-project/composer.lock new file mode 100644 index 00000000..de41cd27 --- /dev/null +++ b/test/assets/example-php-project/composer.lock @@ -0,0 +1,21 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2e7e51dfd351f870a1a657e3e49836ad", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.0", + "ext-example_pie_extension": "^2.0" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 62a59c24..5547736b 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -13,6 +13,9 @@ use Webmozart\Assert\Assert; use function array_merge; +use function assert; +use function realpath; +use function sprintf; class CliContext implements Context { @@ -21,9 +24,10 @@ class CliContext implements Context private string|null $errorOutput = null; private int|null $exitCode = null; /** @var list */ - private array $phpArguments = []; - - private string $theExtension = 'example_pie_extension'; + private array $phpArguments = []; + private string $theExtension = 'example_pie_extension'; + private string $thePackage = 'asgrim/example-pie-extension'; + private string|null $workingDirectory = null; #[When('I run a command to download the latest version of an extension')] public function iRunACommandToDownloadTheLatestVersionOfAnExtension(): void @@ -42,6 +46,11 @@ public function runPieCommand(array $command): void { $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, 'bin/pie'], $command); + if ($this->workingDirectory !== null) { + $pieCommand[] = '--working-dir'; + $pieCommand[] = $this->workingDirectory; + } + $proc = new Process($pieCommand, timeout: 120); $proc->run(); @@ -53,7 +62,24 @@ public function runPieCommand(array $command): void /** @phpstan-assert !null $this->output */ private function assertCommandSuccessful(): void { - Assert::same(0, $this->exitCode); + Assert::same( + 0, + $this->exitCode, + sprintf( + <<<'EOF' + Last command was not successful - exit code was: %d. + + Output: + %s + + Error output: + %s + EOF, + $this->exitCode, + $this->output, + $this->errorOutput, + ), + ); Assert::notNull($this->output); } @@ -121,22 +147,24 @@ public function theExtensionShouldHaveBeenBuiltWithOptions(): void #[Given('an extension was previously installed and enabled')] public function iRunACommandToInstallAnExtension(): void { - $this->runPieCommand(['install', 'asgrim/example-pie-extension']); $this->theExtension = 'example_pie_extension'; + $this->thePackage = 'asgrim/example-pie-extension'; + $this->runPieCommand(['install', $this->thePackage]); } #[When('I run a command to install an extension without enabling it')] public function iRunACommandToInstallAnExtensionWithoutEnabling(): void { - $this->runPieCommand(['install', 'asgrim/example-pie-extension', '--skip-enable-extension']); $this->theExtension = 'example_pie_extension'; + $this->thePackage = 'asgrim/example-pie-extension'; + $this->runPieCommand(['install', $this->thePackage, '--skip-enable-extension']); } #[When('I run a command to uninstall an extension')] public function iRunACommandToUninstallAnExtension(): void { - $this->runPieCommand(['uninstall', 'asgrim/example-pie-extension']); - $this->theExtension = 'example_pie_extension'; + assert($this->thePackage !== ''); + $this->runPieCommand(['uninstall', $this->thePackage]); } #[Then('the extension should not be installed anymore')] @@ -248,10 +276,12 @@ public function iHaveLibsodiumOnMySystem(): void } #[When('I install the sodium extension with PIE')] + #[Given('I have the sodium extension installed with PIE')] public function iInstallTheSodiumExtensionWithPie(): void { - $this->runPieCommand(['install', 'php/sodium']); $this->theExtension = 'sodium'; + $this->thePackage = 'php/sodium'; + $this->runPieCommand(['install', $this->thePackage]); } #[Given('I do not have libsodium on my system')] @@ -263,8 +293,9 @@ public function iDoNotHaveLibsodiumOnMySystem(): void #[When('I display information about the sodium extension with PIE')] public function iDisplayInformationAboutTheSodiumExtensionWithPie(): void { - $this->runPieCommand(['info', 'php/sodium']); $this->theExtension = 'sodium'; + $this->thePackage = 'php/sodium'; + $this->runPieCommand(['info', $this->thePackage]); } #[Then('the information should show that libsodium is a missing dependency')] @@ -281,4 +312,37 @@ public function theExtensionFailsToInstallDueToTheMissingLibrary(): void Assert::notNull($this->errorOutput); Assert::regex($this->errorOutput, '#Cannot use php/sodium\'s latest version .* as it requires lib-sodium .* which is missing from your platform.#'); } + + #[Given('I am in a PHP project that has missing extensions')] + public function iAmInAPHPProjectThatHasMissingExtensions(): void + { + $this->runPieCommand(['uninstall', 'asgrim/example-pie-extension']); + + $this->runPieCommand(['show']); + $this->assertCommandSuccessful(); + Assert::notContains($this->output, 'example_pie_extension'); + + $examplePhpProject = (string) realpath(__DIR__ . '/../assets/example-php-project'); + assert($examplePhpProject !== ''); + + $this->workingDirectory = $examplePhpProject; + } + + #[When('I run a command to install the extensions')] + public function iRunACommandToInstallTheExtensions(): void + { + $this->runPieCommand(['install', '--allow-non-interactive-project-install']); + + $this->assertCommandSuccessful(); + } + + #[Then('I should see all the extensions are now installed')] + public function iShouldSeeAllTheExtensionsAreNowInstalled(): void + { + $this->workingDirectory = null; + + $this->runPieCommand(['show']); + $this->assertCommandSuccessful(); + Assert::contains($this->output, 'example_pie_extension'); + } } From 3018777f7a3ae268367f4ca451229a64ed6cf56f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 11 Nov 2025 13:57:56 +0000 Subject: [PATCH 07/12] Display more output for some failures --- .github/workflows/continuous-integration.yml | 9 ++++++++- test/behaviour/CliContext.php | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 8bb363fe..c877fac0 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -150,10 +150,17 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: intl, sodium, zip + extensions: none, intl, zip + tools: composer env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/checkout@v5 + - name: Testing + run: | + php -m + bin/pie show --all + which composer + composer -v - uses: ramsey/composer-install@v3 - name: Run Behat on Windows if: matrix.operating-system == 'windows-latest' diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 5547736b..2de21f38 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -182,7 +182,11 @@ public function theExtensionShouldNotBeInstalled(): void ->mustRun() ->getOutput(); - Assert::same($isExtEnabled, 'no'); + Assert::same( + $isExtEnabled, + 'no', + sprintf("Failed to remove extension.\n\nOutput:\n%s\n\nError output:\n%s\n", $this->output, $this->errorOutput), + ); } #[Then('the extension should have been installed')] @@ -302,7 +306,11 @@ public function iDisplayInformationAboutTheSodiumExtensionWithPie(): void public function theInformationShouldShowThatLibsodiumIsAMissingDependency(): void { Assert::notNull($this->output); - Assert::contains($this->output, 'lib-sodium: * 🚫 (not installed)'); + Assert::contains( + $this->output, + 'lib-sodium: * 🚫 (not installed)', + sprintf("Could not find missing lib-sodium.\n\nOutput:\n%s\n\nError output:\n%s\n", $this->output, $this->errorOutput), + ); } #[Then('the extension fails to install due to the missing library')] @@ -310,7 +318,11 @@ public function theExtensionFailsToInstallDueToTheMissingLibrary(): void { Assert::notSame(0, $this->exitCode); Assert::notNull($this->errorOutput); - Assert::regex($this->errorOutput, '#Cannot use php/sodium\'s latest version .* as it requires lib-sodium .* which is missing from your platform.#'); + Assert::regex( + $this->errorOutput, + '#Cannot use php/sodium\'s latest version .* as it requires lib-sodium .* which is missing from your platform.#', + sprintf("Did not detect missing lib-sodium correctly.\n\nOutput:\n%s\n\nError output:\n%s\n", $this->output, $this->errorOutput), + ); } #[Given('I am in a PHP project that has missing extensions')] From 4bea38646c0d52797e88048d5c8aaddaef195935 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 11 Nov 2025 15:37:59 +0000 Subject: [PATCH 08/12] Use Dockerfile for reproducible Behat test environment --- .github/pie-behaviour-tests/Dockerfile | 43 ++++++++++++++++++++ .github/workflows/continuous-integration.yml | 28 ++----------- behat.php | 19 +++++++++ 3 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 .github/pie-behaviour-tests/Dockerfile diff --git a/.github/pie-behaviour-tests/Dockerfile b/.github/pie-behaviour-tests/Dockerfile new file mode 100644 index 00000000..d038dd1c --- /dev/null +++ b/.github/pie-behaviour-tests/Dockerfile @@ -0,0 +1,43 @@ +# An approximately reproducible, but primarily isolated, environment for +# running the acceptance tests: +# +# docker buildx build --file .github/pie-behaviour-tests/Dockerfile -t pie-behat-test . +# docker run --volume .:/github/workspace -ti pie-behat-test +FROM ubuntu:24.04 + +# Add the `unzip` package which PIE uses to extract .zip files +RUN export DEBIAN_FRONTEND="noninteractive"; \ + set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + unzip curl jq wget g++ gcc make autoconf libtool bison re2c pkg-config \ + ca-certificates libxml2-dev libssl-dev; \ + update-ca-certificates ; \ + rm -rf /var/lib/apt/lists/* + +# Compile PHP +ARG PHP_VERSION=8.4 +RUN mkdir -p /usr/local/src/php; \ + cd /usr/local/src/php; \ + FULL_LATEST_VERSION=`curl -fsSL "https://www.php.net/releases/index.php?json&max=1&version=$PHP_VERSION" | jq -r '.[].source[]|select(.filename|endswith(".gz")).filename'`; \ + wget -O php.tgz "https://www.php.net/distributions/$FULL_LATEST_VERSION" ; \ + tar zxf php.tgz ; \ + rm php.tgz ; \ + ls -l ; \ + cd * ; \ + ls -l ; \ + ./buildconf --force ; \ + ./configure --without-sqlite3 --disable-pdo --disable-dom --disable-xml --disable-xmlreader --disable-xmlwriter --disable-json --with-openssl ; \ + make -j$(nproc) ; \ + make install + +RUN touch /usr/local/lib/php.ini + +COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie +RUN pie install xdebug/xdebug + +WORKDIR /github/workspace + +ENV USING_PIE_BEHAT_DOCKERFILE=1 +ENTRYPOINT ["php", "vendor/bin/behat"] +CMD ["--no-snippets"] diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c877fac0..acff0875 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -138,38 +138,18 @@ jobs: matrix: operating-system: - ubuntu-latest - - windows-latest php-versions: - '8.1' - '8.2' - '8.3' - '8.4' - - '8.5' steps: - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: none, intl, zip - tools: composer - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/checkout@v5 - - name: Testing - run: | - php -m - bin/pie show --all - which composer - composer -v - uses: ramsey/composer-install@v3 - - name: Run Behat on Windows - if: matrix.operating-system == 'windows-latest' - run: vendor/bin/behat --no-snippets --tags="~@non-windows" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run Behat on non-Windows - if: matrix.operating-system != 'windows-latest' - run: sudo vendor/bin/behat --no-snippets + - name: Build + run: docker buildx build --file .github/pie-behaviour-tests/Dockerfile --build-arg PHP_VERSION=${{ matrix.php-versions }} -t pie-behat-test . + - name: Run Behat + run: docker run --volume .:/github/workspace pie-behat-test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/behat.php b/behat.php index 8202fe0a..c5fa1a54 100644 --- a/behat.php +++ b/behat.php @@ -8,6 +8,25 @@ use Behat\Config\Suite; use Php\PieBehaviourTest\CliContext; +if (getenv('USING_PIE_BEHAT_DOCKERFILE') !== '1') { + echo <<<'HELP' +⚠️ ⚠️ ⚠️ STOP! ⚠️ ⚠️ ⚠️ + +This test suite tinkers with your system, and has lots of expectations about +the system it is running on, so we HIGHLY recommend you run it using the +provided Dockerfile: + + docker buildx build --file .github/actions/pie-behaviour-tests/Dockerfile -t pie-behat-test . + docker run --volume .:/github/workspace -ti pie-behat-test + +If you are really sure, and accept that the test suite installs/uninstalls +stuff from your system, and might break your stuff, set +USING_PIE_BEHAT_DOCKERFILE=1 in your environment. + +HELP; + exit(1); +} + $profile = (new Profile('default')) ->withSuite( (new Suite('default')) From 81f9633417a877eb609a850b22b46ea37bf76a70 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 11 Nov 2025 17:21:28 +0000 Subject: [PATCH 09/12] Implemented features/install-in-pie-project.feature as Behat test --- .github/pie-behaviour-tests/Dockerfile | 8 +++++++- test/behaviour/CliContext.php | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/pie-behaviour-tests/Dockerfile b/.github/pie-behaviour-tests/Dockerfile index d038dd1c..a13267c7 100644 --- a/.github/pie-behaviour-tests/Dockerfile +++ b/.github/pie-behaviour-tests/Dockerfile @@ -3,7 +3,11 @@ # # docker buildx build --file .github/pie-behaviour-tests/Dockerfile -t pie-behat-test . # docker run --volume .:/github/workspace -ti pie-behat-test -FROM ubuntu:24.04 +FROM alpine/git:v2.49.1 AS clone_ext_repo + +RUN cd / && git clone https://github.com/asgrim/example-pie-extension.git + +FROM ubuntu:24.04 AS default # Add the `unzip` package which PIE uses to extract .zip files RUN export DEBIAN_FRONTEND="noninteractive"; \ @@ -36,6 +40,8 @@ RUN touch /usr/local/lib/php.ini COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie RUN pie install xdebug/xdebug +COPY --from=clone_ext_repo /example-pie-extension /example-pie-extension + WORKDIR /github/workspace ENV USING_PIE_BEHAT_DOCKERFILE=1 diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 2de21f38..5156b2aa 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -357,4 +357,21 @@ public function iShouldSeeAllTheExtensionsAreNowInstalled(): void $this->assertCommandSuccessful(); Assert::contains($this->output, 'example_pie_extension'); } + + #[Given('I am in a PIE project')] + public function iAmInAPIEProject(): void + { + $examplePieProject = (string) realpath('/example-pie-extension'); + assert($examplePieProject !== ''); + + $this->workingDirectory = $examplePieProject; + } + + #[When('I run a command to install the extension')] + public function iRunACommandToInstallTheExtension(): void + { + $this->theExtension = 'example_pie_extension'; + $this->thePackage = 'asgrim/example-pie-extension'; + $this->runPieCommand(['install']); + } } From 8a0d5438ccb65efb6ca6a6a06a24d79f2216a262 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 14 Nov 2025 11:43:55 +0000 Subject: [PATCH 10/12] Implemented self-update feature check --- .github/pie-behaviour-tests/Dockerfile | 15 ++++++++++- .github/workflows/continuous-integration.yml | 2 +- features/self-update.feature | 2 +- test/behaviour/CliContext.php | 27 +++++++++++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.github/pie-behaviour-tests/Dockerfile b/.github/pie-behaviour-tests/Dockerfile index a13267c7..6bb7a48e 100644 --- a/.github/pie-behaviour-tests/Dockerfile +++ b/.github/pie-behaviour-tests/Dockerfile @@ -1,12 +1,18 @@ # An approximately reproducible, but primarily isolated, environment for # running the acceptance tests: # -# docker buildx build --file .github/pie-behaviour-tests/Dockerfile -t pie-behat-test . +# GITHUB_TOKEN=$(composer config --global --auth github-oauth.github.com) docker buildx build --file .github/pie-behaviour-tests/Dockerfile --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN -t pie-behat-test . # docker run --volume .:/github/workspace -ti pie-behat-test FROM alpine/git:v2.49.1 AS clone_ext_repo RUN cd / && git clone https://github.com/asgrim/example-pie-extension.git +FROM boxproject/box:4.6.10 AS build_pie_phar + +RUN apk add git +COPY . /app +RUN cd /app && /box.phar compile + FROM ubuntu:24.04 AS default # Add the `unzip` package which PIE uses to extract .zip files @@ -44,6 +50,13 @@ COPY --from=clone_ext_repo /example-pie-extension /example-pie-extension WORKDIR /github/workspace +COPY --from=composer /usr/bin/composer /usr/bin/composer +RUN --mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \ + composer config --global --auth github-oauth.github.com $GITHUB_TOKEN + +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie.original + ENV USING_PIE_BEHAT_DOCKERFILE=1 ENTRYPOINT ["php", "vendor/bin/behat"] CMD ["--no-snippets"] diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index acff0875..8f635b9c 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -147,7 +147,7 @@ jobs: - uses: actions/checkout@v5 - uses: ramsey/composer-install@v3 - name: Build - run: docker buildx build --file .github/pie-behaviour-tests/Dockerfile --build-arg PHP_VERSION=${{ matrix.php-versions }} -t pie-behat-test . + run: docker buildx build --file .github/pie-behaviour-tests/Dockerfile --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN --build-arg PHP_VERSION=${{ matrix.php-versions }} -t pie-behat-test . - name: Run Behat run: docker run --volume .:/github/workspace pie-behat-test env: diff --git a/features/self-update.feature b/features/self-update.feature index 0a6a65b8..b23f2b71 100644 --- a/features/self-update.feature +++ b/features/self-update.feature @@ -3,7 +3,7 @@ Feature: PIE can update itself and verify it is authentic # pie self-update Example: PIE can update itself Given I have an old version of PIE - When I update PIE to the latest stable version + When I update PIE to the latest version Then I should see I have been updated to the latest version # pie self-verify diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 5156b2aa..97b6901a 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -14,12 +14,15 @@ use function array_merge; use function assert; +use function copy; use function realpath; use function sprintf; class CliContext implements Context { private const PHP_BINARY = 'php'; + private const PIE_BINARY = '/usr/local/bin/pie'; + private const PIE_BINARY_BACKUP = '/usr/local/bin/pie.original'; private string|null $output = null; private string|null $errorOutput = null; private int|null $exitCode = null; @@ -44,7 +47,7 @@ public function iRunACommandToDownloadSpecificVersionOfAnExtension(string $versi /** @param list $command */ public function runPieCommand(array $command): void { - $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, 'bin/pie'], $command); + $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, self::PIE_BINARY], $command); if ($this->workingDirectory !== null) { $pieCommand[] = '--working-dir'; @@ -374,4 +377,26 @@ public function iRunACommandToInstallTheExtension(): void $this->thePackage = 'asgrim/example-pie-extension'; $this->runPieCommand(['install']); } + + #[Given('I have an old version of PIE')] + public function iHaveAnOldVersionOfPIE(): void + { + // noop + } + + #[When('I update PIE to the latest version')] + public function iUpdatePIEToTheLatestNightlyVersion(): void + { + $this->runPieCommand(['self-update', '--nightly', '-v']); + + copy(self::PIE_BINARY_BACKUP, self::PIE_BINARY); + } + + #[Then('I should see I have been updated to the latest version')] + public function iShouldSeeIHaveBeenUpdatedToTheLatestVersion(): void + { + $this->assertCommandSuccessful(); + Assert::contains($this->output, '✅ Verified the new PIE version'); + Assert::contains($this->output, '✅ PIE has been upgraded to nightly'); + } } From bfb830d634606edf12b89c04a2cfa75678d0bbc1 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 14 Nov 2025 11:54:42 +0000 Subject: [PATCH 11/12] Ensure the PHAR we build in Behat is not verifiable --- .github/pie-behaviour-tests/Dockerfile | 4 ++-- test/behaviour/CliContext.php | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/pie-behaviour-tests/Dockerfile b/.github/pie-behaviour-tests/Dockerfile index 6bb7a48e..529d5bdc 100644 --- a/.github/pie-behaviour-tests/Dockerfile +++ b/.github/pie-behaviour-tests/Dockerfile @@ -11,7 +11,7 @@ FROM boxproject/box:4.6.10 AS build_pie_phar RUN apk add git COPY . /app -RUN cd /app && /box.phar compile +RUN cd /app && touch creating_this_means_phar_will_never_be_verified && /box.phar compile FROM ubuntu:24.04 AS default @@ -43,7 +43,7 @@ RUN mkdir -p /usr/local/src/php; \ RUN touch /usr/local/lib/php.ini -COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie +COPY --from=ghcr.io/php/pie:bin /pie /usr/local/bin/pie RUN pie install xdebug/xdebug COPY --from=clone_ext_repo /example-pie-extension /example-pie-extension diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 97b6901a..05f95df4 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -399,4 +399,25 @@ public function iShouldSeeIHaveBeenUpdatedToTheLatestVersion(): void Assert::contains($this->output, '✅ Verified the new PIE version'); Assert::contains($this->output, '✅ PIE has been upgraded to nightly'); } + + #[Given('I have a pie.phar built on a nasty hacker\'s machine')] + public function iHaveAPiePharBuiltOnANastyHackerSMachine(): void + { + // noop - the pie.phar built in this does not have attestations + } + + #[When('I verify my PIE installation')] + public function iVerifyMyPIEInstallation(): void + { + $this->runPieCommand(['self-verify']); + } + + #[Then('I should see it has failed verification')] + public function iShouldSeeItHasFailedVerification(): void + { + Assert::same($this->exitCode, 1); + + Assert::notNull($this->errorOutput); + Assert::contains($this->errorOutput, '❌ Failed to verify the pie.phar release'); + } } From d771cdc74c0907e9a893cda65dab4ed36bf3cb24 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 14 Nov 2025 12:15:49 +0000 Subject: [PATCH 12/12] Ensure all the tags are available --- .github/workflows/continuous-integration.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 8f635b9c..31b1690b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -145,8 +145,16 @@ jobs: - '8.4' steps: - uses: actions/checkout@v5 + with: + fetch-depth: 0 + # Fixes `git describe` picking the wrong tag - see https://github.com/php/pie/issues/307 + - run: git fetch --tags --force + # Ensure some kind of previous tag exists, otherwise box fails + - run: git describe --tags HEAD || git tag 0.0.0 - uses: ramsey/composer-install@v3 - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: docker buildx build --file .github/pie-behaviour-tests/Dockerfile --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN --build-arg PHP_VERSION=${{ matrix.php-versions }} -t pie-behat-test . - name: Run Behat run: docker run --volume .:/github/workspace pie-behat-test