Skip to content

Commit

Permalink
Merge pull request #357 from TravisCarden/feature/composer-is-availab…
Browse files Browse the repository at this point in the history
…le-use-composer-process-runner

Make `ComposerIsAvailable` use `ComposerProcessRunner` instead of `ProcessFactory`
  • Loading branch information
TravisCarden committed Apr 10, 2024
2 parents afae8d9 + f7aa963 commit 562354d
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 210 deletions.
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Expand Up @@ -18,7 +18,7 @@ parameters:
- src
treatPhpDocTypesAsCertain: false
checkGenericClassInNonGenericObjectType: false
preconditionSystemHash: 282aea28dca092b4141a77e48da90a80
preconditionSystemHash: 8dfddc6171adcfe004a0bfaea2545f8e
translationSystemHash: dec82389af17442fa61cc1fcc6f89c3e
gitattributesExportInclude:
- composer.json
Expand Down
51 changes: 19 additions & 32 deletions src/Internal/Precondition/Service/ComposerIsAvailable.php
Expand Up @@ -11,7 +11,8 @@
use PhpTuf\ComposerStager\API\Path\Value\PathInterface;
use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
use PhpTuf\ComposerStager\API\Precondition\Service\ComposerIsAvailableInterface;
use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface;
use PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface;
use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface;
use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface;
use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface;
use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface;
Expand All @@ -26,9 +27,10 @@ final class ComposerIsAvailable extends AbstractPrecondition implements Composer
private string $executablePath;

