1
0
mirror of https://github.com/php/pie.git synced 2026-03-23 23:12:17 +01:00
Files
archived-pie/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
2026-03-18 07:22:06 +00:00

533 lines
19 KiB
PHP

<?php
declare(strict_types=1);
namespace Php\PieUnitTest\Platform\TargetPhp;
use Composer\IO\BufferIO;
use Composer\Util\Platform;
use Php\Pie\ExtensionName;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\DebugBuild;
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 Php\Pie\Util\Process;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use Symfony\Component\Console\Output\OutputInterface;
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 chmod;
use function count;
use function defined;
use function dirname;
use function file_exists;
use function file_put_contents;
use function get_loaded_extensions;
use function ini_get;
use function is_dir;
use function is_executable;
use function mkdir;
use function phpversion;
use function sprintf;
use function strtolower;
use function sys_get_temp_dir;
use function tempnam;
use function trim;
use function uniqid;
use function unlink;
use const DIRECTORY_SEPARATOR;
use const PHP_INT_SIZE;
use const PHP_MAJOR_VERSION;
use const PHP_MINOR_VERSION;
use const PHP_OS_FAMILY;
use const PHP_RELEASE_VERSION;
#[CoversClass(PhpBinaryPath::class)]
final class PhpBinaryPathTest extends TestCase
{
private const FAKE_PHP_EXECUTABLE = __DIR__ . '/../../../assets/fake-php.sh';
private const PHP_INVALID_VERSION = __DIR__ . '/../../../assets/fake-php-invalid-version.sh';
private const VALID_PHP_WITH_WARNINGS = __DIR__ . '/../../../assets/valid-php-with-warnings.sh';
public function testNonExistentPhpBinaryIsRejected(): void
{
$this->expectException(InvalidPhpBinaryPath::class);
$this->expectExceptionMessage('does not exist');
PhpBinaryPath::fromPhpBinaryPath(__DIR__ . '/path/to/a/non/existent/php/binary');
}
public function testNonExecutablePhpBinaryIsRejected(): void
{
if (Platform::isWindows()) {
/**
* According to the {@link https://www.php.net/manual/en/function.is-executable.php}:
*
* for BC reasons, files with a .bat or .cmd extension are also considered executable
*
* However, that does not seem to be the case; calling {@see is_executable} always seems to return false,
* even with a `.bat` file.
*/
self::markTestSkipped('is_executable always returns false on Windows it seems...');
}
$this->expectException(InvalidPhpBinaryPath::class);
$this->expectExceptionMessage('is not executable');
PhpBinaryPath::fromPhpBinaryPath(__FILE__);
}
#[RequiresOperatingSystemFamily('Linux')]
public function testInvalidPhpBinaryIsRejected(): void
{
$this->expectException(InvalidPhpBinaryPath::class);
$this->expectExceptionMessage('does not appear to be a PHP binary');
PhpBinaryPath::fromPhpBinaryPath(self::FAKE_PHP_EXECUTABLE);
}
#[RequiresOperatingSystemFamily('Linux')]
public function testInvalidVersion(): void
{
$phpBinary = PhpBinaryPath::fromPhpBinaryPath(self::PHP_INVALID_VERSION);
self::assertSame('5.6.40', $phpBinary->version());
self::assertSame('5.6', $phpBinary->majorMinorVersion());
}
public function testWarningsAndDeprecationsAreFiltered(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Bash script does not run on Windows.');
}
$phpBinary = PhpBinaryPath::fromPhpBinaryPath(self::VALID_PHP_WITH_WARNINGS);
self::assertSame(self::VALID_PHP_WITH_WARNINGS, $phpBinary->phpBinaryPath);
}
public function testVersionFromCurrentProcess(): void
{
$phpBinary = PhpBinaryPath::fromCurrentProcess();
self::assertSame(
PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
$phpBinary->version(),
);
$phpConfig = $phpBinary->phpConfigPath();
if ($phpConfig === null) {
return;
}
self::assertSame($phpBinary->phpBinaryPath, Process::run([$phpConfig, '--php-binary']));
}
/** @return array<string, array{0: non-empty-string, 1: non-empty-string}> */
public static function phpConfigPathProvider(): array
{
// data providers cannot return empty, even if the test is skipped
if (Platform::isWindows()) {
return ['skip' => ['skip', 'skip']];
}
$possiblePhpConfigPaths = array_filter(
[
['/usr/bin/php-config8.5', '8.5'],
['/usr/bin/php-config8.4', '8.4'],
['/usr/bin/php-config8.3', '8.3'],
['/usr/bin/php-config8.2', '8.2'],
['/usr/bin/php-config8.1', '8.1'],
['/usr/bin/php-config8.0', '8.0'],
['/usr/bin/php-config7.4', '7.4'],
['/usr/bin/php-config7.3', '7.3'],
['/usr/bin/php-config7.2', '7.2'],
['/usr/bin/php-config7.1', '7.1'],
['/usr/bin/php-config5.6', '5.6'],
],
static fn (array $phpConfigPath) => file_exists($phpConfigPath[0])
&& is_executable($phpConfigPath[0]),
);
if ($possiblePhpConfigPaths === []) {
return ['skip' => ['skip', 'skip']];
}
return array_combine(
array_column($possiblePhpConfigPaths, 0),
$possiblePhpConfigPaths,
);
}
#[DataProvider('phpConfigPathProvider')]
public function testFromPhpConfigExecutable(string $phpConfigPath, string $expectedMajorMinor): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Do not need to test php-config on Windows as we are not building on Windows.');
}
if ($expectedMajorMinor === 'skip') {
self::markTestSkipped('No known system php-config could be found.');
}
assert($phpConfigPath !== '');
$phpBinary = PhpBinaryPath::fromPhpConfigExecutable($phpConfigPath);
self::assertSame(
$expectedMajorMinor,
$phpBinary->majorMinorVersion(),
);
self::assertStringStartsWith(
$expectedMajorMinor . '.',
$phpBinary->version(),
);
self::assertSame($phpConfigPath, $phpBinary->phpConfigPath());
}
public function testExtensions(): void
{
$exts = get_loaded_extensions();
$extVersions = array_map(
static function ($extension) {
$extVersion = phpversion($extension);
if ($extVersion === false) {
return '0';
}
return $extVersion;
},
$exts,
);
self::assertSame(
array_combine($exts, $extVersions),
PhpBinaryPath::fromCurrentProcess()
->extensions(),
);
}
public function testOperatingSystem(): void
{
self::assertSame(
defined('PHP_WINDOWS_VERSION_BUILD') ? OperatingSystem::Windows : OperatingSystem::NonWindows,
PhpBinaryPath::fromCurrentProcess()
->operatingSystem(),
);
}
public function testOperatingSystemFamily(): void
{
self::assertSame(
OperatingSystemFamily::from(strtolower(PHP_OS_FAMILY)),
PhpBinaryPath::fromCurrentProcess()
->operatingSystemFamily(),
);
}
public function testMajorMinorVersion(): void
{
self::assertSame(
PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
PhpBinaryPath::fromCurrentProcess()
->majorMinorVersion(),
);
}
/** @return list<array{0: OperatingSystem, 1: string, 2: string, 3: int, 4: Architecture}> */
public static function machineTypeProvider(): array
{
return [
// x86 (32-bit)
[OperatingSystem::Windows, 'Architecture => x32', '', 4, Architecture::x86],
[OperatingSystem::NonWindows, 'Architecture => x86', 'x86', 4, Architecture::x86],
[OperatingSystem::NonWindows, '', 'x86', 4, Architecture::x86],
// x86_64 (64-bit)
[OperatingSystem::Windows, 'Architecture => x64', 'AMD64', 8, Architecture::x86_64],
[OperatingSystem::Windows, 'Architecture => x64', '', 8, Architecture::x86_64],
[OperatingSystem::NonWindows, 'Architecture => x86_64', 'x86_64', 8, Architecture::x86_64],
[OperatingSystem::NonWindows, '', 'x86_64', 8, Architecture::x86_64],
// arm64
[OperatingSystem::NonWindows, 'Architecture => arm64', 'arm64', 8, Architecture::arm64],
[OperatingSystem::NonWindows, '', 'arm64', 8, Architecture::arm64],
[OperatingSystem::NonWindows, 'Architecture => aarch64', 'aarch64', 8, Architecture::arm64],
[OperatingSystem::NonWindows, '', 'aarch64', 8, Architecture::arm64],
];
}
#[RequiresOperatingSystemFamily('Linux')]
#[DataProvider('machineTypeProvider')]
public function testMachineType(OperatingSystem $os, string $phpinfo, string $uname, int $phpIntSize, Architecture $expectedArchitecture): void
{
$tmpSh = tempnam(sys_get_temp_dir(), uniqid('pie_machine_type_test'));
file_put_contents($tmpSh, "#!/usr/bin/env bash\necho \"" . $uname . "\";\n");
chmod($tmpSh, 0777);
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['operatingSystem', 'phpinfo', 'phpIntSize']);
(new ReflectionMethod($phpBinary, '__construct'))->invoke($phpBinary, $tmpSh, null);
$phpBinary->method('operatingSystem')->willReturn($os);
$phpBinary->method('phpinfo')->willReturn($phpinfo);
$phpBinary->method('phpIntSize')->willReturn($phpIntSize);
self::assertEquals($expectedArchitecture, $phpBinary->machineType());
unlink($tmpSh);
}
public function testPhpIntSize(): void
{
self::assertSame(
PHP_INT_SIZE,
PhpBinaryPath::fromCurrentProcess()
->phpIntSize(),
);
}
#[RequiresOperatingSystemFamily('Linux')]
public function testExtensionPathOnLinuxThatAlreadyExists(): void
{
$phpBinary = PhpBinaryPath::fromCurrentProcess();
$expectedExtensionDir = (string) ini_get('extension_dir');
self::assertNotEmpty($expectedExtensionDir);
self::assertDirectoryExists($expectedExtensionDir);
self::assertSame(
$expectedExtensionDir,
$phpBinary->extensionPath(),
);
}
#[RequiresOperatingSystemFamily('Linux')]
public function testExtensionPathWithInstallRootPrefixOnLinuxThatAlreadyExists(): void
{
$installRoot = '/tmp/' . uniqid('pie-test-install-root-existing-', true);
$phpBinary = PhpBinaryPath::fromCurrentProcess();
$expectedExtensionDir = $installRoot . ini_get('extension_dir');
mkdir($expectedExtensionDir, 0777, true);
self::assertDirectoryExists($expectedExtensionDir);
self::assertSame(
$expectedExtensionDir,
$phpBinary->extensionPath($installRoot),
);
}
#[RequiresOperatingSystemFamily('Windows')]
public function testExtensionPathOnWindows(): void
{
$phpBinary = PhpBinaryPath::fromCurrentProcess();
$expectedExtensionDir = (string) ini_get('extension_dir');
self::assertNotEmpty($expectedExtensionDir);
// `extension_dir` may be a relative URL on Windows (e.g. "ext"), so resolve it according to the location of PHP
if (! file_exists($expectedExtensionDir) || ! is_dir($expectedExtensionDir)) {
$absoluteExtensionDir = dirname($phpBinary->phpBinaryPath) . DIRECTORY_SEPARATOR . $expectedExtensionDir;
if (file_exists($absoluteExtensionDir) && is_dir($absoluteExtensionDir)) {
$expectedExtensionDir = $absoluteExtensionDir;
}
}
self::assertSame(
$expectedExtensionDir,
$phpBinary->extensionPath(),
);
}
#[RequiresOperatingSystemFamily('Windows')]
public function testRelativeExtensionPathOnWindowsIsFilled(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
(new ReflectionMethod($phpBinary, '__construct'))
->invoke($phpBinary, trim((string) (new PhpExecutableFinder())->find()), null);
$configuredExtensionPath = 'foo';
self::assertDirectoryDoesNotExist($configuredExtensionPath, 'test cannot run if the same-named extension dir already exists in cwd');
$fullExtensionPath = dirname($phpBinary->phpBinaryPath) . DIRECTORY_SEPARATOR . $configuredExtensionPath;
mkdir($fullExtensionPath, 0777, true);
self::assertDirectoryExists($fullExtensionPath);
$phpBinary->expects(self::once())
->method('phpinfo')
->willReturn(sprintf('extension_dir => %s => %s', $configuredExtensionPath, $configuredExtensionPath));
self::assertSame($fullExtensionPath, $phpBinary->extensionPath());
}
public function testExtensionPathIsImplicitlyCreated(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
(new ReflectionMethod($phpBinary, '__construct'))
->invoke($phpBinary, trim((string) (new PhpExecutableFinder())->find()), null);
$configuredExtensionPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('PIE_non_existent_extension_path', true);
self::assertDirectoryDoesNotExist($configuredExtensionPath);
$phpBinary->expects(self::once())
->method('phpinfo')
->willReturn(sprintf('extension_dir => %s => %s', $configuredExtensionPath, $configuredExtensionPath));
self::assertSame($configuredExtensionPath, $phpBinary->extensionPath());
self::assertDirectoryExists($configuredExtensionPath);
}
#[RequiresOperatingSystemFamily('Linux')]
public function testExtensionPathWithInstallRootPrefixIsImplicitlyCreated(): void
{
$installRoot = '/tmp/' . uniqid('pie-test-install-root-not-existing-', true);
$phpBinary = PhpBinaryPath::fromCurrentProcess();
$expectedExtensionDir = $installRoot . ini_get('extension_dir');
self::assertDirectoryDoesNotExist($expectedExtensionDir);
self::assertSame($expectedExtensionDir, $phpBinary->extensionPath($installRoot));
self::assertDirectoryExists($expectedExtensionDir);
}
/** @return array<string, array{0: string}> */
public static function phpPathProvider(): array
{
$possiblePhpBinaries = array_filter(
array_unique([
'/usr/bin/php',
(string) (new PhpExecutableFinder())->find(),
'/usr/bin/php8.4',
'/usr/bin/php8.3',
'/usr/bin/php8.2',
'/usr/bin/php8.1',
'/usr/bin/php8.0',
'/usr/bin/php7.4',
'/usr/bin/php7.3',
'/usr/bin/php7.2',
'/usr/bin/php7.1',
'/usr/bin/php7.0',
'/usr/bin/php5.6',
]),
static fn (string $phpPath) => file_exists($phpPath) && is_executable($phpPath),
);
return array_combine(
$possiblePhpBinaries,
array_map(static fn (string $phpPath) => [$phpPath], $possiblePhpBinaries),
);
}
#[DataProvider('phpPathProvider')]
public function testDifferentVersionsOfPhp(string $phpPath): void
{
assert($phpPath !== '');
$php = PhpBinaryPath::fromPhpBinaryPath($phpPath);
self::assertArrayHasKey('Core', $php->extensions());
self::assertNotEmpty($php->extensionPath());
self::assertNotEmpty($php->version());
self::assertNotEmpty($php->majorMinorVersion());
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...');
}
$io = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE);
$php->assertExtensionIsLoadedInRuntime(ExtensionName::normaliseFromString('Core'), $io);
self::assertStringContainsString(
'Successfully asserted that extension Core is loaded in runtime.',
$io->getOutput(),
);
}
public function testAssertDifferentCasedExtensionIsLoaded(): 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...');
}
$io = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE);
$php->assertExtensionIsLoadedInRuntime(ExtensionName::normaliseFromString('CORE'), $io);
self::assertStringContainsString(
'Successfully asserted that extension CORE is loaded in runtime.',
$io->getOutput(),
);
}
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',
));
}
public function testBuildProviderWhenConfigured(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
$phpBinary->expects(self::once())
->method('phpinfo')
->willReturn('Build Provider => My build provider');
self::assertSame('My build provider', $phpBinary->buildProvider());
}
public function testBuildProviderNullWhenNotConfigured(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
$phpBinary->expects(self::once())
->method('phpinfo')
->willReturn('');
self::assertNull($phpBinary->buildProvider());
}
public function testDebugBuildModeReturnsDebugWhenYes(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
$phpBinary->expects(self::once())
->method('phpinfo')
->willReturn('Debug Build => no');
self::assertSame(DebugBuild::NoDebug, $phpBinary->debugMode());
}
public function testDebugBuildModeReturnsNoDebugWhenNo(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
$phpBinary->expects(self::once())
->method('phpinfo')
->willReturn('Debug Build => yes');
self::assertSame(DebugBuild::Debug, $phpBinary->debugMode());
}
}