diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 46bd27e0..0860038e 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -54,6 +54,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Resolve, [], // Configure options are not needed for resolve only null, + false, // setting up INI not needed for build ), ); @@ -74,6 +75,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Build, $configureOptionsValues, CommandHelper::determinePhpizePathFromInputs($input), + false, // setting up INI not needed for build ), ); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 68b55d2c..8e25165b 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -38,6 +38,7 @@ final class CommandHelper private const OPTION_WITH_PHP_PATH = 'with-php-path'; private const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path'; private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs'; + private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension'; /** @psalm-suppress UnusedConstructor */ private function __construct() @@ -79,6 +80,12 @@ public static function configureDownloadBuildInstallOptions(Command $command): v InputOption::VALUE_REQUIRED, 'The path to the `phpize` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHPIZE_PATH . '=/usr/bin/phpize7.4', ); + $command->addOption( + self::OPTION_SKIP_ENABLE_EXTENSION, + null, + InputOption::VALUE_NONE, + 'Specify this to skip attempting to enable the extension in php.ini', + ); self::configurePhpConfigOptions($command); @@ -154,6 +161,11 @@ public static function determineTargetPlatformFromInputs(InputInterface $input, return $targetPlatform; } + public static function determineAttemptToSetupIniFile(InputInterface $input): bool + { + return ! $input->hasOption(self::OPTION_SKIP_ENABLE_EXTENSION) || ! $input->getOption(self::OPTION_SKIP_ENABLE_EXTENSION); + } + public static function determinePhpizePathFromInputs(InputInterface $input): PhpizePath|null { if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) { diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index a6c237d4..f6616290 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -56,6 +56,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Download, [], // Configure options are not needed for download only null, + false, // setting up INI not needed for download ), ); diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php index 0d3a6cc3..9f1f806f 100644 --- a/src/Command/InfoCommand.php +++ b/src/Command/InfoCommand.php @@ -54,6 +54,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Resolve, [], // Configure options are not needed for resolve only null, + false, // setting up INI not needed for info ), ); diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index ca293165..aca34af2 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -59,6 +59,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Resolve, [], // Configure options are not needed for resolve only null, + false, // setting up INI not needed for resolve step ), ); @@ -79,6 +80,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Install, $configureOptionsValues, CommandHelper::determinePhpizePathFromInputs($input), + CommandHelper::determineAttemptToSetupIniFile($input), ), ); diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index c6944720..39e62fee 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -77,6 +77,7 @@ public function __invoke( $downloadedPackage, $composerRequest->targetPlatform, $output, + $composerRequest->attemptToSetupIniFile, ), ); } diff --git a/src/ComposerIntegration/PieComposerRequest.php b/src/ComposerIntegration/PieComposerRequest.php index 536a3471..2679bdd9 100644 --- a/src/ComposerIntegration/PieComposerRequest.php +++ b/src/ComposerIntegration/PieComposerRequest.php @@ -24,6 +24,7 @@ public function __construct( public readonly PieOperation $operation, public readonly array $configureOptions, public readonly PhpizePath|null $phpizePath, + public readonly bool $attemptToSetupIniFile, ) { } } diff --git a/src/Container.php b/src/Container.php index 263fd418..1c181a9a 100644 --- a/src/Container.php +++ b/src/Container.php @@ -19,6 +19,7 @@ use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\Downloading\PackageReleaseAssets; +use Php\Pie\Installing\Ini; use Php\Pie\Installing\Install; use Php\Pie\Installing\UnixInstall; use Php\Pie\Installing\WindowsInstall; @@ -71,6 +72,19 @@ static function (ContainerInterface $container): Build { }, ); + $container->singleton( + Ini\SetupIniApproach::class, + static function (ContainerInterface $container): Ini\SetupIniApproach { + return new Ini\PickBestSetupIniApproach([ + $container->get(Ini\PreCheckExtensionAlreadyLoaded::class), + $container->get(Ini\OndrejPhpenmod::class), + $container->get(Ini\DockerPhpExtEnable::class), + $container->get(Ini\StandardAdditionalPhpIniDirectory::class), + $container->get(Ini\StandardSinglePhpIni::class), + ]); + }, + ); + $container->singleton( Install::class, static function (ContainerInterface $container): Install { diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index b4d3f7c7..d09a8a0d 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -47,6 +47,7 @@ public function __construct( public readonly string|null $buildPath, public readonly array|null $compatibleOsFamilies, public readonly array|null $incompatibleOsFamilies, + public readonly int $priority, ) { } @@ -93,6 +94,7 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $buildPath, self::convertInputStringsToOperatingSystemFamilies($compatibleOsFamilies), self::convertInputStringsToOperatingSystemFamilies($incompatibleOsFamilies), + $phpExtOptions['priority'] ?? 80, ); } diff --git a/src/Installing/Ini/AddExtensionToTheIniFile.php b/src/Installing/Ini/AddExtensionToTheIniFile.php new file mode 100644 index 00000000..73469e9e --- /dev/null +++ b/src/Installing/Ini/AddExtensionToTheIniFile.php @@ -0,0 +1,102 @@ +writeln( + sprintf( + 'PHP is configured to use %s, but it is not writable by PIE.', + $ini, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + $originalIniContent = file_get_contents($ini); + + if (! is_string($originalIniContent)) { + $output->writeln( + sprintf( + 'Tried making a backup of %s but could not read it, aborting enablement of extension', + $ini, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + try { + file_put_contents( + $ini, + $originalIniContent . $this->iniFileContent($package), + ); + $output->writeln( + sprintf( + 'Enabled extension %s in the INI file %s', + $package->extensionName->name(), + $ini, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + if ($additionalEnableStep !== null && ! $additionalEnableStep()) { + return false; + } + + $phpBinaryPath->assertExtensionIsLoadedInRuntime($package->extensionName, $output); + + return true; + } catch (Throwable $anything) { + file_put_contents($ini, $originalIniContent); + + $output->writeln(sprintf( + 'Something went wrong enabling the %s extension: %s', + $package->extensionName->name(), + $anything->getMessage(), + )); + + return false; + } + } + + /** @return non-empty-string */ + private function iniFileContent(Package $package): string + { + return PHP_EOL + . '; PIE automatically added this to enable the ' . $package->name . ' extension' . PHP_EOL + . '; priority=' . $package->priority . PHP_EOL + . ($package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension') + . '=' + . $package->extensionName->name() . PHP_EOL; + } +} diff --git a/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php new file mode 100644 index 00000000..eec39db0 --- /dev/null +++ b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php @@ -0,0 +1,84 @@ +writeln( + sprintf( + 'PHP is configured to use %s, but it did not exist, or is not readable by PIE.', + $iniFile, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + if (($this->isExtensionAlreadyInTheIniFile)($iniFile, $downloadedPackage->package->extensionName)) { + $output->writeln( + sprintf( + 'Extension is already enabled in the INI file %s', + $iniFile, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + if ($additionalEnableStep !== null && ! $additionalEnableStep()) { + return false; + } + + try { + $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime($downloadedPackage->package->extensionName, $output); + + return true; + } catch (Throwable $anything) { + $output->writeln(sprintf( + 'Something went wrong verifying the %s extension is enabled: %s', + $downloadedPackage->package->extensionName->name(), + $anything->getMessage(), + )); + + return false; + } + } + + return ($this->addExtensionToTheIniFile)( + $iniFile, + $downloadedPackage->package, + $targetPlatform->phpBinaryPath, + $output, + $additionalEnableStep, + ); + } +} diff --git a/src/Installing/Ini/DockerPhpExtEnable.php b/src/Installing/Ini/DockerPhpExtEnable.php new file mode 100644 index 00000000..2878af4d --- /dev/null +++ b/src/Installing/Ini/DockerPhpExtEnable.php @@ -0,0 +1,89 @@ +dockerPhpExtEnablePath() !== null; + } + + public function setup( + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + BinaryFile $binaryFile, + OutputInterface $output, + ): bool { + $dockerPhpExtEnable = $this->dockerPhpExtEnablePath(); + + if ($dockerPhpExtEnable === null) { + return false; + } + + try { + $enableOutput = Process::run([$dockerPhpExtEnable, $downloadedPackage->package->extensionName->name()]); + } catch (ProcessFailedException $processFailed) { + $output->writeln( + sprintf( + 'Could not enable extension %s using %s. Exception was: %s', + $downloadedPackage->package->extensionName->name(), + $this->dockerPhpExtEnableName, + $processFailed->getMessage(), + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + try { + $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime( + $downloadedPackage->package->extensionName, + $output, + ); + + return true; + } catch (ExtensionIsNotLoaded) { + $output->writeln( + sprintf( + 'Asserting that extension %s was enabled using %s failed. Output was: %s', + $downloadedPackage->package->extensionName->name(), + $this->dockerPhpExtEnableName, + $enableOutput !== '' ? $enableOutput : '(empty)', + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + } + + private function dockerPhpExtEnablePath(): string|null + { + try { + return Process::run(['which', $this->dockerPhpExtEnableName]); + } catch (ProcessFailedException) { + return null; + } + } +} diff --git a/src/Installing/Ini/IsExtensionAlreadyInTheIniFile.php b/src/Installing/Ini/IsExtensionAlreadyInTheIniFile.php new file mode 100644 index 00000000..cfe2c8bf --- /dev/null +++ b/src/Installing/Ini/IsExtensionAlreadyInTheIniFile.php @@ -0,0 +1,52 @@ +readIniFile($iniFilePath); + + return in_array($extensionName->name(), array_merge($loadedExts['extensions'], $loadedExts['zend_extensions'])); + } + + /** @return array{extensions: list, zend_extensions: list} */ + private function readIniFile(string $iniFilePath): array + { + $iniFileContentLines = file($iniFilePath); + + $extensions = []; + $zendExtensions = []; + foreach ($iniFileContentLines as $line) { + $lineIni = parse_ini_string($line); + + if (array_key_exists('extension', $lineIni) && is_string($lineIni['extension']) && $lineIni['extension'] !== '') { + $extensions[] = $lineIni['extension']; + } + + if (! array_key_exists('zend_extension', $lineIni) || ! is_string($lineIni['zend_extension']) || $lineIni['zend_extension'] === '') { + continue; + } + + $zendExtensions[] = $lineIni['zend_extension']; + } + + return [ + 'extensions' => $extensions, + 'zend_extensions' => $zendExtensions, + ]; + } +} diff --git a/src/Installing/Ini/OndrejPhpenmod.php b/src/Installing/Ini/OndrejPhpenmod.php new file mode 100644 index 00000000..051dfb65 --- /dev/null +++ b/src/Installing/Ini/OndrejPhpenmod.php @@ -0,0 +1,194 @@ +phpenmodPath() !== null; + } + + public function setup( + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + BinaryFile $binaryFile, + OutputInterface $output, + ): bool { + $phpenmodPath = $this->phpenmodPath(); + + /** In practice, this shouldn't happen since {@see canBeUsed()} checks this */ + if ($phpenmodPath === null) { + return false; + } + + // the Ondrej repo uses an additional php.ini directory, if this isn't set, we may not actually be using Ondrej repo for this particular PHP install + $additionalPhpIniPath = $targetPlatform->phpBinaryPath->additionalIniDirectory(); + + if ($additionalPhpIniPath === null) { + $output->writeln( + 'Additional INI file path was not set - may not be Ondrej PHP repo', + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + // Cursory check for the expected PHP INI directory; this is another indication we're using the Ondrej repo + if (preg_match('#/etc/php/\d\.\d/[a-z-_]+/conf.d#', $additionalPhpIniPath) !== 1) { + $output->writeln( + sprintf( + 'Warning: additional INI file path was not in the expected format (/etc/php/{version}/{sapi}/conf.d). Path was: %s', + $additionalPhpIniPath, + ), + OutputInterface::VERBOSITY_VERY_VERBOSE, + ); + } + + $expectedModsAvailablePath = sprintf($this->modsAvailablePath, $targetPlatform->phpBinaryPath->majorMinorVersion()); + + if (! file_exists($expectedModsAvailablePath)) { + $output->writeln( + sprintf( + 'Mods available path %s does not exist', + $expectedModsAvailablePath, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + if (! is_dir($expectedModsAvailablePath)) { + $output->writeln( + sprintf( + 'Mods available path %s is not a directory', + $expectedModsAvailablePath, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + if (! is_writable($expectedModsAvailablePath)) { + $output->writeln( + sprintf( + 'Mods available path %s is not writable', + $expectedModsAvailablePath, + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + + $expectedIniFile = sprintf( + '%s%s%s.ini', + rtrim($expectedModsAvailablePath, DIRECTORY_SEPARATOR), + DIRECTORY_SEPARATOR, + $downloadedPackage->package->extensionName->name(), + ); + + $pieCreatedTheIniFile = false; + if (! file_exists($expectedIniFile)) { + $output->writeln( + sprintf( + 'Creating new INI file based on extension priority: %s', + $expectedIniFile, + ), + OutputInterface::VERBOSITY_VERY_VERBOSE, + ); + $pieCreatedTheIniFile = true; + touch($expectedIniFile); + } + + $addingExtensionWasSuccessful = ($this->checkAndAddExtensionToIniIfNeeded)( + $expectedIniFile, + $targetPlatform, + $downloadedPackage, + $output, + static function () use ($phpenmodPath, $targetPlatform, $downloadedPackage, $output): bool { + try { + Process::run([ + $phpenmodPath, + '-v', + $targetPlatform->phpBinaryPath->majorMinorVersion(), + '-s', + 'ALL', + $downloadedPackage->package->extensionName->name(), + ]); + + return true; + } catch (ProcessFailedException $processFailedException) { + $output->writeln( + sprintf( + 'Failed to use %s to enable %s for PHP %s: %s', + $phpenmodPath, + $downloadedPackage->package->extensionName->name(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $processFailedException->getMessage(), + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + + return false; + } + }, + ); + + if (! $addingExtensionWasSuccessful && $pieCreatedTheIniFile) { + unlink($expectedIniFile); + } + + return $addingExtensionWasSuccessful; + } + + /** @return non-empty-string|null */ + private function phpenmodPath(): string|null + { + if (Platform::isWindows()) { + return null; + } + + try { + $phpenmodPath = Process::run(['which', $this->phpenmod]); + + return $phpenmodPath !== '' ? $phpenmodPath : null; + } catch (ProcessFailedException) { + return null; + } + } +} diff --git a/src/Installing/Ini/PickBestSetupIniApproach.php b/src/Installing/Ini/PickBestSetupIniApproach.php new file mode 100644 index 00000000..83c719d3 --- /dev/null +++ b/src/Installing/Ini/PickBestSetupIniApproach.php @@ -0,0 +1,79 @@ +|null */ + private array|null $memoizedApproachesThatCanBeUsed = null; + + /** @param list $possibleApproaches */ + public function __construct( + private readonly array $possibleApproaches, + ) { + } + + /** @return list */ + private function approachesThatCanBeUsed(TargetPlatform $targetPlatform): array + { + if ($this->memoizedApproachesThatCanBeUsed === null) { + $this->memoizedApproachesThatCanBeUsed = array_values(array_filter( + $this->possibleApproaches, + static fn (SetupIniApproach $approach) => $approach->canBeUsed($targetPlatform), + )); + } + + return $this->memoizedApproachesThatCanBeUsed; + } + + public function canBeUsed(TargetPlatform $targetPlatform): bool + { + return count($this->approachesThatCanBeUsed($targetPlatform)) > 0; + } + + public function setup( + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + BinaryFile $binaryFile, + OutputInterface $output, + ): bool { + $approaches = $this->approachesThatCanBeUsed($targetPlatform); + + if (count($approaches) === 0) { + $output->writeln('No INI setup approaches can be used on this platform.', OutputInterface::VERBOSITY_VERBOSE); + + return false; + } + + foreach ($approaches as $approach) { + $output->writeln( + sprintf( + 'Trying to enable extension using %s', + (new ReflectionClass($approach))->getShortName(), + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + if ($approach->setup($targetPlatform, $downloadedPackage, $binaryFile, $output)) { + return true; + } + } + + $output->writeln('None of the INI setup approaches succeeded.', OutputInterface::VERBOSITY_VERBOSE); + + return false; + } +} diff --git a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php new file mode 100644 index 00000000..89f36819 --- /dev/null +++ b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php @@ -0,0 +1,38 @@ +phpBinaryPath->assertExtensionIsLoadedInRuntime( + $downloadedPackage->package->extensionName, + $output, + ); + + return true; + } catch (ExtensionIsNotLoaded) { + return false; + } + } +} diff --git a/src/Installing/Ini/SetupIniApproach.php b/src/Installing/Ini/SetupIniApproach.php new file mode 100644 index 00000000..37870e90 --- /dev/null +++ b/src/Installing/Ini/SetupIniApproach.php @@ -0,0 +1,31 @@ +phpBinaryPath->additionalIniDirectory() !== null; + } + + public function setup( + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + BinaryFile $binaryFile, + OutputInterface $output, + ): bool { + $additionalIniFilesPath = $targetPlatform->phpBinaryPath->additionalIniDirectory(); + + /** In practice, this shouldn't happen since {@see canBeUsed()} checks this */ + if ($additionalIniFilesPath === null) { + return false; + } + + $expectedIniFile = sprintf( + '%s%s%d-%s.ini', + rtrim($additionalIniFilesPath, DIRECTORY_SEPARATOR), + DIRECTORY_SEPARATOR, + $downloadedPackage->package->priority, + $downloadedPackage->package->extensionName->name(), + ); + + $pieCreatedTheIniFile = false; + if (! file_exists($expectedIniFile)) { + $output->writeln( + sprintf( + 'Creating new INI file based on extension priority: %s', + $expectedIniFile, + ), + OutputInterface::VERBOSITY_VERY_VERBOSE, + ); + $pieCreatedTheIniFile = true; + touch($expectedIniFile); + } + + $addingExtensionWasSuccessful = ($this->checkAndAddExtensionToIniIfNeeded)( + $expectedIniFile, + $targetPlatform, + $downloadedPackage, + $output, + null, + ); + + if (! $addingExtensionWasSuccessful && $pieCreatedTheIniFile) { + unlink($expectedIniFile); + } + + return $addingExtensionWasSuccessful; + } +} diff --git a/src/Installing/Ini/StandardSinglePhpIni.php b/src/Installing/Ini/StandardSinglePhpIni.php new file mode 100644 index 00000000..0eb046e8 --- /dev/null +++ b/src/Installing/Ini/StandardSinglePhpIni.php @@ -0,0 +1,46 @@ +phpBinaryPath->loadedIniConfigurationFile() !== null; + } + + public function setup( + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + BinaryFile $binaryFile, + OutputInterface $output, + ): bool { + $ini = $targetPlatform->phpBinaryPath->loadedIniConfigurationFile(); + + /** In practice, this shouldn't happen since {@see canBeUsed()} checks this */ + if ($ini === null) { + return false; + } + + return ($this->checkAndAddExtensionToIniIfNeeded)( + $ini, + $targetPlatform, + $downloadedPackage, + $output, + null, + ); + } +} diff --git a/src/Installing/Install.php b/src/Installing/Install.php index 71e2849d..17982b41 100644 --- a/src/Installing/Install.php +++ b/src/Installing/Install.php @@ -20,5 +20,6 @@ public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, OutputInterface $output, + bool $attemptToSetupIniFile, ): BinaryFile; } diff --git a/src/Installing/SetupIniFile.php b/src/Installing/SetupIniFile.php new file mode 100644 index 00000000..d6ca58bb --- /dev/null +++ b/src/Installing/SetupIniFile.php @@ -0,0 +1,53 @@ +setupIniApproach->canBeUsed($targetPlatform) + && $this->setupIniApproach->setup($targetPlatform, $downloadedPackage, $binaryFile, $output) + ) { + $output->writeln(sprintf( + '✅ Extension is enabled and loaded in %s', + $targetPlatform->phpBinaryPath->phpBinaryPath, + )); + } else { + if (! $attemptToSetupIniFile) { + $output->writeln('Automatic extension enabling was skipped.', OutputInterface::VERBOSITY_VERBOSE); + } + + $output->writeln('⚠️ Extension has NOT been automatically enabled.'); + $output->writeln(sprintf( + 'You must now add "%s=%s" to your php.ini', + $downloadedPackage->package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $downloadedPackage->package->extensionName->name(), + )); + } + } +} diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index 6411e6b5..da831eed 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -6,7 +6,6 @@ use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; -use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use RuntimeException; @@ -22,8 +21,16 @@ final class UnixInstall implements Install { private const MAKE_INSTALL_TIMEOUT_SECS = 60; // 1 minute - public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, OutputInterface $output): BinaryFile + public function __construct(private readonly SetupIniFile $setupIniFile) { + } + + public function __invoke( + DownloadedPackage $downloadedPackage, + TargetPlatform $targetPlatform, + OutputInterface $output, + bool $attemptToSetupIniFile, + ): BinaryFile { $targetExtensionPath = $targetPlatform->phpBinaryPath->extensionPath(); $sharedObjectName = $downloadedPackage->package->extensionName->name() . '.so'; @@ -63,17 +70,16 @@ public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $t $output->writeln('Install complete: ' . $expectedSharedObjectLocation); - /** - * @link https://github.com/php/pie/issues/20 - * - * @todo this should be improved in future to try to automatically set up the ext - */ - $output->writeln(sprintf( - 'You must now add "%s=%s" to your php.ini', - $downloadedPackage->package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension', - $downloadedPackage->package->extensionName->name(), - )); - - return BinaryFile::fromFileWithSha256Checksum($expectedSharedObjectLocation); + $binaryFile = BinaryFile::fromFileWithSha256Checksum($expectedSharedObjectLocation); + + ($this->setupIniFile)( + $targetPlatform, + $downloadedPackage, + $binaryFile, + $output, + $attemptToSetupIniFile, + ); + + return $binaryFile; } } diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index a9f6c3c1..7ff5ef1d 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -31,8 +31,16 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class WindowsInstall implements Install { - public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, OutputInterface $output): BinaryFile + public function __construct(private readonly SetupIniFile $setupIniFile) { + } + + public function __invoke( + DownloadedPackage $downloadedPackage, + TargetPlatform $targetPlatform, + OutputInterface $output, + bool $attemptToSetupIniFile, + ): BinaryFile { $extractedSourcePath = $downloadedPackage->extractedSourcePath; $sourceDllName = WindowsExtensionAssetName::determineDllName($targetPlatform, $downloadedPackage); $sourcePdbName = str_replace('.dll', '.pdb', $sourceDllName); @@ -82,7 +90,17 @@ public function __invoke(DownloadedPackage $downloadedPackage, TargetPlatform $t $downloadedPackage->package->extensionName->name(), )); - return BinaryFile::fromFileWithSha256Checksum($destinationDllName); + $binaryFile = BinaryFile::fromFileWithSha256Checksum($destinationDllName); + + ($this->setupIniFile)( + $targetPlatform, + $downloadedPackage, + $binaryFile, + $output, + $attemptToSetupIniFile, + ); + + return $binaryFile; } /** diff --git a/src/Platform/TargetPhp/Exception/ExtensionIsNotLoaded.php b/src/Platform/TargetPhp/Exception/ExtensionIsNotLoaded.php new file mode 100644 index 00000000..7955186f --- /dev/null +++ b/src/Platform/TargetPhp/Exception/ExtensionIsNotLoaded.php @@ -0,0 +1,23 @@ +name(), + $php->phpBinaryPath, + )); + } +} diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 7589b2fa..f421b65a 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -6,22 +6,26 @@ use Composer\Semver\VersionParser; use Composer\Util\Platform; +use Php\Pie\ExtensionName; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Util\Process; use RuntimeException; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\PhpExecutableFinder; use Webmozart\Assert\Assert; use function array_combine; use function array_key_exists; +use function array_keys; use function array_map; use function assert; use function dirname; use function explode; use function file_exists; use function implode; +use function in_array; use function is_dir; use function is_executable; use function preg_match; @@ -112,6 +116,58 @@ public function extensionPath(): string throw new RuntimeException('Could not determine extension path for ' . $this->phpBinaryPath); } + public function assertExtensionIsLoadedInRuntime(ExtensionName $extension, OutputInterface|null $output = null): void + { + if (! in_array($extension->name(), array_keys($this->extensions()))) { + throw Exception\ExtensionIsNotLoaded::fromExpectedExtension( + $this, + $extension, + ); + } + + if ($output === null) { + return; + } + + $output->writeln( + sprintf( + 'Successfully asserted that extension %s is loaded in runtime.', + $extension->name(), + ), + OutputInterface::VERBOSITY_VERBOSE, + ); + } + + /** @return non-empty-string|null */ + public function additionalIniDirectory(): string|null + { + if ( + preg_match('/Scan this dir for additional \.ini files([ =>\t]*)(.*)/', $this->phpinfo(), $m) + && array_key_exists(2, $m) + && $m[2] !== '' + && $m[2] !== '(none)' + ) { + return $m[2]; + } + + return null; + } + + /** @return non-empty-string|null */ + public function loadedIniConfigurationFile(): string|null + { + if ( + preg_match('/Loaded Configuration File([ =>\t]*)(.*)/', $this->phpinfo(), $m) + && array_key_exists(2, $m) + && $m[2] !== '' + && $m[2] !== '(none)' + ) { + return $m[2]; + } + + return null; + } + /** * Returns a map where the key is the name of the extension and the value is the version ('0' if not defined) * diff --git a/test/assets/docker-php-ext-enable/bad b/test/assets/docker-php-ext-enable/bad new file mode 100755 index 00000000..33514cfc --- /dev/null +++ b/test/assets/docker-php-ext-enable/bad @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "something bad happened" +exit 1 diff --git a/test/assets/docker-php-ext-enable/good b/test/assets/docker-php-ext-enable/good new file mode 100755 index 00000000..5e2e73e3 --- /dev/null +++ b/test/assets/docker-php-ext-enable/good @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "hi" +exit 0 diff --git a/test/assets/example_ini_files/with_commented_extension.ini b/test/assets/example_ini_files/with_commented_extension.ini new file mode 100644 index 00000000..2beb79d2 --- /dev/null +++ b/test/assets/example_ini_files/with_commented_extension.ini @@ -0,0 +1 @@ +;extension=foobar diff --git a/test/assets/example_ini_files/with_extension.ini b/test/assets/example_ini_files/with_extension.ini new file mode 100644 index 00000000..40e7397e --- /dev/null +++ b/test/assets/example_ini_files/with_extension.ini @@ -0,0 +1 @@ +extension=foobar diff --git a/test/assets/example_ini_files/with_zend_extension.ini b/test/assets/example_ini_files/with_zend_extension.ini new file mode 100644 index 00000000..d2b87581 --- /dev/null +++ b/test/assets/example_ini_files/with_zend_extension.ini @@ -0,0 +1 @@ +zend_extension=foobar diff --git a/test/assets/example_ini_files/without_extension.ini b/test/assets/example_ini_files/without_extension.ini new file mode 100644 index 00000000..131f894d --- /dev/null +++ b/test/assets/example_ini_files/without_extension.ini @@ -0,0 +1 @@ +html_errors=1 diff --git a/test/assets/phpenmod/bad b/test/assets/phpenmod/bad new file mode 100755 index 00000000..33514cfc --- /dev/null +++ b/test/assets/phpenmod/bad @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "something bad happened" +exit 1 diff --git a/test/assets/phpenmod/good b/test/assets/phpenmod/good new file mode 100755 index 00000000..5e2e73e3 --- /dev/null +++ b/test/assets/phpenmod/good @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "hi" +exit 0 diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 16076023..801dbe9b 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -17,6 +17,7 @@ /** @psalm-api */ class CliContext implements Context { + private const PHP_BINARY = 'php'; private string|null $output = null; private int|null $exitCode = null; /** @var list */ @@ -37,7 +38,7 @@ public function iRunACommandToDownloadSpecificVersionOfAnExtension(string $versi /** @param list $command */ public function runPieCommand(array $command): void { - $pieCommand = array_merge(['php', ...$this->phpArguments, 'bin/pie'], $command); + $pieCommand = array_merge([self::PHP_BINARY, ...$this->phpArguments, 'bin/pie'], $command); $proc = (new Process($pieCommand))->mustRun(); @@ -123,6 +124,8 @@ public function theExtensionShouldHaveBeenInstalled(): 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_example_pie_extension.dll#'); @@ -130,6 +133,12 @@ public function theExtensionShouldHaveBeenInstalled(): void } Assert::regex($this->output, '#Install complete: [-_a-zA-Z0-9/]+/example_pie_extension.so#'); + + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";'])) + ->mustRun() + ->getOutput(); + + Assert::same('yes', $isExtEnabled); } #[Given('I have an invalid extension installed')] diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index e8e8c61c..976ca7a0 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -50,6 +50,7 @@ public function testUnixBuildCanBuildExtension(): void null, null, null, + 99, ), self::TEST_EXTENSION_PATH, ); @@ -105,6 +106,7 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( null, null, null, + 99, ), self::TEST_EXTENSION_PATH, ); @@ -148,6 +150,7 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void 'pie_test_ext', null, null, + 99, ), dirname(self::TEST_EXTENSION_PATH), ); @@ -207,6 +210,7 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void null, null, null, + 99, ), self::TEST_EXTENSION_PATH, ); @@ -251,6 +255,7 @@ public function testVerboseOutputShowsCleanupMessages(): void null, null, null, + 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php index c6ed049a..3257ae95 100644 --- a/test/integration/Command/InstallCommandTest.php +++ b/test/integration/Command/InstallCommandTest.php @@ -79,7 +79,11 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin } $this->commandTester->execute( - ['requested-package-and-version' => self::TEST_PACKAGE, '--with-php-config' => $phpConfigPath], + [ + 'requested-package-and-version' => self::TEST_PACKAGE, + '--with-php-config' => $phpConfigPath, + '--skip-enable-extension' => true, + ], ['verbosity' => BufferedOutput::VERBOSITY_VERY_VERBOSE], ); @@ -112,7 +116,10 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin #[RequiresOperatingSystemFamily('Windows')] public function testInstallCommandWillInstallCompatibleExtensionWindows(): void { - $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE]); + $this->commandTester->execute([ + 'requested-package-and-version' => self::TEST_PACKAGE, + '--skip-enable-extension' => true, + ]); $this->commandTester->assertCommandIsSuccessful(); diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php index c708dff1..1477b39c 100644 --- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -90,6 +90,7 @@ public function testDependenciesAreResolvedToExpectedVersions( PieOperation::Resolve, [], null, + false, ), ), $targetPlatform, diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index df71559c..53cd9b9f 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -56,6 +56,7 @@ public function testDeterminingReleaseAssetUrlForWindows(): void null, null, null, + 99, ); $io = $this->createMock(IOInterface::class); diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index 049a7af4..ca28ee0d 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -12,6 +12,8 @@ use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\Installing\Ini\PickBestSetupIniApproach; +use Php\Pie\Installing\SetupIniFile; use Php\Pie\Installing\UnixInstall; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPlatform; @@ -93,6 +95,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void null, null, null, + 99, ), self::TEST_EXTENSION_PATH, ); @@ -105,10 +108,11 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void null, ); - $installedSharedObject = (new UnixInstall())->__invoke( + $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke( $downloadedPackage, $targetPlatform, $output, + true, ); $outputString = $output->fetch(); diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php index b8664728..4d043415 100644 --- a/test/integration/Installing/WindowsInstallTest.php +++ b/test/integration/Installing/WindowsInstallTest.php @@ -9,6 +9,8 @@ use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\Installing\Ini\PickBestSetupIniApproach; +use Php\Pie\Installing\SetupIniFile; use Php\Pie\Installing\WindowsInstall; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -57,6 +59,7 @@ public function testWindowsInstallCanInstallExtension(): void null, null, null, + 99, ), self::TEST_EXTENSION_PATH, ); @@ -73,9 +76,9 @@ public function testWindowsInstallCanInstallExtension(): void $phpPath = dirname($targetPlatform->phpBinaryPath->phpBinaryPath); $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); - $installer = new WindowsInstall(); + $installer = new WindowsInstall(new SetupIniFile(new PickBestSetupIniApproach([]))); - $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output); + $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output, true); self::assertSame($extensionPath . '\php_pie_test_ext.dll', $installedDll->filePath); $outputString = $output->fetch(); diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 04da3504..34183746 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -112,6 +112,7 @@ public function testProcessingConfigureOptionsFromInput(): void null, null, null, + 99, ); $inputDefinition = new InputDefinition(); $inputDefinition->addOption(new InputOption('with-stuff', null, InputOption::VALUE_REQUIRED)); diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php index 1b186ad5..a0137f5b 100644 --- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php +++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php @@ -68,6 +68,7 @@ public function testDownloadWithoutBuildAndInstall(): void PieOperation::Download, ['--foo', '--bar="yes"'], null, + false, ); $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $installPath = '/path/to/install'; @@ -109,6 +110,7 @@ public function testDownloadAndBuildWithoutInstall(): void PieOperation::Build, ['--foo', '--bar="yes"'], null, + false, ); $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $installPath = '/path/to/install'; @@ -153,6 +155,7 @@ public function testDownloadBuildAndInstall(): void PieOperation::Install, ['--foo', '--bar="yes"'], null, + false, ); $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $installPath = '/path/to/install'; diff --git a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php index 7151bb35..48790e74 100644 --- a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php +++ b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php @@ -64,6 +64,7 @@ public function testMetadataForDownloads(): void PieOperation::Build, ['--foo', '--bar="yes"'], null, + false, ), clone $package, ); @@ -102,6 +103,7 @@ public function testMetadataForBuilds(): void PieOperation::Build, ['--foo', '--bar="yes"'], new PhpizePath('/path/to/phpize'), + false, ), clone $package, new BinaryFile('/path/to/built', 'sha256-checksum-value'), diff --git a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php b/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php index 4022d462..8e9d2697 100644 --- a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php @@ -80,6 +80,7 @@ public function testEventListenerRegistration(): void PieOperation::Install, [], null, + false, ), ); } @@ -125,6 +126,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void PieOperation::Install, [], null, + false, ), ))($installerEvent); @@ -182,6 +184,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void PieOperation::Install, [], null, + false, ), ))($installerEvent); diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index 79736362..fc99924a 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -140,6 +140,7 @@ public function testGithubOrgAndRepo(string $composerPackageName, string|null $d null, null, null, + 99, ); self::assertSame($expectedGithubOrgAndRepo, $package->githubOrgAndRepository()); diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php index 8cdc941c..c4edf0b2 100644 --- a/test/unit/Downloading/DownloadedPackageTest.php +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -35,6 +35,7 @@ public function testFromPackageAndExtractedPath(): void null, null, null, + 99, ); $extractedSourcePath = uniqid('/path/to/downloaded/package', true); @@ -60,6 +61,7 @@ public function testFromPackageAndExtractedPathWithBuildPath(): void 'Downloading', null, null, + 99, ); $extractedSourcePath = realpath(__DIR__ . '/../'); diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index 3eb2e69a..e25bcecb 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -36,6 +36,7 @@ public function testForPackage(): void null, null, null, + 99, ); $exception = CouldNotFindReleaseAsset::forPackage($package, ['something.zip', 'something2.zip']); @@ -58,6 +59,7 @@ public function testForPackageWithMissingTag(): void null, null, null, + 99, ); $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index b618b927..34d8e25a 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -84,6 +84,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void null, null, null, + 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); @@ -146,6 +147,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr null, null, null, + 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); @@ -189,6 +191,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF null, null, null, + 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); diff --git a/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php b/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php new file mode 100644 index 00000000..3f6f3f2e --- /dev/null +++ b/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php @@ -0,0 +1,293 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + } + + public function testReturnsFalseWhenFileIsNotWritable(): void + { + if (TargetPlatform::isRunningAsRoot()) { + self::markTestSkipped('Test cannot be run as root, as root can always write files'); + } + + $unwritableFilename = tempnam(sys_get_temp_dir(), 'PIE_unwritable_ini_file'); + touch($unwritableFilename); + chmod($unwritableFilename, 000); + + try { + self::assertFalse((new AddExtensionToTheIniFile())( + $unwritableFilename, + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.0.0', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + $this->mockPhpBinary, + $this->output, + null, + )); + + self::assertStringContainsString( + sprintf('PHP is configured to use %s, but it is not writable by PIE.', $unwritableFilename), + $this->output->fetch(), + ); + } finally { + chmod($unwritableFilename, 644); + unlink($unwritableFilename); + } + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testReturnsFalseWhenExistingIniCouldNotBeRead(): void + { + if (TargetPlatform::isRunningAsRoot()) { + self::markTestSkipped('Test cannot be run as root, as root can always read files'); + } + + $unreadableIniFile = tempnam(sys_get_temp_dir(), 'PIE_unreadable_ini_file'); + touch($unreadableIniFile); + chmod($unreadableIniFile, 222); + + try { + self::assertFalse((new AddExtensionToTheIniFile())( + $unreadableIniFile, + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.0.0', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + $this->mockPhpBinary, + $this->output, + null, + )); + + self::assertStringContainsString( + sprintf('Tried making a backup of %s but could not read it, aborting enablement of extension', $unreadableIniFile), + $this->output->fetch(), + ); + } finally { + chmod($unreadableIniFile, 644); + unlink($unreadableIniFile); + } + } + + public function testReturnsFalseWhenExtensionWasAddedButPhpRuntimeDidNotLoadExtension(): void + { + $extensionName = ExtensionName::normaliseFromString('foobar'); + $originalIniContent = "; some comment\nerror_reporting=E_ALL\n"; + + $iniFile = tempnam(sys_get_temp_dir(), 'PIE_ini_file'); + file_put_contents($iniFile, $originalIniContent); + + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->willThrowException( + ExtensionIsNotLoaded::fromExpectedExtension($this->mockPhpBinary, $extensionName), + ); + + try { + self::assertFalse((new AddExtensionToTheIniFile())( + $iniFile, + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + $extensionName, + 'foo/bar', + '1.0.0', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + $this->mockPhpBinary, + $this->output, + null, + )); + + self::assertStringContainsString( + 'Something went wrong enabling the foobar extension: Expected extension foobar to be loaded in PHP /path/to/php, but it was not detected.', + $this->output->fetch(), + ); + + // Ensure the original INI file content was restored + self::assertSame($originalIniContent, file_get_contents($iniFile)); + } finally { + unlink($iniFile); + } + } + + public function testReturnsTrueWhenExtensionAdded(): void + { + $iniFile = tempnam(sys_get_temp_dir(), 'PIE_ini_file'); + touch($iniFile); + + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime'); + + try { + self::assertTrue((new AddExtensionToTheIniFile())( + $iniFile, + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.0.0', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + $this->mockPhpBinary, + $this->output, + null, + )); + + $iniContent = file_get_contents($iniFile); + self::assertSame( + PHP_EOL . '; PIE automatically added this to enable the foo/bar extension' . PHP_EOL + . '; priority=99' . PHP_EOL + . 'extension=foobar' . PHP_EOL, + $iniContent, + ); + + self::assertStringContainsString( + sprintf('Enabled extension foobar in the INI file %s', $iniFile), + $this->output->fetch(), + ); + } finally { + unlink($iniFile); + } + } + + public function testReturnsTrueWhenExtensionAddedWithAdditionalStep(): void + { + $iniFile = tempnam(sys_get_temp_dir(), 'PIE_ini_file'); + touch($iniFile); + + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime'); + + try { + $additionalStepInvoked = false; + self::assertTrue((new AddExtensionToTheIniFile())( + $iniFile, + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.0.0', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + $this->mockPhpBinary, + $this->output, + static function () use (&$additionalStepInvoked): bool { + $additionalStepInvoked = true; + + return true; + }, + )); + + self::assertTrue($additionalStepInvoked, 'Failed asserting that the additional step was invoked'); + + $iniContent = file_get_contents($iniFile); + self::assertSame( + PHP_EOL . '; PIE automatically added this to enable the foo/bar extension' . PHP_EOL + . '; priority=99' . PHP_EOL + . 'extension=foobar' . PHP_EOL, + $iniContent, + ); + + self::assertStringContainsString( + sprintf('Enabled extension foobar in the INI file %s', $iniFile), + $this->output->fetch(), + ); + } finally { + unlink($iniFile); + } + } +} diff --git a/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php new file mode 100644 index 00000000..ac31ecc2 --- /dev/null +++ b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php @@ -0,0 +1,294 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + + $this->isExtensionAlreadyInTheIniFile = $this->createMock(IsExtensionAlreadyInTheIniFile::class); + $this->addExtensionToTheIniFile = $this->createMock(AddExtensionToTheIniFile::class); + + $this->targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $this->mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + [], + true, + true, + null, + null, + null, + 66, + ), + '/path/to/extracted/source', + ); + + $this->checkAndAddExtensionToIniIfNeeded = new CheckAndAddExtensionToIniIfNeeded( + $this->isExtensionAlreadyInTheIniFile, + $this->addExtensionToTheIniFile, + ); + } + + public function testReturnsFalseWhenIniFileDoesNotExist(): void + { + $this->isExtensionAlreadyInTheIniFile + ->expects(self::never()) + ->method('__invoke'); + + $this->mockPhpBinary + ->expects(self::never()) + ->method('assertExtensionIsLoadedInRuntime'); + + $this->addExtensionToTheIniFile + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse($this->checkAndAddExtensionToIniIfNeeded->__invoke( + '/path/to/non/existent/php.ini', + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + null, + )); + + self::assertStringContainsString( + 'PHP is configured to use /path/to/non/existent/php.ini, but it did not exist, or is not readable by PIE.', + $this->output->fetch(), + ); + } + + public function testExtensionIsAlreadyEnabledButExtensionDoesNotLoad(): void + { + $this->isExtensionAlreadyInTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->willReturn(true); + + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output) + ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( + $this->mockPhpBinary, + $this->downloadedPackage->package->extensionName, + )); + + $this->addExtensionToTheIniFile + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse($this->checkAndAddExtensionToIniIfNeeded->__invoke( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + null, + )); + + $output = $this->output->fetch(); + self::assertStringContainsString( + 'Extension is already enabled in the INI file', + $output, + ); + self::assertStringContainsString( + 'Something went wrong verifying the foobar extension is enabled: Expected extension foobar to be loaded in PHP /path/to/php, but it was not detected.', + $output, + ); + } + + public function testExtensionIsAlreadyEnabledAndExtensionLoaded(): void + { + $this->isExtensionAlreadyInTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->willReturn(true); + + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output); + + $this->addExtensionToTheIniFile + ->expects(self::never()) + ->method('__invoke'); + + self::assertTrue($this->checkAndAddExtensionToIniIfNeeded->__invoke( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + null, + )); + + $output = $this->output->fetch(); + self::assertStringContainsString( + 'Extension is already enabled in the INI file', + $output, + ); + } + + public function testExtensionIsAlreadyEnabledWithAdditionalStepAndExtensionLoaded(): void + { + $this->isExtensionAlreadyInTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->willReturn(true); + + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output); + + $this->addExtensionToTheIniFile + ->expects(self::never()) + ->method('__invoke'); + + $additionalStepInvoked = false; + self::assertTrue($this->checkAndAddExtensionToIniIfNeeded->__invoke( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + static function () use (&$additionalStepInvoked): bool { + $additionalStepInvoked = true; + + return true; + }, + )); + + self::assertTrue($additionalStepInvoked, 'Failed asserting that the additional step was invoked'); + + $output = $this->output->fetch(); + self::assertStringContainsString( + 'Extension is already enabled in the INI file', + $output, + ); + } + + public function testExtensionIsNotYetAdded(): void + { + $this->isExtensionAlreadyInTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->willReturn(false); + + $this->mockPhpBinary + ->expects(self::never()) + ->method('assertExtensionIsLoadedInRuntime'); + + $this->addExtensionToTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with( + self::INI_FILE, + $this->downloadedPackage->package, + $this->targetPlatform->phpBinaryPath, + $this->output, + ) + ->willReturn(true); + + self::assertTrue($this->checkAndAddExtensionToIniIfNeeded->__invoke( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + null, + )); + } + + public function testExtensionIsNotYetAddedButFailsToBeAdded(): void + { + $this->isExtensionAlreadyInTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->willReturn(false); + + $this->mockPhpBinary + ->expects(self::never()) + ->method('assertExtensionIsLoadedInRuntime'); + + $this->addExtensionToTheIniFile + ->expects(self::once()) + ->method('__invoke') + ->with( + self::INI_FILE, + $this->downloadedPackage->package, + $this->targetPlatform->phpBinaryPath, + $this->output, + ) + ->willReturn(false); + + self::assertFalse($this->checkAndAddExtensionToIniIfNeeded->__invoke( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + null, + )); + } +} diff --git a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php new file mode 100644 index 00000000..410190a0 --- /dev/null +++ b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php @@ -0,0 +1,176 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + + $this->targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $this->mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ); + + $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum'); + } + + public function testCannotBeUsedWhenDockerPhpExtEnableIsNotInPath(): void + { + self::assertFalse( + (new DockerPhpExtEnable(self::NON_EXISTENT_DOCKER_PHP_EXT_ENABLE)) + ->canBeUsed($this->targetPlatform), + ); + } + + public function testCanBeUsedWhenDockerPhpExtEnableIsInPath(): void + { + self::assertTrue( + (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE)) + ->canBeUsed($this->targetPlatform), + ); + } + + public function testSetupReturnsFalseWhenWhenDockerPhpExtEnableIsNotInPath(): void + { + $this->mockPhpBinary + ->expects(self::never()) + ->method('assertExtensionIsLoadedInRuntime'); + + self::assertFalse( + (new DockerPhpExtEnable(self::NON_EXISTENT_DOCKER_PHP_EXT_ENABLE)) + ->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + } + + public function testReturnsTrueWhenDockerPhpExtEnableSuccessfullyEnablesExtension(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output); + + self::assertTrue( + (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE)) + ->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + } + + public function testReturnsFalseWhenDockerPhpExtEnableFailsToBeRun(): void + { + $this->mockPhpBinary + ->expects(self::never()) + ->method('assertExtensionIsLoadedInRuntime'); + + self::assertFalse( + (new DockerPhpExtEnable(self::BAD_DOCKER_PHP_EXT_ENABLE)) + ->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + } + + public function testReturnsFalseWhenDockerPhpExtEnableFailsToAssertExtensionWasEnabled(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output) + ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( + $this->mockPhpBinary, + $this->downloadedPackage->package->extensionName, + )); + + self::assertFalse( + (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE)) + ->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + } +} diff --git a/test/unit/Installing/Ini/IsExtensionAlreadyInTheIniFileTest.php b/test/unit/Installing/Ini/IsExtensionAlreadyInTheIniFileTest.php new file mode 100644 index 00000000..18f1a293 --- /dev/null +++ b/test/unit/Installing/Ini/IsExtensionAlreadyInTheIniFileTest.php @@ -0,0 +1,49 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + + $this->checkAndAddExtensionToIniIfNeeded = $this->createMock(CheckAndAddExtensionToIniIfNeeded::class); + + $this->targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $this->mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ); + + $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum'); + } + + #[RequiresOperatingSystemFamily('Windows')] + public function testCanBeUsedReturnsFalseOnWindows(): void + { + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->canBeUsed($this->targetPlatform), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testCanBeUsedReturnsFalseWhenPhpenmodNotInPath(): void + { + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::NON_EXISTENT_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->canBeUsed($this->targetPlatform), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testCanBeUsedReturnsTrueWhenPhpenmodInPath(): void + { + self::assertTrue( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->canBeUsed($this->targetPlatform), + ); + } + + #[RequiresOperatingSystemFamily('Windows')] + public function testSetupReturnsFalseOnWindows(): void + { + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseWhenPhpenmodNotInPath(): void + { + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::NON_EXISTENT_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseWhenAdditionalPhpIniPathNotSet(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn(null); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertStringContainsString( + 'Additional INI file path was not set - may not be Ondrej PHP repo', + $this->output->fetch(), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseWhenModsAvailablePathDoesNotExist(): void + { + $this->mockPhpBinary + ->method('additionalIniDirectory') + ->willReturn('/value/does/not/matter'); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + self::NON_EXISTENT_MODS_AVAILABLE_PATH, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertStringContainsString( + 'Mods available path ' . self::NON_EXISTENT_MODS_AVAILABLE_PATH . ' does not exist', + $this->output->fetch(), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseWhenModsAvailablePathNotADirectory(): void + { + $this->mockPhpBinary + ->method('additionalIniDirectory') + ->willReturn('/value/does/not/matter'); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + __FILE__, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertStringContainsString( + 'Mods available path ' . __FILE__ . ' is not a directory', + $this->output->fetch(), + ); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseWhenModsAvailablePathNotWritable(): void + { + if (TargetPlatform::isRunningAsRoot()) { + self::markTestSkipped('Test cannot be run as root, as root can always write files'); + } + + $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path'); + unlink($modsAvailablePath); + mkdir($modsAvailablePath, 000, true); + + $this->mockPhpBinary + ->method('additionalIniDirectory') + ->willReturn('/value/does/not/matter'); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + $modsAvailablePath, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertStringContainsString( + 'Mods available path ' . $modsAvailablePath . ' is not writable', + $this->output->fetch(), + ); + + rmdir($modsAvailablePath); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseAndRemovesPieCreatedIniFileWhenPhpenmodAdditionalStepFails(): void + { + $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path'); + unlink($modsAvailablePath); + mkdir($modsAvailablePath, recursive: true); + + $expectedIniFile = $modsAvailablePath . DIRECTORY_SEPARATOR . 'foobar.ini'; + + $this->mockPhpBinary + ->method('additionalIniDirectory') + ->willReturn('/value/does/not/matter'); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + $expectedIniFile, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + self::isType(IsType::TYPE_CALLABLE), + ) + ->willReturnCallback( + /** @param callable():bool $additionalEnableStep */ + static function ( + string $iniFile, + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + OutputInterface $output, + callable $additionalEnableStep, + ): bool { + return $additionalEnableStep(); + }, + ); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::BAD_PHPENMOD, + $modsAvailablePath, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertFileDoesNotExist($expectedIniFile); + + self::assertStringContainsString( + 'something bad happened', + $this->output->fetch(), + ); + + rmdir($modsAvailablePath); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsFalseAndRemovesPieCreatedIniFileWhenCheckAndAddExtensionFails(): void + { + $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path'); + unlink($modsAvailablePath); + mkdir($modsAvailablePath, recursive: true); + + $expectedIniFile = $modsAvailablePath . DIRECTORY_SEPARATOR . 'foobar.ini'; + + $this->mockPhpBinary + ->method('additionalIniDirectory') + ->willReturn('/value/does/not/matter'); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + $expectedIniFile, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + self::isType(IsType::TYPE_CALLABLE), + ) + ->willReturn(false); + + self::assertFalse( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + $modsAvailablePath, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertFileDoesNotExist($expectedIniFile); + + rmdir($modsAvailablePath); + } + + #[RequiresOperatingSystemFamily('Linux')] + public function testSetupReturnsTrueWhenExtensionIsEnabled(): void + { + $modsAvailablePath = tempnam(sys_get_temp_dir(), 'pie_test_mods_available_path'); + unlink($modsAvailablePath); + mkdir($modsAvailablePath, recursive: true); + + $expectedIniFile = $modsAvailablePath . DIRECTORY_SEPARATOR . 'foobar.ini'; + + $this->mockPhpBinary + ->method('additionalIniDirectory') + ->willReturn('/value/does/not/matter'); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + $expectedIniFile, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + self::isType(IsType::TYPE_CALLABLE), + ) + ->willReturnCallback( + /** @param callable():bool $additionalEnableStep */ + static function ( + string $iniFile, + TargetPlatform $targetPlatform, + DownloadedPackage $downloadedPackage, + OutputInterface $output, + callable $additionalEnableStep, + ): bool { + return $additionalEnableStep(); + }, + ); + + self::assertTrue( + (new OndrejPhpenmod( + $this->checkAndAddExtensionToIniIfNeeded, + self::GOOD_PHPENMOD, + $modsAvailablePath, + ))->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + ), + ); + + self::assertFileExists($expectedIniFile); + + unlink($expectedIniFile); + rmdir($modsAvailablePath); + } +} diff --git a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php new file mode 100644 index 00000000..07d4bd68 --- /dev/null +++ b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php @@ -0,0 +1,204 @@ +canBeUsed($this->targetPlatform())); + } + + public function testCannotBeUsedWithAnyApproaches(): void + { + $one = $this->createMock(SetupIniApproach::class); + $one->expects(self::once())->method('canBeUsed')->willReturn(false); + $two = $this->createMock(SetupIniApproach::class); + $two->expects(self::once())->method('canBeUsed')->willReturn(false); + + self::assertFalse((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform())); + } + + public function testCanBeUsedWithApproachOne(): void + { + $one = $this->createMock(SetupIniApproach::class); + $one->expects(self::once())->method('canBeUsed')->willReturn(false); + $two = $this->createMock(SetupIniApproach::class); + $two->expects(self::once())->method('canBeUsed')->willReturn(true); + + self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform())); + } + + public function testCanBeUsedWithApproachTwo(): void + { + $one = $this->createMock(SetupIniApproach::class); + $one->expects(self::once())->method('canBeUsed')->willReturn(true); + $two = $this->createMock(SetupIniApproach::class); + $two->expects(self::once())->method('canBeUsed')->willReturn(false); + + self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform())); + } + + public function testCanBeUsedWithAllApproaches(): void + { + $one = $this->createMock(SetupIniApproach::class); + $one->expects(self::once())->method('canBeUsed')->willReturn(true); + $two = $this->createMock(SetupIniApproach::class); + $two->expects(self::once())->method('canBeUsed')->willReturn(true); + + self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->canBeUsed($this->targetPlatform())); + } + + public function testVerboseMessageIsEmittedSettingUpWithoutAnyApproaches(): void + { + $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + self::assertFalse((new PickBestSetupIniApproach([]))->setup( + $this->targetPlatform(), + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'test-vendor/test-package', + '1.2.3', + 'https://test-uri/', + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ), + new BinaryFile('/path/to/extracted/source/module/foo.so', 'some-checksum'), + $output, + )); + + $outputString = $output->fetch(); + self::assertStringContainsString( + 'No INI setup approaches can be used on this platform.', + $outputString, + ); + } + + public function testWorkingApproachIsUsed(): void + { + $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $one = $this->createMock(SetupIniApproach::class); + $one->method('canBeUsed')->willReturn(true); + $one->expects(self::once())->method('setup')->willReturn(false); + $two = $this->createMock(SetupIniApproach::class); + $two->method('canBeUsed')->willReturn(true); + $two->expects(self::once())->method('setup')->willReturn(true); + + self::assertTrue((new PickBestSetupIniApproach([$one, $two]))->setup( + $this->targetPlatform(), + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'test-vendor/test-package', + '1.2.3', + 'https://test-uri/', + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ), + new BinaryFile('/path/to/extracted/source/module/foo.so', 'some-checksum'), + $output, + )); + + $outputString = $output->fetch(); + self::assertStringContainsString( + 'Trying to enable extension using MockObject_SetupIniApproach', + $outputString, + ); + } + + public function testSetupFailsWhenNoApproachesWork(): void + { + $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $one = $this->createMock(SetupIniApproach::class); + $one->method('canBeUsed')->willReturn(true); + $one->expects(self::once())->method('setup')->willReturn(false); + $two = $this->createMock(SetupIniApproach::class); + $two->method('canBeUsed')->willReturn(true); + $two->expects(self::once())->method('setup')->willReturn(false); + + self::assertFalse((new PickBestSetupIniApproach([$one, $two]))->setup( + $this->targetPlatform(), + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackage::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'test-vendor/test-package', + '1.2.3', + 'https://test-uri/', + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ), + new BinaryFile('/path/to/extracted/source/module/foo.so', 'some-checksum'), + $output, + )); + + $outputString = $output->fetch(); + self::assertStringContainsString( + 'None of the INI setup approaches succeeded.', + $outputString, + ); + } +} diff --git a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php new file mode 100644 index 00000000..de1795f3 --- /dev/null +++ b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php @@ -0,0 +1,124 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + + $this->targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $this->mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ); + + $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum'); + + $this->preCheckExtensionAlreadyLoaded = new PreCheckExtensionAlreadyLoaded(); + } + + public function testCanBeUsed(): void + { + self::assertTrue($this->preCheckExtensionAlreadyLoaded->canBeUsed( + $this->targetPlatform, + )); + } + + public function testSetupReturnsTrueWhenExtAlreadyRuntimeLoaded(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output); + + self::assertTrue($this->preCheckExtensionAlreadyLoaded->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + } + + public function testSetupReturnsFalseWhenExtIsNotRuntimeLoaded(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('assertExtensionIsLoadedInRuntime') + ->with($this->downloadedPackage->package->extensionName, $this->output) + ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( + $this->mockPhpBinary, + $this->downloadedPackage->package->extensionName, + )); + + self::assertFalse($this->preCheckExtensionAlreadyLoaded->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + } +} diff --git a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php new file mode 100644 index 00000000..404a1d04 --- /dev/null +++ b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php @@ -0,0 +1,244 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + + $this->checkAndAddExtensionToIniIfNeeded = $this->createMock(CheckAndAddExtensionToIniIfNeeded::class); + + $this->targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $this->mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ); + + $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum'); + + $this->standardAdditionalPhpIniDirectory = new StandardAdditionalPhpIniDirectory( + $this->checkAndAddExtensionToIniIfNeeded, + ); + } + + public function testCannotBeUsedWithNoDefinedAdditionalPhpIniDirectory(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn(null); + + self::assertFalse($this->standardAdditionalPhpIniDirectory->canBeUsed($this->targetPlatform)); + } + + public function testCanBeUsedWithDefinedAdditionalPhpIniDirectory(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn('/path/to/the/php.d'); + + self::assertTrue($this->standardAdditionalPhpIniDirectory->canBeUsed($this->targetPlatform)); + } + + public function testSetupReturnsWhenAdditionalPhpIniDirectoryIsNotSet(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn(null); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse($this->standardAdditionalPhpIniDirectory->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + } + + public function testReturnsTrueWhenCheckAndAddExtensionIsInvoked(): void + { + $additionalPhpIniDirectory = tempnam(sys_get_temp_dir(), 'pie_additional_php_ini_path'); + unlink($additionalPhpIniDirectory); + mkdir($additionalPhpIniDirectory, recursive: true); + + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn($additionalPhpIniDirectory); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + $expectedIniFile, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + ) + ->willReturn(true); + + self::assertTrue($this->standardAdditionalPhpIniDirectory->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + self::assertFileExists($expectedIniFile); + + unlink($expectedIniFile); + rmdir($additionalPhpIniDirectory); + } + + public function testReturnsFalseAndRemovesPieCreatedIniFileWhenCheckAndAddExtensionIsInvoked(): void + { + $additionalPhpIniDirectory = tempnam(sys_get_temp_dir(), 'pie_additional_php_ini_path'); + unlink($additionalPhpIniDirectory); + mkdir($additionalPhpIniDirectory, recursive: true); + + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn($additionalPhpIniDirectory); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + $expectedIniFile, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + ) + ->willReturn(false); + + self::assertFalse($this->standardAdditionalPhpIniDirectory->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + self::assertFileDoesNotExist($expectedIniFile); + + rmdir($additionalPhpIniDirectory); + } + + public function testReturnsFalseAndLeavesNonPieCreatedIniFileWhenCheckAndAddExtensionIsInvoked(): void + { + $additionalPhpIniDirectory = tempnam(sys_get_temp_dir(), 'pie_additional_php_ini_path'); + unlink($additionalPhpIniDirectory); + mkdir($additionalPhpIniDirectory, recursive: true); + + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + touch($expectedIniFile); + + $this->mockPhpBinary + ->expects(self::once()) + ->method('additionalIniDirectory') + ->willReturn($additionalPhpIniDirectory); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + $expectedIniFile, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + ) + ->willReturn(false); + + self::assertFalse($this->standardAdditionalPhpIniDirectory->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + self::assertFileExists($expectedIniFile); + + unlink($expectedIniFile); + rmdir($additionalPhpIniDirectory); + } +} diff --git a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php new file mode 100644 index 00000000..34b19daa --- /dev/null +++ b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php @@ -0,0 +1,181 @@ +output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + + $this->mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** + * @psalm-suppress PossiblyNullFunctionCall + * @psalm-suppress UndefinedThisPropertyAssignment + */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($this->mockPhpBinary, PhpBinaryPath::class)(); + + $this->checkAndAddExtensionToIniIfNeeded = $this->createMock(CheckAndAddExtensionToIniIfNeeded::class); + + $this->targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $this->mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + [], + true, + true, + null, + null, + null, + 99, + ), + '/path/to/extracted/source', + ); + + $this->binaryFile = new BinaryFile('/path/to/compiled/extension.so', 'fake checksum'); + + $this->standardSinglePhpIni = new StandardSinglePhpIni( + $this->checkAndAddExtensionToIniIfNeeded, + ); + } + + public function testCannotBeUsedWithNoDefinedPhpIni(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('loadedIniConfigurationFile') + ->willReturn(null); + + self::assertFalse($this->standardSinglePhpIni->canBeUsed($this->targetPlatform)); + } + + public function testCanBeUsedWithDefinedPhpIni(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('loadedIniConfigurationFile') + ->willReturn('/path/to/php.ini'); + + self::assertTrue($this->standardSinglePhpIni->canBeUsed($this->targetPlatform)); + } + + public function testSetupReturnsWhenIniFileIsNotSet(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('loadedIniConfigurationFile') + ->willReturn(null); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::never()) + ->method('__invoke'); + + self::assertFalse($this->standardSinglePhpIni->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + } + + public function testReturnsTrueWhenCheckAndAddExtensionIsInvoked(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('loadedIniConfigurationFile') + ->willReturn(self::INI_FILE); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + ) + ->willReturn(true); + + self::assertTrue($this->standardSinglePhpIni->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + } + + public function testReturnsFalseWhenCheckAndAddExtensionIsInvoked(): void + { + $this->mockPhpBinary + ->expects(self::once()) + ->method('loadedIniConfigurationFile') + ->willReturn(self::INI_FILE); + + $this->checkAndAddExtensionToIniIfNeeded + ->expects(self::once()) + ->method('__invoke') + ->with( + self::INI_FILE, + $this->targetPlatform, + $this->downloadedPackage, + $this->output, + ) + ->willReturn(false); + + self::assertFalse($this->standardSinglePhpIni->setup( + $this->targetPlatform, + $this->downloadedPackage, + $this->binaryFile, + $this->output, + )); + } +} diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 39ffdaa3..5e3bd10f 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -5,22 +5,27 @@ namespace Php\PieUnitTest\Platform\TargetPhp; use Composer\Util\Platform; +use Php\Pie\ExtensionName; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; +use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; use Php\Pie\Platform\TargetPhp\Exception\InvalidPhpBinaryPath; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Process\PhpExecutableFinder; use function array_column; use function array_combine; use function array_filter; +use function array_key_exists; use function array_map; use function array_unique; use function assert; +use function count; use function defined; use function dirname; use function file_exists; @@ -271,4 +276,32 @@ public function testDifferentVersionsOfPhp(string $phpPath): void self::assertGreaterThan(0, $php->phpIntSize()); self::assertNotEmpty($php->phpinfo()); } + + public function testAssertExtensionIsLoaded(): void + { + $php = PhpBinaryPath::fromCurrentProcess(); + $loadedExtensions = $php->extensions(); + + if (! count($loadedExtensions) || ! array_key_exists('Core', $loadedExtensions)) { + self::fail('Core extension is not loaded, this is quite unexpected...'); + } + + $output = new BufferedOutput(BufferedOutput::VERBOSITY_VERBOSE); + $php->assertExtensionIsLoadedInRuntime(ExtensionName::normaliseFromString('Core'), $output); + + self::assertStringContainsString( + 'Successfully asserted that extension Core is loaded in runtime.', + $output->fetch(), + ); + } + + public function testAssertExtensionFailsWhenNotLoaded(): void + { + $php = PhpBinaryPath::fromCurrentProcess(); + + $this->expectException(ExtensionIsNotLoaded::class); + $php->assertExtensionIsLoadedInRuntime(ExtensionName::normaliseFromString( + 'hopefully_this_extension_name_is_not_real_otherwise_this_test_will_fail', + )); + } } diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php index f152af9b..75e72a6b 100644 --- a/test/unit/Platform/WindowsExtensionAssetNameTest.php +++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php @@ -55,6 +55,7 @@ public function setUp(): void null, null, null, + 99, ); }