public function __construct(
private readonly ComposerProcessRunnerInterface $composerProcessRunner,
EnvironmentInterface $environment,
private readonly ExecutableFinderInterface $executableFinder,
private readonly ProcessFactoryInterface $processFactory,
private readonly OutputCallbackInterface $outputCallback,
TranslatableFactoryInterface $translatableFactory,
) {
parent::__construct($environment, $translatableFactory);
Expand Down Expand Up @@ -76,9 +78,7 @@ private function assertExecutableExists(): void
/** @throws \PhpTuf\ComposerStager\API\Exception\PreconditionException */
private function assertIsActuallyComposer(): void
{
$process = $this->getProcess();

if (!$this->isValidExecutable($process)) {
if (!$this->isValidExecutable()) {
throw new PreconditionException($this, $this->t(
'The Composer executable at %name is invalid.',
$this->p(['%name' => $this->executablePath]),
Expand All @@ -87,36 +87,12 @@ private function assertIsActuallyComposer(): void
}
}

/** @throws \PhpTuf\ComposerStager\API\Exception\PreconditionException */
private function getProcess(): ProcessInterface
{
try {
return $this->processFactory->create([
$this->executablePath,
'list',
'--format=json',
]);
} catch (LogicException $e) {
throw new PreconditionException($this, $this->t(
'Cannot check for Composer due to a host configuration problem: %details',
$this->p(['%details' => $e->getMessage()]),
$this->d()->exceptions(),
), 0, $e);
}
}

private function isValidExecutable(ProcessInterface $process): bool
private function isValidExecutable(): bool
{
try {
$process->mustRun();
$output = $process->getOutput();
} catch (ExceptionInterface) {
return false;
}

try {
$output = $this->getComposerOutput();
$data = json_decode($output, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
} catch (ExceptionInterface|JsonException) {
return false;
}

Expand All @@ -126,4 +102,15 @@ private function isValidExecutable(ProcessInterface $process): bool

return $data['application']['name'] === 'Composer';
}

/** @throws \PhpTuf\ComposerStager\API\Exception\ExceptionInterface */
private function getComposerOutput(): string
{
$this->composerProcessRunner->run(['--format=json'], null, [], $this->outputCallback);

$output = $this->outputCallback->getOutput();
$this->outputCallback->clearOutput();

return implode('', $output);
}
}
98 changes: 77 additions & 21 deletions tests/Precondition/Service/ComposerIsAvailableFunctionalTest.php
Expand Up @@ -5,37 +5,43 @@
use PhpTuf\ComposerStager\API\Exception\LogicException;
use PhpTuf\ComposerStager\API\Exception\PreconditionException;
use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface;
use PhpTuf\ComposerStager\Internal\Finder\Service\ExecutableFinder;
use PhpTuf\ComposerStager\Internal\Precondition\Service\ComposerIsAvailable;
use PhpTuf\ComposerStager\Tests\TestCase;
use PhpTuf\ComposerStager\Tests\TestUtils\ContainerTestHelper;
use PhpTuf\ComposerStager\Tests\TestUtils\FilesystemTestHelper;
use PhpTuf\ComposerStager\Tests\TestUtils\PathTestHelper;
use PhpTuf\ComposerStager\Tests\TestUtils\TranslationTestHelper;
use ReflectionProperty;
use Symfony\Component\DependencyInjection\Definition;

/** @coversNothing */
/**
* @coversDefaultClass \PhpTuf\ComposerStager\Internal\Precondition\Service\ComposerIsAvailable
*
* @covers ::__construct
*/
final class ComposerIsAvailableFunctionalTest extends TestCase
{
private string $executableFinderClass;

protected function setUp(): void
{
self::createTestEnvironment();
self::mkdir(self::stagingDirRelative());

$this->executableFinderClass = ExecutableFinder::class;
}

protected function tearDown(): void
{
self::removeTestEnvironment();
}

private function createSut(): ComposerIsAvailable
private function createSut(?string $executableFinderClass = null): ComposerIsAvailable
{
if ($executableFinderClass === null) {
return ContainerTestHelper::get(ComposerIsAvailable::class);
}

$container = ContainerTestHelper::container();

// Override the ExecutableFinder implementation.
$executableFinder = new Definition($this->executableFinderClass);
$executableFinder = new Definition($executableFinderClass);
$container->setDefinition(ExecutableFinderInterface::class, $executableFinder);

// Compile the container.
Expand All @@ -45,51 +51,101 @@ private function createSut(): ComposerIsAvailable
return $container->get(ComposerIsAvailable::class);
}

// The happy path, which would usually have a test method here, is implicitly tested in the end-to-end test.
// @see \PhpTuf\ComposerStager\Tests\EndToEnd\EndToEndFunctionalTestCase
/**
* @covers ::assertExecutableExists
* @covers ::assertIsActuallyComposer
* @covers ::doAssertIsFulfilled
* @covers ::getFulfilledStatusMessage
* @covers ::isValidExecutable
*/
public function testFulfilled(): void
{
$sut = $this->createSut();

$isFulfilled = $sut->isFulfilled(self::activeDirPath(), self::stagingDirPath());
$isStillFulfilled = $sut->isFulfilled(self::activeDirPath(), self::stagingDirPath());

self::assertTrue($isFulfilled, 'Found Composer.');
self::assertTrue($isStillFulfilled, 'Achieved idempotency');
}

/** @covers ::assertExecutableExists */
public function testComposerNotFound(): void
{
$this->executableFinderClass = ComposerNotFoundExecutableFinder::class;
$sut = $this->createSut();
$sut = $this->createSut(ComposerNotFoundExecutableFinder::class);

$message = ComposerNotFoundExecutableFinder::EXCEPTION_MESSAGE;
$message = 'Cannot find Composer.';
self::assertTranslatableException(static function () use ($sut): void {
$sut->assertIsFulfilled(self::activeDirPath(), self::stagingDirPath());
}, PreconditionException::class, $message, null, LogicException::class);
}

public function testInvalidComposerFound(): void
/**
* @covers ::assertIsActuallyComposer
* @covers ::getComposerOutput
* @covers ::isValidExecutable
*
* @dataProvider providerInvalidComposerFound
*/
public function testInvalidComposerFound(string $output): void
{
$this->executableFinderClass = InvalidComposerFoundExecutableFinder::class;
$sut = $this->createSut();
$sut = $this->createSut(InvalidComposerFoundExecutableFinder::class);

// Dynamically set invalid executable output.
$reflection = new ReflectionProperty($sut, 'executableFinder');
/** @var \PhpTuf\ComposerStager\Tests\Precondition\Service\InvalidComposerFoundExecutableFinder $executableFinder */
$executableFinder = $reflection->getValue($sut);
$executableFinder->output = $output;

$message = InvalidComposerFoundExecutableFinder::getExceptionMessage();
self::assertTranslatableException(static function () use ($sut): void {
$sut->assertIsFulfilled(self::activeDirPath(), self::stagingDirPath());
}, PreconditionException::class, $message);
}

public function providerInvalidComposerFound(): array
{
return [
'No output' => [''],
'Non-JSON' => ['invalid'],
'Empty JSON/missing application name' => ['{}'],
'Wrong application name' => ['{"application":{"name":"Invalid"}}'],
];
}
}

final class ComposerNotFoundExecutableFinder implements ExecutableFinderInterface
{
public const EXCEPTION_MESSAGE = 'Cannot find Composer.';

public function find(string $name): string
{
throw new LogicException(TranslationTestHelper::createTranslatableMessage(self::EXCEPTION_MESSAGE));
throw new LogicException(TranslationTestHelper::createTranslatableMessage(''));
}
}

final class InvalidComposerFoundExecutableFinder implements ExecutableFinderInterface
{
public string $output = '';

public function find(string $name): string
{
return __FILE__;
$executable = self::executablePath();

file_put_contents($executable, <<<END
#!/usr/bin/env bash
echo '{$this->output}'
END);
FilesystemTestHelper::chmod($executable, 0777);

return $executable;
}

public static function getExceptionMessage(): string
{
return sprintf('The Composer executable at %s is invalid.', __FILE__);
return sprintf('The Composer executable at %s is invalid.', self::executablePath());
}

private static function executablePath(): string
{
return PathTestHelper::makeAbsolute('composer');
}
}

0 comments on commit 562354d

Please sign in to comment.