1
0
mirror of https://github.com/php/pie.git synced 2026-03-23 23:12:17 +01:00

Merge pull request #417 from asgrim/behat-feature-improvements

Behat feature description improvements
This commit is contained in:
James Titcumb
2025-11-14 13:00:29 +00:00
committed by GitHub
15 changed files with 439 additions and 37 deletions

62
.github/pie-behaviour-tests/Dockerfile vendored Normal file
View File

@@ -0,0 +1,62 @@
# An approximately reproducible, but primarily isolated, environment for
# running the acceptance tests:
#
# 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 && touch creating_this_means_phar_will_never_be_verified && /box.phar compile
FROM ubuntu:24.04 AS default
# 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/local/bin/pie
RUN pie install xdebug/xdebug
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"]

View File

@@ -138,29 +138,28 @@ 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
- uses: actions/checkout@v5
with:
php-version: ${{ matrix.php-versions }}
extensions: intl, sodium, zip
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v5
- uses: ramsey/composer-install@v3
- name: Run Behat on Windows
if: matrix.operating-system == 'windows-latest'
run: vendor/bin/behat --tags="~@non-windows"
- name: Run Behat on non-Windows
if: matrix.operating-system != 'windows-latest'
run: sudo vendor/bin/behat
coding-standards:
runs-on: ubuntu-latest

View File

@@ -8,6 +8,25 @@ use Behat\Config\Profile;
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'))

View File

@@ -0,0 +1,12 @@
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

View File

@@ -1,9 +1,11 @@
Feature: Extensions can be installed with PIE
# pie download <ext>
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 <ext>:<version>
Scenario Outline: A version matching the requested constraint can be downloaded
When I run a command to download version "<constraint>" of an extension
Then version "<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 <ext>: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 <ext>
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 <ext> --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 <ext> --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 <ext>
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,13 @@
Feature: Platform dependencies are checked when installing
# pie info <ext>
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 <ext>
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

View File

@@ -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 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

View File

@@ -1,6 +1,7 @@
Feature: Extensions can be uninstalled with PIE
# pie uninstall <ext>
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

View File

@@ -0,0 +1 @@
vendor

View File

@@ -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"
}
}

View File

@@ -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"
}

View File

@@ -13,14 +13,24 @@ use Symfony\Component\Process\Process;
use Webmozart\Assert\Assert;
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 string|null $output = null;
private int|null $exitCode = null;
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;
/** @var list<string> */
private array $phpArguments = [];
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
@@ -37,18 +47,42 @@ class CliContext implements Context
/** @param list<non-empty-string> $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);
$proc = (new Process($pieCommand))->mustRun();
if ($this->workingDirectory !== null) {
$pieCommand[] = '--working-dir';
$pieCommand[] = $this->workingDirectory;
}
$this->output = $proc->getOutput();
$this->exitCode = $proc->getExitCode();
$proc = new Process($pieCommand, timeout: 120);
$proc->run();
$this->output = $proc->getOutput();
$this->errorOutput = $proc->getErrorOutput();
$this->exitCode = $proc->getExitCode();
}
/** @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);
}
@@ -113,16 +147,27 @@ class CliContext implements Context
}
#[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';
$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->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']);
assert($this->thePackage !== '');
$this->runPieCommand(['uninstall', $this->thePackage]);
}
#[Then('the extension should not be installed anymore')]
@@ -131,16 +176,20 @@ class CliContext implements Context
$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();
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')]
@@ -148,17 +197,33 @@ class CliContext implements Context
{
$this->assertCommandSuccessful();
Assert::contains($this->output, 'Extension is enabled and loaded');
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#');
}
$isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";']))
#[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()) {
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/]+/' . $this->theExtension . '.so#');
$isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";']))
->mustRun()
->getOutput();
@@ -209,4 +274,150 @@ class CliContext implements Context
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')]
#[Given('I have the sodium extension installed with PIE')]
public function iInstallTheSodiumExtensionWithPie(): void
{
$this->theExtension = 'sodium';
$this->thePackage = 'php/sodium';
$this->runPieCommand(['install', $this->thePackage]);
}
#[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->theExtension = 'sodium';
$this->thePackage = 'php/sodium';
$this->runPieCommand(['info', $this->thePackage]);
}
#[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)',
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')]
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.#',
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')]
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');
}
#[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']);
}
#[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');
}
#[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');
}
}