Skip to content

Commit

Permalink
Fix uninstall and complete tests
Browse files Browse the repository at this point in the history
  • Loading branch information
asgrim committed Feb 27, 2025
1 parent 1e14281 commit 927d822
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 12 deletions.
6 changes: 6 additions & 0 deletions features/uninstall-extensions.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Feature: Extensions can be uninstalled with PIE

Example: The latest version of an extension can be downloaded
Given an extension was previously installed
When I run a command to uninstall an extension
Then the extension should not be installed anymore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Transaction;
use Composer\Installer\InstallerEvent;
use Composer\Installer\InstallerEvents;
Expand Down Expand Up @@ -49,7 +50,7 @@ public function __invoke(InstallerEvent $installerEvent): void
$newOperations = array_filter(
$installerEvent->getTransaction()?->getOperations() ?? [],
function (OperationInterface $operation) use ($pieOutput): bool {
if (! $operation instanceof InstallOperation) {
if (! $operation instanceof InstallOperation && ! $operation instanceof UninstallOperation) {
$pieOutput->writeln(
sprintf(
'Unexpected operation during installer: %s',
Expand Down
18 changes: 14 additions & 4 deletions src/ComposerIntegration/UninstallProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Php\Pie\DependencyResolver\Package;
use Php\Pie\Installing\Ini\RemoveIniEntry;
use Php\Pie\Installing\Uninstall;
use Symfony\Component\Console\Output\OutputInterface;

use function array_walk;
use function count;
Expand Down Expand Up @@ -35,14 +36,23 @@ public function __invoke(
$affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform);

if (count($affectedIniFiles) === 1) {
$output->writeln(sprintf('INI file "%s" was updated to remove the extension.', reset($affectedIniFiles)));
$output->writeln(
sprintf('INI file "%s" was updated to remove the extension.', reset($affectedIniFiles)),
OutputInterface::VERBOSITY_VERBOSE,
);
} elseif (count($affectedIniFiles) === 0) {
$output->writeln('No INI files were updated to remove the extension.');
$output->writeln(
'No INI files were updated to remove the extension.',
OutputInterface::VERBOSITY_VERBOSE,
);
} else {
$output->writeln('The following INI files were updated to remove the extnesion:');
$output->writeln(
'The following INI files were updated to remove the extnesion:',
OutputInterface::VERBOSITY_VERBOSE,
);
array_walk($affectedIniFiles, static fn (string $ini) => $output->writeln(' - ' . $ini));
}

($this->uninstall)($piePackage);
$output->writeln(sprintf('👋 <info>Removed extension:</info> %s', ($this->uninstall)($piePackage)->filePath));
}
}
7 changes: 6 additions & 1 deletion src/Installing/Ini/RemoveIniEntryWithFileGetContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ class RemoveIniEntryWithFileGetContents implements RemoveIniEntry
/** @return list<string> Returns a list of INI files that were updated to remove the extension */
public function __invoke(Package $package, TargetPlatform $targetPlatform): array
{
$allIniFiles = [$targetPlatform->phpBinaryPath->loadedIniConfigurationFile()];
$allIniFiles = [];

$mainIni = $targetPlatform->phpBinaryPath->loadedIniConfigurationFile();
if ($mainIni !== null) {
$allIniFiles[] = $mainIni;
}

$additionalIniDirectory = $targetPlatform->phpBinaryPath->additionalIniDirectory();
if ($additionalIniDirectory !== null) {
Expand Down
3 changes: 2 additions & 1 deletion src/Installing/Uninstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

namespace Php\Pie\Installing;

use Php\Pie\BinaryFile;
use Php\Pie\DependencyResolver\Package;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
interface Uninstall
{
public function __invoke(Package $package): void;
public function __invoke(Package $package): BinaryFile;
}
4 changes: 3 additions & 1 deletion src/Installing/UninstallUsingUnlink.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class UninstallUsingUnlink implements Uninstall
{
public function __invoke(Package $package): void
public function __invoke(Package $package): BinaryFile
{
$pieMetadata = PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($package->composerPackage());

Expand All @@ -40,6 +40,8 @@ public function __invoke(Package $package): void
$expectedBinaryFile->verify();

unlink($expectedBinaryFile->filePath);

// @todo verify the unlink worked etc, maybe permissions failed
return $expectedBinaryFile;
}
}
21 changes: 21 additions & 0 deletions test/behaviour/CliContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,32 @@ public function theExtensionShouldHaveBeenBuiltWithOptions(): void
}

#[When('I run a command to install an extension')]
#[Given('an extension was previously installed')]
public function iRunACommandToInstallAnExtension(): void
{
$this->runPieCommand(['install', 'asgrim/example-pie-extension']);
}

#[When('I run a command to uninstall an extension')]
public function iRunACommandToUninstallAnExtension(): void
{
$this->runPieCommand(['uninstall', 'asgrim/example-pie-extension']);
}

#[Then('the extension should not be installed anymore')]
public function theExtensionShouldNotBeInstalled(): void
{
$this->assertCommandSuccessful();

Assert::regex($this->output, '#👋 Removed extension: [-_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('no', $isExtEnabled);
}

#[Then('the extension should have been installed')]
public function theExtensionShouldHaveBeenInstalled(): void
{
Expand Down
112 changes: 110 additions & 2 deletions test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,123 @@

namespace Php\PieUnitTest\Installing\Ini;

use Composer\Package\CompletePackageInterface;
use Composer\Util\Filesystem;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Installing\Ini\RemoveIniEntryWithFileGetContents;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Platform\ThreadSafetyMode;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Webmozart\Assert\Assert;

use function file_get_contents;
use function file_put_contents;
use function mkdir;
use function sys_get_temp_dir;
use function uniqid;

use const DIRECTORY_SEPARATOR;

#[CoversClass(RemoveIniEntryWithFileGetContents::class)]
final class RemoveIniEntryWithFileGetContentsTest extends TestCase
{
public function testRelevantIniFilesHaveExtensionRemoved(): void
private const INI_WITH_COMMENTED_EXTS = ";extension=foobar\n;zend_extension=foobar\n";
private const INI_WITH_ACTIVE_EXTS = "extension=foobar\nzend_extension=foobar\n";

private string $iniFilePath;

public function setUp(): void
{
self::fail('to be implemented'); // @todo
parent::setUp();

$this->iniFilePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_remove_ini_test', true);
mkdir($this->iniFilePath);
Assert::positiveInteger(file_put_contents(
$this->iniFilePath . DIRECTORY_SEPARATOR . 'with_commented_exts.ini',
self::INI_WITH_COMMENTED_EXTS,
));
Assert::positiveInteger(file_put_contents(
$this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini',
self::INI_WITH_ACTIVE_EXTS,
));
}

public function tearDown(): void
{
parent::tearDown();

(new Filesystem())->remove($this->iniFilePath);
}

/**
* @return array<non-empty-string, array{0: ExtensionType, 1: non-empty-string}>
*
* @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131
*/
public function extensionTypeProvider(): array
{
return [
'phpModule' => [ExtensionType::PhpModule, "; extension=foobar ; removed by PIE\nzend_extension=foobar\n"],
'zendExtension' => [ExtensionType::ZendExtension, "extension=foobar\n; zend_extension=foobar ; removed by PIE\n"],
];
}

#[DataProvider('extensionTypeProvider')]
public function testRelevantIniFilesHaveExtensionRemoved(ExtensionType $extensionType, string $expectedActiveContent): void
{
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
$phpBinaryPath
->method('loadedIniConfigurationFile')
->willReturn(null);
$phpBinaryPath
->method('additionalIniDirectory')
->willReturn($this->iniFilePath);

$package = new Package(
$this->createMock(CompletePackageInterface::class),
$extensionType,
ExtensionName::normaliseFromString('foobar'),
'foobar/foobar',
'1.2.3',
null,
);

$targetPlatform = new TargetPlatform(
OperatingSystem::NonWindows,
OperatingSystemFamily::Linux,
$phpBinaryPath,
Architecture::x86_64,
ThreadSafetyMode::ThreadSafe,
1,
null,
);

$affectedFiles = (new RemoveIniEntryWithFileGetContents())(
$package,
$targetPlatform,
);

self::assertSame(
[$this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini'],
$affectedFiles,
);

self::assertSame(
self::INI_WITH_COMMENTED_EXTS,
file_get_contents($this->iniFilePath . DIRECTORY_SEPARATOR . 'with_commented_exts.ini'),
);

self::assertSame(
$expectedActiveContent,
file_get_contents($this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini'),
);
}
}
57 changes: 55 additions & 2 deletions test/unit/Installing/UninstallUsingUnlinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,73 @@

namespace Php\PieUnitTest\Installing;

use Composer\Package\CompletePackageInterface;
use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Installing\PackageMetadataMissing;
use Php\Pie\Installing\UninstallUsingUnlink;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function file_put_contents;
use function hash_file;
use function sys_get_temp_dir;
use function uniqid;

use const DIRECTORY_SEPARATOR;

#[CoversClass(UninstallUsingUnlink::class)]
final class UninstallUsingUnlinkTest extends TestCase
{
public function testMissingMetadataThrowsException(): void
{
self::fail('to be implemented'); // @todo
$composerPackage = $this->createMock(CompletePackageInterface::class);
$composerPackage
->method('getExtra')
->willReturn([]);

$package = new Package(
$composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('foobar'),
'foobar/foobar',
'1.2.3',
null,
);

$this->expectException(PackageMetadataMissing::class);
$this->expectExceptionMessage('PIE metadata was missing for package foobar/foobar. Missing metadata keys: pie-installed-binary, pie-installed-binary-checksum');
(new UninstallUsingUnlink())($package);
}

public function testBinaryFileIsRemoved(): void
{
self::fail('to be implemented'); // @todo
$testFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_uninstall_binary_test_', true);
file_put_contents($testFilename, 'test content');
$testHash = hash_file('sha256', $testFilename);

$composerPackage = $this->createMock(CompletePackageInterface::class);
$composerPackage
->method('getExtra')
->willReturn([
PieInstalledJsonMetadataKeys::InstalledBinary->value => $testFilename,
PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash,
]);

$package = new Package(
$composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('foobar'),
'foobar/foobar',
'1.2.3',
null,
);

$uninstalled = (new UninstallUsingUnlink())($package);

self::assertSame($testFilename, $uninstalled->filePath);
self::assertFileDoesNotExist($testFilename);
}
}

0 comments on commit 927d822

Please sign in to comment.