diff --git a/app/config.php b/app/config.php index 3e0b693..892782f 100644 --- a/app/config.php +++ b/app/config.php @@ -17,13 +17,13 @@ use PhpSchool\WorkshopManager\Command\UninstallWorkshop; use PhpSchool\WorkshopManager\Command\UpdateWorkshop; use PhpSchool\WorkshopManager\Command\VerifyInstall; +use PhpSchool\WorkshopManager\ComposerInstaller; use PhpSchool\WorkshopManager\ComposerInstallerFactory; use PhpSchool\WorkshopManager\Downloader; use PhpSchool\WorkshopManager\Filesystem; use PhpSchool\WorkshopManager\Installer\Installer; use PhpSchool\WorkshopManager\Installer\Uninstaller; use PhpSchool\WorkshopManager\Installer\Updater; -use PhpSchool\WorkshopManager\IOFactory; use PhpSchool\WorkshopManager\Linker; use PhpSchool\WorkshopManager\ManagerState; use PhpSchool\WorkshopManager\Repository\InstalledWorkshopRepository; @@ -125,14 +125,13 @@ ); }), Installer::class => \DI\factory(function (ContainerInterface $c) { - $io = $c->get(IOFactory::class)->getNullableIO($c->get(InputInterface::class), $c->get(OutputInterface::class)); return new Installer( $c->get(InstalledWorkshopRepository::class), $c->get(RemoteWorkshopRepository::class), $c->get(Linker::class), $c->get(Filesystem::class), $c->get('appDir'), - new ComposerInstallerFactory($c->get(Factory::class), $io), + $c->get(ComposerInstaller::class), $c->get(Client::class), $c->get(VersionChecker::class) ); @@ -159,14 +158,14 @@ Client::class => \DI\factory(function (ContainerInterface $c) { return new Client; }), - Factory::class => \DI\object(), - IOFactory::class => \DI\object(), - IOInterface::class => \DI\factory(function (ContainerInterface $c) { - return $c->get(IOFactory::class)->getIO( + ComposerInstaller::class => function (ContainerInterface $c) { + return new ComposerInstaller( $c->get(InputInterface::class), - $c->get(OutputInterface::class) + $c->get(OutputInterface::class), + new Factory ); - }), + }, + Factory::class => \DI\object(), InputInterface::class => \Di\factory(function () { return new \Symfony\Component\Console\Input\ArgvInput($_SERVER['argv']); }), diff --git a/composer.json b/composer.json index 663244c..60e249b 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,11 @@ "symfony/filesystem": "^3.1", "tm/tooly-composer-script": "^1.0", "padraic/phar-updater": "^1.0", - "samsonasik/package-versions": "^1.0" + "samsonasik/package-versions": "^1.0", + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-zip": "*" }, "require-dev": { "phpunit/phpunit": "~5.4", diff --git a/src/Command/InstallWorkshop.php b/src/Command/InstallWorkshop.php index 1b5b1be..1c2b92d 100644 --- a/src/Command/InstallWorkshop.php +++ b/src/Command/InstallWorkshop.php @@ -68,10 +68,17 @@ public function __invoke(OutputInterface $output, $workshopName) '' ]); } catch (ComposerFailureException $e) { - $message = " There was a problem installing dependencies for \"%s\". Try running in verbose"; - $message .= " mode to see the composer error: %s \n"; + $message = " There was a problem installing dependencies for \"%s\".%s Try running in verbose"; + $message .= " mode to see more details: %s \n"; - $output->writeln(sprintf($message, $workshopName, $this->getCommand())); + $output->writeln( + sprintf( + $message, + $workshopName, + $e->getMessage() ? sprintf(' %s', $e->getMessage()) : '', + $this->getCommand() + ) + ); } catch (\Exception $e) { $output->writeln( sprintf(" An unknown error occurred: \"%s\" \n", $e->getMessage()) diff --git a/src/Command/VerifyInstall.php b/src/Command/VerifyInstall.php index 52dbfc4..c05f2b2 100644 --- a/src/Command/VerifyInstall.php +++ b/src/Command/VerifyInstall.php @@ -11,6 +11,11 @@ */ class VerifyInstall { + /** + * @var array + */ + private static $requiredExtensions = ['json', 'zip', 'mbstring', 'curl']; + /** * @var InputInterface */ @@ -26,6 +31,7 @@ class VerifyInstall */ private $workshopHomeDirectory; + /** * @param InputInterface $input * @param OutputInterface $output @@ -44,9 +50,9 @@ public function __invoke() $style->title("Verifying your installation"); - + if (strpos(getenv('PATH'), sprintf('%s/bin', $this->workshopHomeDirectory)) !== false) { - $style->success('Your $PATH environment variable is configured correctly'); + $style->success('Your $PATH environment variable is configured correctly.'); } else { $style->error('The PHP School bin directory is not in your PATH variable.'); @@ -68,13 +74,33 @@ public function __invoke() '' ]); } - + if (version_compare(PHP_VERSION, '5.6')) { - $message = 'Your PHP version is at least 5.6, which is required by this tool. Be aware that some '; - $message .= 'workshops may require a higher version of PHP, so you may not be able to install them.'; - $style->success($message); + $message = 'Your PHP version is %s, PHP 5.6 is the minimum supported version for this tool. Please note '; + $message .= 'that some workshops may require a higher version of PHP, so you may not be able to install '; + $message .= 'them without upgrading PHP.'; + $style->success(sprintf($message, PHP_VERSION)); } else { $style->error('You need a PHP version of at least 5.6 to use PHP School.'); } + + $missingExtensions = array_filter(static::$requiredExtensions, function ($extension) { + return !extension_loaded($extension); + }); + + array_walk($missingExtensions, function ($missingExtension) use ($style) { + $style->error( + sprintf( + 'The %s extension is missing - use your preferred package manager to install it.', + $missingExtension + ) + ); + }); + + if (empty($missingExtensions)) { + $message = 'All required PHP extensions are installed. Please note that some workshops may require '; + $message .= 'additional PHP extensions.'; + $style->success($message); + } } } diff --git a/src/ComposerInstaller.php b/src/ComposerInstaller.php new file mode 100644 index 0000000..a0c99ae --- /dev/null +++ b/src/ComposerInstaller.php @@ -0,0 +1,81 @@ + + */ +class ComposerInstaller +{ + /** + * @var InputInterface + */ + private $input; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var Factory + */ + private $composerFactory; + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param Factory $composerFactory + */ + public function __construct(InputInterface $input, OutputInterface $output, Factory $composerFactory) + { + $this->input = $input; + $this->output = $output; + $this->composerFactory = $composerFactory; + } + + /** + * @param string $pathToComposerProject + * @return InstallResult + */ + public function install($pathToComposerProject) + { + if ($this->output->isVerbose()) { + $output = $this->output; + } else { + //write all output in verbose mode to a temp stream + //so we don't write it out when not in verbose mode + $output = new StreamOutput( + fopen('php://memory', 'w'), + OutputInterface::VERBOSITY_VERY_VERBOSE, + $this->output->isDecorated(), + $this->output->getFormatter() + ); + } + + $wrappedOutput = new RecordingOutput($output); + $io = new ConsoleIO($this->input, $wrappedOutput, new HelperSet); + + $composer = $this->composerFactory->createComposer( + $io, + sprintf('%s/composer.json', rtrim($pathToComposerProject, '/')), + false, + $pathToComposerProject + ); + + return new InstallResult( + Installer::create($io, $composer)->run(), + $wrappedOutput->getOutput() + ); + } +} diff --git a/src/ComposerInstallerFactory.php b/src/ComposerInstallerFactory.php deleted file mode 100644 index 65cc21c..0000000 --- a/src/ComposerInstallerFactory.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class ComposerInstallerFactory -{ - /** - * @var ComposerFactory - */ - private $composerFactory; - - /** - * @var IOInterface - */ - private $io; - - /** - * @param ComposerFactory $composerFactory - * @param IOInterface $io - */ - public function __construct(ComposerFactory $composerFactory, IOInterface $io) - { - $this->io = $io; - $this->composerFactory = $composerFactory; - } - - /** - * @param string $path - * @return ComposerInstaller - */ - public function create($path) - { - $composer = $this->composerFactory->createComposer( - $this->io, - sprintf('%s/composer.json', $path), - false, - $path - ); - - return ComposerInstaller::create($this->io, $composer); - } -} diff --git a/src/Exception/ComposerFailureException.php b/src/Exception/ComposerFailureException.php index f2787d2..baecb39 100644 --- a/src/Exception/ComposerFailureException.php +++ b/src/Exception/ComposerFailureException.php @@ -6,14 +6,26 @@ * Class ComposerFailureException * @author Michael Woodward */ -final class ComposerFailureException extends \RuntimeException +class ComposerFailureException extends \RuntimeException { /** * @param \Exception $e - * @return ComposerFailureException + * @return self */ public static function fromException(\Exception $e) { return new self($e->getMessage()); } + + /** + * @param array $missingExtensions + * @return self + */ + public static function fromMissingExtensions(array $missingExtensions) + { + $message = 'This workshop requires some extra PHP extensions. Please install them'; + $message .= ' and try again. Required extensions are %s.'; + + return new self(sprintf($message, implode(', ', $missingExtensions))); + } } diff --git a/src/IOFactory.php b/src/IOFactory.php deleted file mode 100644 index 98b58e4..0000000 --- a/src/IOFactory.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class IOFactory -{ - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return IOInterface - */ - public function getIO(InputInterface $input, OutputInterface $output) - { - return new ConsoleIO($input, $output, new HelperSet); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return IOInterface - */ - public function getNullableIO(InputInterface $input, OutputInterface $output) - { - switch ($output->getVerbosity()) { - case OutputInterface::VERBOSITY_VERBOSE: - case OutputInterface::VERBOSITY_VERY_VERBOSE: - case OutputInterface::VERBOSITY_DEBUG: - return new ConsoleIO($input, $output, new HelperSet); - default: - return new NullIO; - } - } -} diff --git a/src/InstallResult.php b/src/InstallResult.php new file mode 100644 index 0000000..fc463c3 --- /dev/null +++ b/src/InstallResult.php @@ -0,0 +1,91 @@ + + */ +class InstallResult +{ + /** + * @var int + */ + private $exitCode; + + /** + * @var string + */ + private $output; + + /** + * @var Collection + */ + private $missingExtensions; + + /** + * @param int $exitCode + * @param string $output + */ + public function __construct($exitCode, $output) + { + $this->exitCode = $exitCode; + $this->output = $output; + + $this->checkForMissingExtensions(); + } + + private function checkForMissingExtensions() + { + $this->missingExtensions = collect(explode(PHP_EOL, $this->output)) + ->filter(function ($line) { + return preg_match( + '/the requested PHP extension [a-z-A-Z-_]+ is missing from your system/', + $line + ); + }) + ->map(function ($extError) { + preg_match( + '/the requested PHP extension ([a-z-A-Z-_]+) is missing from your system/', + $extError, + $match + ); + + return trim($match[1]); + }) + ->unique(); + } + + /** + * @return int + */ + public function getExitCode() + { + return $this->exitCode; + } + + /** + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * @return bool + */ + public function missingExtensions() + { + return !$this->missingExtensions->isEmpty(); + } + + /** + * @return array + */ + public function getMissingExtensions() + { + return $this->missingExtensions->all(); + } +} diff --git a/src/Installer/Installer.php b/src/Installer/Installer.php index fff4425..0640a6d 100644 --- a/src/Installer/Installer.php +++ b/src/Installer/Installer.php @@ -5,6 +5,7 @@ use Exception; use Github\Client; use Github\Exception\ExceptionInterface; +use PhpSchool\WorkshopManager\ComposerInstaller; use PhpSchool\WorkshopManager\ComposerInstallerFactory; use PhpSchool\WorkshopManager\Entity\InstalledWorkshop; use PhpSchool\WorkshopManager\Entity\Workshop; @@ -68,9 +69,9 @@ class Installer private $versionChecker; /** - * @var ComposerInstallerFactory + * @var ComposerInstaller */ - private $composerFactory; + private $composerInstaller; /** * @param InstalledWorkshopRepository $installedWorkshops @@ -78,7 +79,7 @@ class Installer * @param Linker $linker * @param Filesystem $filesystem * @param string $workshopHomeDirectory - * @param ComposerInstallerFactory $composerFactory + * @param ComposerInstaller $composerInstaller * @param Client $gitHubClient * @param VersionChecker $versionChecker * @param string|null $notifyUrlFormat @@ -89,7 +90,7 @@ public function __construct( Linker $linker, Filesystem $filesystem, $workshopHomeDirectory, - ComposerInstallerFactory $composerFactory, + ComposerInstaller $composerInstaller, Client $gitHubClient, VersionChecker $versionChecker, $notifyUrlFormat = null @@ -99,7 +100,7 @@ public function __construct( $this->linker = $linker; $this->filesystem = $filesystem; $this->workshopHomeDirectory = $workshopHomeDirectory; - $this->composerFactory = $composerFactory; + $this->composerInstaller = $composerInstaller; $this->gitHubClient = $gitHubClient; $this->versionChecker = $versionChecker; $this->notifyFormat = $notifyUrlFormat ?: $this->notifyFormat; @@ -163,17 +164,20 @@ public function installWorkshop($workshop) $this->filesystem->executeInPath($destinationPath, function ($path) { try { - $res = $this->composerFactory->create($path)->run(); + $result = $this->composerInstaller->install($path); } catch (Exception $e) { throw ComposerFailureException::fromException($e); } - if ($res > 0) { + if ($result->getExitCode() > 0) { + if ($result->missingExtensions()) { + throw ComposerFailureException::fromMissingExtensions($result->getMissingExtensions()); + } + throw new ComposerFailureException(); } }); - $installedWorkshop = InstalledWorkshop::fromWorkshop($workshop, $release->getTag()); $this->installedWorkshopRepository->add($installedWorkshop); $this->installedWorkshopRepository->save(); diff --git a/src/RecordingOutput.php b/src/RecordingOutput.php new file mode 100644 index 0000000..8202701 --- /dev/null +++ b/src/RecordingOutput.php @@ -0,0 +1,175 @@ + + */ +class RecordingOutput implements OutputInterface +{ + /** + * @var OutputInterface + */ + private $output; + + /** + * @var string + */ + private $buffer = ''; + + /** + * @param OutputInterface $output + */ + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + /** + * Returns whether verbosity is debug (-vvv). + * + * @return bool true if verbosity is set to VERBOSITY_DEBUG, false otherwise + */ + public function isDebug() + { + return $this->output->isDebug(); + } + + /** + * Sets output formatter. + * + * @param OutputFormatterInterface $formatter + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + $this->output->setFormatter($formatter); + } + + /** + * Returns whether verbosity is verbose (-v). + * + * @return bool true if verbosity is set to VERBOSITY_VERBOSE, false otherwise + */ + public function isVerbose() + { + return $this->output->isVerbose(); + } + + /** + * Returns whether verbosity is very verbose (-vv). + * + * @return bool true if verbosity is set to VERBOSITY_VERY_VERBOSE, false otherwise + */ + public function isVeryVerbose() + { + return $this->output->isVeryVerbose(); + } + + /** + * Writes a message to the output. + * + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered + * the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + */ + public function write($messages, $newline = false, $options = 0) + { + $messages = (array) $messages; + $this->buffer .= sprintf('%s%s', implode($newline ? "\n" : '', $messages), $newline ? "\n" : ''); + return $this->output->write($messages, $newline, $options); + } + + /** + * Writes a message to the output and adds a newline at the end. + * + * @param string|array $messages The message as an array of lines of a single string + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered + * the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + */ + public function writeln($messages, $options = 0) + { + return $this->write($messages, true, $options); + } + + /** + * Sets the verbosity of the output. + * + * @param int $level The level of verbosity (one of the VERBOSITY constants) + */ + public function setVerbosity($level) + { + $this->output->setVerbosity($level); + } + + /** + * Gets the current verbosity of the output. + * + * @return int The current level of verbosity (one of the VERBOSITY constants) + */ + public function getVerbosity() + { + return $this->output->getVerbosity(); + } + + /** + * Sets the decorated flag. + * + * @param bool $decorated Whether to decorate the messages + */ + public function setDecorated($decorated) + { + $this->output->setDecorated($decorated); + } + + /** + * Gets the decorated flag. + * + * @return bool true if the output will decorate messages, false otherwise + */ + public function isDecorated() + { + return $this->output->isDecorated(); + } + + /** + * Returns current output formatter instance. + * + * @return OutputFormatterInterface + */ + public function getFormatter() + { + return $this->output->getFormatter(); + } + + /** + * Returns whether verbosity is quiet (-q). + * + * @return bool true if verbosity is set to VERBOSITY_QUIET, false otherwise + */ + public function isQuiet() + { + return $this->output->isQuiet(); + } + + /** + * @return string + */ + public function getOutput() + { + //see \Composer\IO\BufferIO + return preg_replace_callback("{(?<=^|\n|\x08)(.+?)(\x08+)}", function ($matches) { + $pre = strip_tags($matches[1]); + + if (strlen($pre) === strlen($matches[2])) { + return ''; + } + + return rtrim($matches[1])."\n"; + }, $this->buffer); + } +} diff --git a/test/Command/InstallWorkshopTest.php b/test/Command/InstallWorkshopTest.php index 3de0988..04d9aa3 100644 --- a/test/Command/InstallWorkshopTest.php +++ b/test/Command/InstallWorkshopTest.php @@ -129,15 +129,18 @@ public function testWhenComposerInstallFails() ->expects($this->once()) ->method('installWorkshop') ->with('learnyouphp') - ->willThrowException(new ComposerFailureException('Some error')); + ->willThrowException(new ComposerFailureException('Some error.')); - $msg = " There was a problem installing dependencies for \"learnyouphp\". Try running in verbose mode"; - $msg .= sprintf(" to see the composer error: %s install -v \n", $_SERVER['argv'][0]); + $msg = " There was a problem installing dependencies for \"learnyouphp\". Some error."; + $msg .= sprintf( + " Try running in verbose mode to see more details: %s install -v \n", + $_SERVER['argv'][0] + ); $this->output ->expects($this->exactly(2)) ->method('writeln') ->withConsecutive( - [""], + [''], [$msg] ); diff --git a/test/Command/VerifyInstallTest.php b/test/Command/VerifyInstallTest.php index 395d744..0dbf35b 100644 --- a/test/Command/VerifyInstallTest.php +++ b/test/Command/VerifyInstallTest.php @@ -52,7 +52,7 @@ public function testErrorIsPrintedIfWorkshopDirNotInPath() $output = $this->output->fetch(); $this->assertRegExp( - sprintf('/%s/', preg_quote('The PHP School bin directory is not in your PATH variable.')), + sprintf('/%s/', preg_quote('[ERROR] The PHP School bin directory is not in your PATH variable.')), $output ); } @@ -65,7 +65,42 @@ public function testSuccessIsPrintedIfWorkshopDirInPath() $output = $this->output->fetch(); $this->assertRegExp( - sprintf('/%s/', preg_quote('Your $PATH environment variable is configured correctly')), + sprintf('/%s/', preg_quote('[OK] Your $PATH environment variable is configured correctly')), + $output + ); + } + + public function testAllRequiredExtensions() + { + $this->command->__invoke(); + + $output = $this->output->fetch(); + $this->assertRegExp( + sprintf('/%s/', preg_quote('[OK] All required PHP extensions are installed.')), + $output + ); + } + + public function testMissingExtensions() + { + $rc = new \ReflectionClass(VerifyInstall::class); + $rp = $rc->getProperty('requiredExtensions'); + $rp->setAccessible(true); + $rp->setValue($this->command, ['some-ext']); + + $this->command->__invoke(); + + $output = $this->output->fetch(); + $this->assertRegExp( + sprintf( + '/%s/', + preg_quote('[ERROR] The some-ext extension is missing - use your preferred package manager to install it') + ), + $output + ); + + $this->assertNotRegExp( + sprintf('/%s/', preg_quote('[OK] All required PHP extensions are installed.')), $output ); } diff --git a/test/ComposerInstallerFactoryTest.php b/test/ComposerInstallerFactoryTest.php deleted file mode 100644 index 50bbfb8..0000000 --- a/test/ComposerInstallerFactoryTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -class ComposerInstallerFactoryTest extends PHPUnit_Framework_TestCase -{ - public function testCreate() - { - $tmpDir = sprintf('%s/%s', sys_get_temp_dir(), $this->getName()); - @mkdir($tmpDir); - file_put_contents(sprintf('%s/composer.json', $tmpDir), json_encode(['name' => 'project'])); - - $factory = new ComposerInstallerFactory(new Factory, new NullIO); - - $this->assertInstanceOf(Installer::class, $factory->create($tmpDir)); - - unlink(sprintf('%s/composer.json', $tmpDir)); - rmdir($tmpDir); - } -} diff --git a/test/ComposerInstallerTest.php b/test/ComposerInstallerTest.php new file mode 100644 index 0000000..96e2b50 --- /dev/null +++ b/test/ComposerInstallerTest.php @@ -0,0 +1,117 @@ + + */ +class ComposerInstallerTest extends PHPUnit_Framework_TestCase +{ + /** + * @var string + */ + private $tempDir; + + /** + * @var Filesystem + */ + private $filesystem; + + public function setUp() + { + $this->filesystem = new Filesystem; + $this->tempDir = sprintf('%s/%s', realpath(sys_get_temp_dir()), $this->getName()); + @mkdir($this->tempDir, 0777, true); + } + + public function tearDown() + { + $this->filesystem->remove($this->tempDir); + } + + public function testComposerOutputIsWrittenIfInVerboseMode() + { + $input = new ArrayInput([]); + $output = new BufferedOutput(); + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + + $installer = new ComposerInstaller($input, $output, new Factory); + file_put_contents(sprintf('%s/composer.json', $this->tempDir), '{"name" : "learnyouphp", "require" : { "php": ">=5.6"}}'); + $res = $installer->install($this->tempDir); + + $this->assertFileExists(sprintf('%s/vendor', $this->tempDir)); + $this->assertFileExists(sprintf('%s/composer.lock', $this->tempDir)); + + $expectedOutput = "/Loading composer repositories with package information\n"; + $expectedOutput .= "Updating dependencies\n"; + $expectedOutput .= "Dependency resolution completed in \\d+\\.\\d+ seconds\n"; + $expectedOutput .= "Analyzed \\d+ packages to resolve dependencies\n"; + $expectedOutput .= "Analyzed \\d+ rules to resolve dependencies\n"; + $expectedOutput .= "Nothing to install or update\n"; + $expectedOutput .= "Writing lock file\n"; + $expectedOutput .= "Generating autoload files\n/"; + $this->assertRegExp($expectedOutput, $output->fetch()); + $this->assertRegExp($expectedOutput, strip_tags($res->getOutput())); + $this->assertEquals(0, $res->getExitCode()); + } + + public function testComposerOutputIsNotWrittenIfNotInVerboseMode() + { + $input = new ArrayInput([]); + $output = new BufferedOutput; + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + + $installer = new ComposerInstaller($input, $output, new Factory); + file_put_contents(sprintf('%s/composer.json', $this->tempDir), '{"name" : "learnyouphp", "require" : { "php": ">=5.6"}}'); + $res = $installer->install($this->tempDir); + + $this->assertFileExists(sprintf('%s/vendor', $this->tempDir)); + $this->assertFileExists(sprintf('%s/composer.lock', $this->tempDir)); + + $expectedOutput = "/Loading composer repositories with package information\n"; + $expectedOutput .= "Updating dependencies\n"; + $expectedOutput .= "Dependency resolution completed in \\d+\\.\\d+ seconds\n"; + $expectedOutput .= "Analyzed \\d+ packages to resolve dependencies\n"; + $expectedOutput .= "Analyzed \\d+ rules to resolve dependencies\n"; + $expectedOutput .= "Nothing to install or update\n"; + $expectedOutput .= "Writing lock file\n"; + $expectedOutput .= "Generating autoload files\n/"; + $this->assertEquals('', $output->fetch()); + $this->assertRegExp($expectedOutput, strip_tags($res->getOutput())); + $this->assertEquals(0, $res->getExitCode()); + } + + public function testExceptionIsThrownIfNoComposerJson() + { + $input = new ArrayInput([]); + $output = new BufferedOutput; + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + + $this->expectException(\InvalidArgumentException::class); + + $installer = new ComposerInstaller($input, $output, new Factory); + $installer->install($this->tempDir); + } + + public function testExceptionIsThrownIfInvalidComposerJson() + { + $input = new ArrayInput([]); + $output = new BufferedOutput; + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + + $this->expectException(ParsingException::class); + + $installer = new ComposerInstaller($input, $output, new Factory); + file_put_contents(sprintf('%s/composer.json', $this->tempDir), '{"name" : "learnyouphp"'); + $installer->install($this->tempDir); + } +} diff --git a/test/Exception/ComposerFailureExceptionTest.php b/test/Exception/ComposerFailureExceptionTest.php new file mode 100644 index 0000000..0e10cb9 --- /dev/null +++ b/test/Exception/ComposerFailureExceptionTest.php @@ -0,0 +1,30 @@ + + */ +class ComposerFailureExceptionTest extends PHPUnit_Framework_TestCase +{ + public function testFromException() + { + $e = new Exception('Some Error'); + + $composerException = ComposerFailureException::fromException($e); + $this->assertEquals('Some Error', $composerException->getMessage()); + } + + public function testFromMissingExtensions() + { + $message = 'This workshop requires some extra PHP extensions. Please install them'; + $message .= ' and try again. Required extensions are mbstring, zip.'; + + $composerException = ComposerFailureException::fromMissingExtensions(['mbstring', 'zip']); + $this->assertEquals($message, $composerException->getMessage()); + } +} diff --git a/test/IOFactoryTest.php b/test/IOFactoryTest.php deleted file mode 100644 index 40a8dc0..0000000 --- a/test/IOFactoryTest.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ -class IOFactoryTest extends PHPUnit_Framework_TestCase -{ - public function testGetIO() - { - $factory = new IOFactory; - $input = $this->createMock(InputInterface::class); - $output = $this->createMock(OutputInterface::class); - - $this->assertInstanceOf(ConsoleIO::class, $factory->getIO($input, $output)); - } - - public function testGetNullableIOReturnsNullIOIfNotInVerboseMode() - { - $factory = new IOFactory; - $input = $this->createMock(InputInterface::class); - $output = $this->createMock(OutputInterface::class); - - $output - ->expects($this->once()) - ->method('getVerbosity') - ->willReturn(OutputInterface::VERBOSITY_NORMAL); - - $this->assertInstanceOf(NullIO::class, $factory->getNullableIO($input, $output)); - } - - public function testGetNullableIOReturnsNormalIOIfInVerboseMode() - { - $factory = new IOFactory; - $input = $this->createMock(InputInterface::class); - $output = $this->createMock(OutputInterface::class); - - $output - ->expects($this->once()) - ->method('getVerbosity') - ->willReturn(OutputInterface::VERBOSITY_VERBOSE); - - $this->assertInstanceOf(ConsoleIO::class, $factory->getNullableIO($input, $output)); - } -} diff --git a/test/InstallResultTest.php b/test/InstallResultTest.php new file mode 100644 index 0000000..9580633 --- /dev/null +++ b/test/InstallResultTest.php @@ -0,0 +1,48 @@ + + */ +class InstallResultTest extends PHPUnit_Framework_TestCase +{ + public function testGetters() + { + $result = new InstallResult(0, ''); + + $this->assertEquals(0, $result->getExitCode()); + $this->assertEquals('', $result->getOutput()); + $this->assertFalse($result->missingExtensions()); + $this->assertEmpty($result->getMissingExtensions()); + } + + public function testMissingExtensionsAreParsed() + { + $output = "the requested PHP extension mbstring is missing from your system\n"; + $output .= "the requested PHP extension zip is missing from your system\n"; + + $result = new InstallResult(1, $output); + + $this->assertEquals(1, $result->getExitCode()); + $this->assertEquals($output, $result->getOutput()); + $this->assertTrue($result->missingExtensions()); + $this->assertSame(['mbstring', 'zip'], $result->getMissingExtensions()); + } + + public function testMissingExtensionsDupesAreRemoved() + { + $output = "the requested PHP extension mbstring is missing from your system\n"; + $output .= "the requested PHP extension mbstring is missing from your system\n"; + + $result = new InstallResult(1, $output); + + $this->assertEquals(1, $result->getExitCode()); + $this->assertEquals($output, $result->getOutput()); + $this->assertTrue($result->missingExtensions()); + $this->assertSame(['mbstring'], $result->getMissingExtensions()); + } +} diff --git a/test/Intstaller/InstallerTest.php b/test/Intstaller/InstallerTest.php index 1719d3b..2206bda 100644 --- a/test/Intstaller/InstallerTest.php +++ b/test/Intstaller/InstallerTest.php @@ -11,6 +11,7 @@ use Github\Api\Repository\Contents; use Github\Client; use Github\Exception\RuntimeException; +use PhpSchool\WorkshopManager\ComposerInstaller; use PhpSchool\WorkshopManager\ComposerInstallerFactory; use PhpSchool\WorkshopManager\Entity\InstalledWorkshop; use PhpSchool\WorkshopManager\Entity\Workshop; @@ -21,11 +22,14 @@ use PhpSchool\WorkshopManager\Exception\WorkshopNotFoundException; use PhpSchool\WorkshopManager\Filesystem; use PhpSchool\WorkshopManager\Installer\Installer; +use PhpSchool\WorkshopManager\InstallResult; use PhpSchool\WorkshopManager\Linker; use PhpSchool\WorkshopManager\Repository\InstalledWorkshopRepository; use PhpSchool\WorkshopManager\Repository\RemoteWorkshopRepository; use PhpSchool\WorkshopManager\VersionChecker; use PHPUnit_Framework_TestCase; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; /** * @author Aydin Hassan @@ -38,7 +42,7 @@ class InstallerTest extends PHPUnit_Framework_TestCase private $linker; private $filesystem; private $workshopHomeDir; - private $composerFactory; + private $composerInstaller; private $versionChecker; private $ghClient; private $installer; @@ -56,7 +60,7 @@ public function setup() $this->linker = $this->createMock(Linker::class); $this->filesystem = new Filesystem; $this->workshopHomeDir = sprintf('%s/%s', realpath(sys_get_temp_dir()), $this->getName()); - $this->composerFactory = new ComposerInstallerFactory(new Factory, new NullIO); + $this->composerInstaller = $this->createMock(ComposerInstaller::class); @mkdir($this->workshopHomeDir); $this->ghClient = $this->createMock(Client::class); $this->versionChecker = new VersionChecker($this->ghClient); @@ -66,7 +70,7 @@ public function setup() $this->linker, $this->filesystem, $this->workshopHomeDir, - $this->composerFactory, + $this->composerInstaller, $this->ghClient, $this->versionChecker, '/dev/null/%s/%s' @@ -222,15 +226,43 @@ public function testExceptionIsThrownIfCannotMoveWorkshopToInstallDir() public function testExceptionIsThrownIfCannotRunComposerInstall() { + $path = sprintf('%s/workshops/', $this->workshopHomeDir); + @mkdir($path); + $workshop = $this->configureRemoteRepository(); $this->configureTags($workshop); - $this->configureDownload($workshop, false); + $this->configureDownload($workshop); + $this->composerInstaller + ->expects($this->once()) + ->method('install') + ->with(sprintf('%slearn-you-php', $path)) + ->will($this->throwException(new \InvalidArgumentException('composer.json not found'))); + + $this->expectException(ComposerFailureException::class); + $this->expectExceptionMessage('composer.json not found'); + $this->installer->installWorkshop($workshop->getCode()); + } + + public function testExceptionIsThrownIfCannotRunComposerInstallBecauseMissingExtensions() + { $path = sprintf('%s/workshops/', $this->workshopHomeDir); @mkdir($path); + $workshop = $this->configureRemoteRepository(); + $this->configureTags($workshop); + $this->configureDownload($workshop); + $this->composerInstaller + ->expects($this->once()) + ->method('install') + ->with(sprintf('%slearn-you-php', $path)) + ->willReturn(new InstallResult(1, "the requested PHP extension mbstring is missing from your system\n")); + + $message = 'This workshop requires some extra PHP extensions. Please install them'; + $message .= ' and try again. Required extensions are mbstring.'; + $this->expectException(ComposerFailureException::class); - $this->expectExceptionMessageRegExp('/^Composer could not find the config.*/'); + $this->expectExceptionMessage($message); $this->installer->installWorkshop($workshop->getCode()); } @@ -244,6 +276,12 @@ public function testSuccessfulInstall() $path = sprintf('%s/workshops/', $this->workshopHomeDir); @mkdir($path); + $this->composerInstaller + ->expects($this->once()) + ->method('install') + ->with(sprintf('%slearn-you-php', $path)) + ->willReturn(new InstallResult(0, '')); + $this->localJsonFile ->expects($this->once()) ->method('write'); @@ -265,6 +303,12 @@ public function testWorkshopDirIsCreatedIfNotExists() $this->configureTags($workshop); $this->configureDownload($workshop); + $this->composerInstaller + ->expects($this->once()) + ->method('install') + ->with(sprintf('%s/workshops/learn-you-php', $this->workshopHomeDir)) + ->willReturn(new InstallResult(0, '')); + $this->localJsonFile ->expects($this->once()) ->method('write'); @@ -287,6 +331,12 @@ public function testWorkshopNameFolderIsRemovedIfExists() $this->configureDownload($workshop); mkdir(sprintf('%s/workshops/learn-you-php', $this->workshopHomeDir), 0775, true); + $this->composerInstaller + ->expects($this->once()) + ->method('install') + ->with(sprintf('%s/workshops/learn-you-php', $this->workshopHomeDir)) + ->willReturn(new InstallResult(0, '')); + $this->localJsonFile ->expects($this->once()) ->method('write'); @@ -310,6 +360,12 @@ public function testWorkshopTempDownloadIsRemovedIfExists() mkdir(sprintf('%s/.temp', $this->workshopHomeDir), 0775, true); touch(sprintf('%s/.temp/learn-you-php.zip', $this->workshopHomeDir)); + $this->composerInstaller + ->expects($this->once()) + ->method('install') + ->with(sprintf('%s/workshops/learn-you-php', $this->workshopHomeDir)) + ->willReturn(new InstallResult(0, '')); + $this->localJsonFile ->expects($this->once()) ->method('write'); @@ -371,7 +427,7 @@ private function configureTags(Workshop $workshop) ]); } - private function configureDownload(Workshop $workshop, $correctComposerJson = true) + private function configureDownload(Workshop $workshop) { $repo = $this->createMock(Repo::class); $contents = $this->createMock(Contents::class); @@ -391,10 +447,7 @@ private function configureDownload(Workshop $workshop, $correctComposerJson = tr $zipArchive->open(sprintf('%s/temp.zip', $this->workshopHomeDir), \ZipArchive::CREATE); $zipArchive->addEmptyDir('learnyouphp'); $zipArchive->addFromString('learnyouphp/file1.txt', 'data'); - - if ($correctComposerJson) { - $zipArchive->addFromString('learnyouphp/composer.json', '{"name" : "learnyouphp"}'); - } + $zipArchive->addFromString('learnyouphp/composer.json', '{"name" : "learnyouphp"}'); $zipArchive->close(); diff --git a/test/RecordingOutputTest.php b/test/RecordingOutputTest.php new file mode 100644 index 0000000..6e40ef5 --- /dev/null +++ b/test/RecordingOutputTest.php @@ -0,0 +1,107 @@ + + */ +class RecordingOutputTest extends PHPUnit_Framework_TestCase +{ + /** + * @var RecordingOutput + */ + private $output; + + /** + * @var BufferedOutput + */ + private $wrappedOutput; + + public function setup() + { + $this->wrappedOutput = new BufferedOutput; + $this->output = new RecordingOutput($this->wrappedOutput); + } + + public function testMethodsDelegateToWrapped() + { + $this->assertSame($this->wrappedOutput->isDebug(), $this->output->isDebug()); + $this->assertSame($this->wrappedOutput->isVerbose(), $this->output->isVerbose()); + $this->assertSame($this->wrappedOutput->isVeryVerbose(), $this->output->isVeryVerbose()); + $this->assertSame($this->wrappedOutput->getVerbosity(), $this->output->getVerbosity()); + $this->assertSame($this->wrappedOutput->isDecorated(), $this->output->isDecorated()); + $this->assertSame($this->wrappedOutput->getFormatter(), $this->output->getFormatter()); + $this->assertSame($this->wrappedOutput->isQuiet(), $this->output->isQuiet()); + } + + public function testSettersDelegateToWrapped() + { + $formatter = new OutputFormatter; + $this->assertNotSame($formatter, $this->wrappedOutput->getFormatter()); + $this->assertFalse($this->wrappedOutput->isDecorated()); + $this->assertEquals(OutputInterface::VERBOSITY_NORMAL, $this->wrappedOutput->getVerbosity()); + + $this->output->setFormatter($formatter); + $this->assertSame($formatter, $this->wrappedOutput->getFormatter()); + + $this->output->setDecorated(true); + $this->assertTrue($this->output->isDecorated()); + + $this->output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + $this->assertEquals(OutputInterface::VERBOSITY_VERBOSE, $this->wrappedOutput->getVerbosity()); + } + + public function testWriteWithRecordsAndDelegates() + { + $this->output->write('Hello', false); + + $this->assertEquals('Hello', $this->output->getOutput()); + $this->assertEquals('Hello', $this->wrappedOutput->fetch()); + } + + public function testWriteWithArrayAndRecordsAndDelegates() + { + $this->output->write(['Hello', 'Aydin'], false); + + $this->assertEquals('HelloAydin', $this->output->getOutput()); + $this->assertEquals('HelloAydin', $this->wrappedOutput->fetch()); + } + + public function testWriteWithNewLineRecordsAndDelegates() + { + $this->output->write('Hello', true); + + $this->assertEquals("Hello\n", $this->output->getOutput()); + $this->assertEquals("Hello\n", $this->wrappedOutput->fetch()); + } + + public function testWriteWithArrayAndNewLineRecordsAndDelegates() + { + $this->output->write(['Hello', 'Aydin'], true); + + $this->assertEquals("Hello\nAydin\n", $this->output->getOutput()); + $this->assertEquals("Hello\nAydin\n", $this->wrappedOutput->fetch()); + } + + public function testWriteLineRecordsAndDelegates() + { + $this->output->writeln('Hello'); + + $this->assertEquals("Hello\n", $this->output->getOutput()); + $this->assertEquals("Hello\n", $this->wrappedOutput->fetch()); + } + + public function testWriteLineWithArrayRecordsAndDelegates() + { + $this->output->writeln(['Hello', 'Aydin']); + + $this->assertEquals("Hello\nAydin\n", $this->output->getOutput()); + $this->assertEquals("Hello\nAydin\n", $this->wrappedOutput->fetch()); + } +}