diff --git a/.gitignore b/.gitignore index 93c7248..da2668a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ report *.sublime-project *.sublime-workspace testbench/ +skeleton-cache/ diff --git a/config/packager.php b/config/packager.php index f230c47..e078ec7 100644 --- a/config/packager.php +++ b/config/packager.php @@ -7,6 +7,7 @@ * Default: http://github.com/Jeroen-G/packager-skeleton/archive/master.zip */ 'skeleton' => 'http://github.com/Jeroen-G/packager-skeleton/archive/master.zip', + 'cache_skeleton' => false, /* * You can set defaults for the following placeholders. diff --git a/readme.md b/readme.md index d317f0c..8667661 100644 --- a/readme.md +++ b/readme.md @@ -66,13 +66,14 @@ $ php artisan packager:git https://github.com/author/repository **Result:** This will register the package in the app's `composer.json` file. -If the `packager:git` command is used, the entire Git repository is cloned. If `packager:get` is used, the package will be downloaded, without a repository. This also works with Bitbucket repositories, but you have to provide the flag `--host=bitbucket` for the `packager:get` command. +If the `packager:git` command is used, the entire Git repository is cloned (you can optionally specify the branch/version to clone using the `--branch` option). If `packager:get` is used, the package will be downloaded, without a repository. This also works with Bitbucket repositories, but you have to provide the flag `--host=bitbucket` for the `packager:get` command. **Options:** ```bash $ php artisan packager:get https://github.com/author/repository --branch=develop $ php artisan packager:get https://github.com/author/repository MyVendor MyPackage $ php artisan packager:git https://github.com/author/repository MyVendor MyPackage +$ php artisan packager:git github-user/github-repo --branch=dev-mybranch ``` It is possible to specify a branch with the `--branch` option. If you specify a vendor and name directly after the url, those will be used instead of the pieces of the url. @@ -142,6 +143,14 @@ You first need to run $ composer require sensiolabs/security-checker ``` +## Managing dependencies +When you install a new package using `packager:new`, `packager:get` or `packager:git`, the package dependencies will automatically be installed into the parent project's `vendor/` folder. + +Installing or updating package dependencies should *not* be done directly from the `packages/` folder. + +When you've edited the `composer.json` file in your package folder, you should run `composer update` from the root folder of the parent project. + +If your package was installed using the `packager:git` command, any changes you make to the package's `composer.json` file will not be detected by the parent project until the changes have been committed. ## Issues with cURL SSL certificate It turns out that, especially on Windows, there might arise some problems with the downloading of the skeleton, due to a file regarding SSL certificates missing on the OS. This can be solved by opening up your .env file and putting this in it: diff --git a/src/Commands/CheckPackage.php b/src/Commands/CheckPackage.php index 08c1b50..9c80953 100644 --- a/src/Commands/CheckPackage.php +++ b/src/Commands/CheckPackage.php @@ -3,8 +3,10 @@ namespace JeroenG\Packager\Commands; use Illuminate\Console\Command; +use JeroenG\Packager\ComposerHandler; use SensioLabs\Security\SecurityChecker; use SensioLabs\Security\Formatters\SimpleFormatter; +use Symfony\Component\Finder\Exception\DirectoryNotFoundException; /** * List all locally installed packages. @@ -13,6 +15,7 @@ **/ class CheckPackage extends Command { + use ComposerHandler; /** * The name and signature of the console command. * @var string @@ -34,7 +37,14 @@ public function handle() { $this->info('Using the SensioLabs Security Checker the composer.lock of the package is scanned for known security vulnerabilities in the dependencies.'); $this->info('Make sure you have a composer.lock file first (for example by running "composer install" in the folder'); - + try { + $this->findInstalledPath('sensiolabs/security-checker'); + } catch (DirectoryNotFoundException $e) { + $this->warn('SensioLabs Security Checker is not installed.'); + $this->info('Run the following command and try again:'); + $this->getOutput()->writeln('composer require sensiolabs/security-checker'); + return 1; + } $checker = new SecurityChecker(); $formatter = new SimpleFormatter($this->getHelperSet()->get('formatter')); $vendor = $this->argument('vendor'); diff --git a/src/Commands/EnablePackage.php b/src/Commands/EnablePackage.php index e7e8fbc..22292a3 100644 --- a/src/Commands/EnablePackage.php +++ b/src/Commands/EnablePackage.php @@ -74,7 +74,7 @@ public function handle() // Install the package $this->info('Installing package...'); - $this->conveyor->installPackage(); + $this->conveyor->installPackageFromPath(); $this->makeProgress(); // Finished removing the package, end of the progress bar diff --git a/src/Commands/GetPackage.php b/src/Commands/GetPackage.php index 14fed9b..d32feae 100644 --- a/src/Commands/GetPackage.php +++ b/src/Commands/GetPackage.php @@ -65,7 +65,7 @@ public function __construct(Conveyor $conveyor, Wrapping $wrapping) public function handle() { // Start the progress bar - $this->startProgressBar(4); + $this->startProgressBar(5); // Common variables if ($this->option('host') == 'bitbucket') { @@ -109,7 +109,7 @@ public function handle() // Install the package $this->info('Installing package...'); - $this->conveyor->installPackage(); + $this->conveyor->installPackageFromPath(); $this->makeProgress(); // Finished creating the package, end of the progress bar diff --git a/src/Commands/GitPackage.php b/src/Commands/GitPackage.php index 24d8bb6..b4f8b80 100644 --- a/src/Commands/GitPackage.php +++ b/src/Commands/GitPackage.php @@ -2,11 +2,11 @@ namespace JeroenG\Packager\Commands; +use Illuminate\Console\Command; use Illuminate\Support\Str; use JeroenG\Packager\Conveyor; -use JeroenG\Packager\Wrapping; -use Illuminate\Console\Command; use JeroenG\Packager\ProgressBar; +use JeroenG\Packager\Wrapping; /** * Get an existing package from a remote git repository with its VCS. @@ -24,7 +24,8 @@ class GitPackage extends Command protected $signature = 'packager:git {url : The url of the git repository} {vendor? : The vendor part of the namespace} - {name? : The name of package for the namespace}'; + {name? : The name of package for the namespace} + {--constraint=dev-master : The version to install}'; /** * The console command description. @@ -65,49 +66,35 @@ public function handle() { // Start the progress bar $this->startProgressBar(4); - // Common variables $source = $this->argument('url'); - $origin = rtrim(strtolower($source), '/'); - - if (is_null($this->argument('vendor')) || is_null($this->argument('name'))) { + $origin = strtolower(rtrim($source, '/')); + // If only "user/repository" is provided as origin, assume a https Github repository + if (preg_match('/^[\w-]+\/[\w-]+$/', $origin)) { + $origin = 'https://github.com/'.$origin; + } + if ($this->argument('vendor') === null || $this->argument('name') === null) { $this->setGitVendorAndPackage($origin); } else { $this->conveyor->vendor($this->argument('vendor')); $this->conveyor->package($this->argument('name')); } - // Start creating the package $this->info('Creating package '.$this->conveyor->vendor().'\\'.$this->conveyor->package().'...'); $this->conveyor->checkIfPackageExists(); $this->makeProgress(); - + // Install package from VCS + $this->info('Installing package from VCS...'); + $this->conveyor->installPackageFromVcs($origin, $this->option('constraint')); + $this->makeProgress(); // Create the package directory $this->info('Creating packages directory...'); $this->conveyor->makeDir($this->conveyor->packagesPath()); - - // Clone the repository - $this->info('Cloning repository...'); - exec("git clone $source ".$this->conveyor->packagePath(), $output, $exit_code); - - if ($exit_code != 0) { - $this->error('Unable to clone repository'); - $this->warn('Please check credentials and try again'); - - return; - } - - $this->makeProgress(); - - // Create the vendor directory - $this->info('Creating vendor...'); $this->conveyor->makeDir($this->conveyor->vendorPath()); $this->makeProgress(); - - $this->info('Installing package...'); - $this->conveyor->installPackage(); + $this->info('Symlinking package to '.$this->conveyor->packagePath()); + $this->conveyor->symlinkInstalledPackage(); $this->makeProgress(); - // Finished creating the package, end of the progress bar $this->finishProgress('Package cloned successfully!'); } @@ -115,7 +102,6 @@ public function handle() protected function setGitVendorAndPackage($origin) { $pieces = explode('/', $origin); - if (Str::contains($origin, 'https')) { $vendor = $pieces[3]; $package = $pieces[4]; @@ -123,7 +109,6 @@ protected function setGitVendorAndPackage($origin) $vendor = explode(':', $pieces[0])[1]; $package = rtrim($pieces[1], '.git'); } - $this->conveyor->vendor($vendor); $this->conveyor->package($package); } diff --git a/src/Commands/ListPackages.php b/src/Commands/ListPackages.php index f034171..44d4334 100644 --- a/src/Commands/ListPackages.php +++ b/src/Commands/ListPackages.php @@ -3,6 +3,8 @@ namespace JeroenG\Packager\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\File; +use Symfony\Component\Process\Process; /** * List all locally installed packages. @@ -35,14 +37,45 @@ public function handle() $repositories = $composer['repositories'] ?? []; $packages = []; foreach ($repositories as $name => $info) { - $path = $info['url']; - $pattern = '{'.addslashes($packages_path).'(.*)$}'; - if (preg_match($pattern, $path, $match)) { - $packages[] = explode(DIRECTORY_SEPARATOR, $match[1]); + if ($info['type'] === 'path') { + $path = $info['url']; + $pattern = '{'.addslashes($packages_path).'(.*)$}'; + if (preg_match($pattern, $path, $match)) { + $status = $this->getGitStatus($path); + $packages[] = [$name, 'packages/'.$match[1], $status]; + } + } elseif ($info['type'] === 'vcs') { + $path = $packages_path.$name; + if (file_exists($path)) { + $pattern = '{'.addslashes($packages_path).'(.*)$}'; + if (preg_match($pattern, $path, $match)) { + $status = $this->getGitStatus($path); + $packages[] = [$name, 'packages/'.$match[1], $status]; + } + } } } - - $headers = ['Package', 'Path']; + $headers = ['Package', 'Path', 'Git status']; $this->table($headers, $packages); } + + protected function getGitStatus($path) + { + if (!File::exists($path.'/.git')) { + return 'Not initialized'; + } + $status = 'Up to date'; + (new Process(['git', 'fetch', '--all'], $path))->run(); + (new Process(['git', '--git-dir='.$path.'/.git', '--work-tree='.$path, 'status', '-sb'], $path))->run(function ( + $type, + $buffer + ) use (&$status) { + if (preg_match('/^##/', $buffer)) { + if (preg_match('/\[(.*)\]$/', $buffer, $match)) { + $status = ''.ucfirst($match[1]).''; + } + } + }); + return $status; + } } diff --git a/src/Commands/NewPackage.php b/src/Commands/NewPackage.php index d416ad4..a2188ea 100644 --- a/src/Commands/NewPackage.php +++ b/src/Commands/NewPackage.php @@ -135,7 +135,7 @@ public function handle() // Add path repository to composer.json and install package $this->info('Installing package...'); - $this->conveyor->installPackage(); + $this->conveyor->installPackageFromPath(); $this->makeProgress(); diff --git a/src/ComposerHandler.php b/src/ComposerHandler.php new file mode 100644 index 0000000..3c4e5c7 --- /dev/null +++ b/src/ComposerHandler.php @@ -0,0 +1,120 @@ +modifyComposerJson(function (array $composer) use ($name){ + unset($composer['repositories'][$name]); + return $composer; + }, base_path()); + } + + /** + * Determines the path to Composer executable + * @return string + */ + public function getComposerExecutable(): string + { + return trim(shell_exec('which composer')) ?: 'composer'; + } + + protected function removePackage(string $packageName): array + { + return $this->runComposerCommand([ + 'remove', + strtolower($packageName), + '--no-progress', + ]); + } + + protected function requirePackage(string $packageName, string $version = null, bool $prefer_source = true): bool + { + $package = strtolower($packageName); + if ($version) { + $package .= ':'.$version; + } + $result = $this->runComposerCommand([ + 'require', + $package, + '--prefer-'.($prefer_source ? 'source' : 'dist'), + '--prefer-stable', + '--no-suggest', + '--no-progress', + ]); + if (!$result['success']) { + return false; + } + return true; + } + + protected function addComposerRepository(string $name, string $type = 'path', string $url = null) + { + $params = [ + 'type' => $type, + 'url' => $url + ]; + return $this->modifyComposerJson(function (array $composer) use ($params, $name){ + $composer['repositories'][$name] = $params; + return $composer; + }, base_path()); + } + + /** + * Find the package's path in Composer's vendor folder + * @param string $packageName + * @return string + * @throws RuntimeException + */ + public function findInstalledPath(string $packageName): string + { + $packageName = strtolower($packageName); + $result = $this->runComposerCommand([ + 'info', + $packageName, + '--path']); + if ($result['success'] &&preg_match('{'.$packageName.' (.*)$}m', $result['output'], $match)) { + return trim($match[1]); + } + throw new DirectoryNotFoundException('Package ' . $packageName.' not found in vendor folder'); + } + + /** + * @param string $path + * @return array + */ + public function getComposerJsonArray(string $path): array + { + return json_decode(file_get_contents($path), true); + } + + public function modifyComposerJson(Closure $callback, string $composer_path) + { + $composer_path = rtrim($composer_path, '/'); + if (!preg_match('/composer\.json$/', $composer_path)){ + $composer_path .= '/composer.json'; + } + $original = $this->getComposerJsonArray($composer_path); + $modified = $callback($original); + return file_put_contents($composer_path, json_encode($modified, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); + } + + /** + * @param array $command + * @param string|null $cwd + * @return array + */ + public function runComposerCommand(array $command, string $cwd = null): array + { + array_unshift($command, 'php', '-n', $this->getComposerExecutable()); + return $this->runProcess($command, $cwd); + } +} diff --git a/src/Conveyor.php b/src/Conveyor.php index c53072a..9848403 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -3,11 +3,10 @@ namespace JeroenG\Packager; use RuntimeException; -use Illuminate\Support\Str; class Conveyor { - use FileHandler; + use FileHandler, ComposerHandler; /** * Package vendor namespace. @@ -57,111 +56,88 @@ public function package($package = null) return $this->package; } + public static function fetchSkeleton(string $source, string $destination): void + { + $zipFilePath = tempnam(getcwd(), 'package'); + (new self())->download($zipFilePath, $source) + ->extract($zipFilePath, $destination) + ->cleanUp($zipFilePath); + } + /** * Download the skeleton package. * * @return void */ - public function downloadSkeleton() + public function downloadSkeleton(): void { - $this->download($zipFile = $this->makeFilename(), config('packager.skeleton')) - ->extract($zipFile, $this->vendorPath()) - ->cleanUp($zipFile); - rename($this->vendorPath().'/packager-skeleton-master', $this->packagePath()); + $useCached = config('packager.cache_skeleton'); + $cachePath = $this->getSkeletonCachePath(); + $cacheExists = $this->pathExists($cachePath); + if ($useCached && $cacheExists) { + $this->copyDir($cachePath, $this->vendorPath()); + } else { + $this->fetchSkeleton(config('packager.skeleton'), $this->vendorPath()); + } + $temporaryPath = $this->vendorPath().'/packager-skeleton-master'; + if ($useCached && ! $cacheExists) { + $this->copyDir($temporaryPath, $cachePath); + } + $this->rename($temporaryPath, $this->packagePath()); } /** * Download the package from Github. * * @param string $origin The Github URL + * @param $piece * @param string $branch The branch to download * @return void */ - public function downloadFromGithub($origin, $piece, $branch) + public function downloadFromGithub($origin, $piece, $branch): void { $this->download($zipFile = $this->makeFilename(), $origin) ->extract($zipFile, $this->vendorPath()) ->cleanUp($zipFile); - rename($this->vendorPath().'/'.$piece.'-'.$branch, $this->packagePath()); + $this->rename($this->vendorPath().'/'.$piece.'-'.$branch, $this->packagePath()); } - /** - * Dump Composer's autoloads. - * - * @return void - */ - public function dumpAutoloads() + public function getPackageName(): string { - shell_exec('composer dump-autoload'); + return $this->vendor.'/'.$this->package; } - public function installPackage() + public function installPackageFromPath(): void { - $this->addPathRepository(); - $this->requirePackage(); + $this->addComposerRepository($this->getPackageName(), 'path', $this->packagePath()); + $this->requirePackage($this->getPackageName(), null, false); } - public function uninstallPackage() + public function installPackageFromVcs($url, $version): void { - $this->removePackage(); - $this->removePathRepository(); - } - - public function addPathRepository() - { - $params = json_encode([ - 'type' => 'path', - 'url' => $this->packagePath(), - ]); - $command = [ - 'composer', - 'config', - 'repositories.'.Str::slug($this->vendor.'-'.$this->package), - $params, - '--file', - 'composer.json', - ]; - - return $this->runProcess($command); - } - - public function removePathRepository() - { - return $this->runProcess([ - 'composer', - 'config', - '--unset', - 'repositories.'.Str::slug($this->vendor.'-', $this->package), - ]); + $this->addComposerRepository($this->getPackageName(), 'vcs', $url); + $success = $this->requirePackage($this->getPackageName(), $version); + if (!$success) { + $this->removeComposerRepository($this->getPackageName()); + $message = 'No package named '.$this->getPackageName().' with version '.$version.' was found in '.$url; + throw new RuntimeException($message); + } } - public function requirePackage() + public function symlinkInstalledPackage(): bool { - return $this->runProcess([ - 'composer', - 'require', - $this->vendor.'/'.$this->package, - ]); + $sourcePath = $this->findInstalledPath($this->getPackageName()); + return $this->createSymlink($sourcePath, $this->packagePath()); } - public function removePackage() + public function uninstallPackage(): void { - return $this->runProcess([ - 'composer', - 'remove', - $this->vendor.'/'.$this->package, - ]); + $this->removePackage($this->getPackageName()); + $this->removeComposerRepository($this->getPackageName()); } - /** - * @param array $command - * @return bool - */ - protected function runProcess(array $command) + public static function getSkeletonCachePath(): string { - $process = new \Symfony\Component\Process\Process($command, base_path()); - $process->run(); - - return $process->getExitCode() === 0; + return __DIR__.'/../skeleton-cache'; } } diff --git a/src/FileHandler.php b/src/FileHandler.php index 364048e..44622b3 100644 --- a/src/FileHandler.php +++ b/src/FileHandler.php @@ -2,6 +2,7 @@ namespace JeroenG\Packager; +use Illuminate\Filesystem\Filesystem; use ZipArchive; use RuntimeException; use GuzzleHttp\Client; @@ -75,6 +76,11 @@ public function makeDir($path) return false; } + public function copyDir($source, $destination) + { + return (new Filesystem())->copyDirectory($source, $destination); + } + /** * Remove a directory if it exists. * @@ -83,7 +89,11 @@ public function makeDir($path) */ public function removeDir($path) { - if ($path == 'packages' || $path == '/') { + if (is_link($path)) { + unlink($path); + return true; + } + if ($path === 'packages' || $path === '/'){ return false; } @@ -183,4 +193,28 @@ public function cleanUpRules() } } } + + protected function createSymlink(string $from, string $to) + { + return symlink($from, $to); + } + + /** + * @param $path + * @return bool + */ + protected function pathExists($path): bool + { + return (new Filesystem())->exists($path); + } + + /** + * @param $path + * @param $to + * @return bool + */ + protected function rename($path, $to): bool + { + return (new Filesystem())->move($path, $to); + } } diff --git a/src/ProcessRunner.php b/src/ProcessRunner.php new file mode 100644 index 0000000..5a2c19a --- /dev/null +++ b/src/ProcessRunner.php @@ -0,0 +1,27 @@ +setTimeout(null); + $output = ''; + $process->run(static function ($type, $buffer) use (&$output) { + $output .= $buffer; + }); + $success = $process->getExitCode() === 0; + return compact('success', 'output'); + } +} diff --git a/tests/IntegratedTest.php b/tests/IntegratedTest.php index 45fd514..380fe63 100644 --- a/tests/IntegratedTest.php +++ b/tests/IntegratedTest.php @@ -3,38 +3,81 @@ namespace JeroenG\Packager\Tests; use Illuminate\Support\Facades\Artisan; +use Symfony\Component\Process\Process; class IntegratedTest extends TestCase { public function test_new_package_is_created() { + // Also test downloading package-skeleton (the other test will use a cached copy) + config()->set('packager.cache_skeleton', false); Artisan::call('packager:new', ['vendor' => 'MyVendor', 'name' => 'MyPackage']); - $this->seeInConsoleOutput('Package created successfully!'); + $this->assertComposerPackageInstalled('MyVendor/MyPackage'); + // Save the generated package for use in later tests + $this->storePackageAsFake(); } public function test_get_existing_package() { - Artisan::call('packager:get', - ['url' => 'https://github.com/Jeroen-G/packager-skeleton', 'vendor' => 'MyVendor', 'name' => 'MyPackage']); - + $this->assertComposerPackageNotInstalled('MyVendor/MyPackage'); + Artisan::call('packager:get', [ + 'url' => 'https://github.com/Jeroen-G/packager-skeleton', + 'vendor' => 'MyVendor', + 'name' => 'MyPackage' + ]); $this->seeInConsoleOutput('Package downloaded successfully!'); + $this->assertComposerPackageInstalled('MyVendor/MyPackage'); } + /** + * @depends test_new_package_is_created + */ public function test_list_packages() { - Artisan::call('packager:new', ['vendor' => 'MyVendor', 'name' => 'MyPackage']); + $this->installFakePackageFromPath(); Artisan::call('packager:list'); - - $this->seeInConsoleOutput('MyVendor'); + $this->seeInConsoleOutput(['MyVendor', 'MyPackage', 'Not initialized']); } + /** + * @depends test_new_package_is_created + */ public function test_removing_package() { - Artisan::call('packager:new', ['vendor' => 'MyVendor', 'name' => 'MyPackage']); - $this->seeInConsoleOutput('MyVendor'); - + $this->installFakePackageFromPath(); Artisan::call('packager:remove', ['vendor' => 'MyVendor', 'name' => 'MyPackage', '--no-interaction' => true]); $this->seeInConsoleOutput('Package removed successfully!'); + $this->assertComposerPackageNotInstalled('MyVendor/MyPackage'); + } + + public function test_adding_git_package() + { + Artisan::call('packager:git', [ + 'url' => 'https://github.com/Jeroen-G/packager-skeleton', + 'vendor' => 'MyVendor', + 'name' => 'MyPackage' + ]); + $this->seeInConsoleOutput('Package cloned successfully!'); + $this->assertComposerPackageInstalled('MyVendor/MyPackage'); + Artisan::call('packager:list'); + $this->seeInConsoleOutput(['MyVendor', 'MyPackage', 'Up to date']); + $package_path = base_path('packages/MyVendor/MyPackage'); + (new Process(['touch', 'new-file.txt'], $package_path))->run(); + (new Process(['git', 'add', '.'], $package_path))->run(); + (new Process(['git', 'commit', '-m', 'New commit'], $package_path))->run(); + Artisan::call('packager:list'); + $this->seeInConsoleOutput(['MyVendor', 'MyPackage', 'Ahead 1']); + (new Process(['git', 'reset', '--hard', 'HEAD~2'], $package_path))->run(); + Artisan::call('packager:list'); + $this->seeInConsoleOutput(['MyVendor', 'MyPackage', 'Behind 1']); + } + + public function test_warning_shown_when_security_checker_not_installed() + { + $this->installFakePackageFromPath(); + Artisan::call('packager:check', ['vendor' => 'MyPackage', 'name' => 'MyPackage']); + $this->seeInConsoleOutput('SensioLabs Security Checker is not installed.'); + // It's possible to install security-checker in the testbench, but it's currently not possible to use it. } } diff --git a/tests/RefreshTestbench.php b/tests/RefreshTestbench.php new file mode 100644 index 0000000..eed8982 --- /dev/null +++ b/tests/RefreshTestbench.php @@ -0,0 +1,116 @@ +fetchSkeleton( + 'http://github.com/Jeroen-G/packager-skeleton/archive/master.zip', + $instance->getSkeletonCachePath() + ); + $instance->makeDir(self::getTestbenchTemplatePath()); + $original = __DIR__.'/../vendor/orchestra/testbench-core/laravel/'; + $instance->copyDir($original, self::getTestbenchTemplatePath()); + $instance->modifyComposerJson(function (array $composer) { + // Remove "tests/TestCase.php" from autoload (it doesn't exist) + unset($composer['autoload']['classmap'][1]); + // Pre-install dependencies + $composer['require'] = ['illuminate/support' => '~5']; + $composer['minimum-stability'] = 'stable'; + // Enable optimized autoloader, allowing to test if package classes are properly installed + $composer['config'] = [ + 'optimize-autoloader' => true, + 'preferred-install' => 'dist', + ]; + return $composer; + }, self::getTestbenchTemplatePath()); + fwrite(STDOUT, "Installing test environment dependencies\n"); + $instance->runComposerCommand([ + 'install', + '--prefer-dist', + '--no-progress' + ], self::getTestbenchTemplatePath()); + fwrite(STDOUT, "Test environment installed\n"); + } catch (Exception $e) { + if (isset($instance)){ + $instance->removeDir(self::getTestbenchTemplatePath()); + } + } + } + + protected function installTestApp(): void + { + if ($this->pathExists(self::getTestbenchWorkingCopyPath())){ + $this->uninstallTestApp(); + } + $this->copyDir(self::getTestbenchTemplatePath(), self::getTestbenchWorkingCopyPath()); + } + + protected function uninstallTestApp(): void + { + $this->removeDir(self::getTestbenchWorkingCopyPath()); + } + + public static function setUpBeforeClass(): void + { + if (!file_exists(self::getTestbenchTemplatePath())) { + self::setUpLocalTestbench(); + } + parent::setUpBeforeClass(); + } + + protected function getBasePath(): string + { + return self::getTestbenchWorkingCopyPath(); + } + + /** + * Setup before each test. + */ + public function setUp(): void + { + $this->installTestApp(); + parent::setUp(); + config()->set('packager.cache_skeleton', true); + } + + /** + * Tear down after each test. + */ + public function tearDown(): void + { + $this->uninstallTestApp(); + parent::tearDown(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index a940d31..8a4147b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,55 +2,125 @@ namespace JeroenG\Packager\Tests; +use Illuminate\Contracts\Console\Kernel; +use JeroenG\Packager\ComposerHandler; +use JeroenG\Packager\FileHandler; use Orchestra\Testbench\TestCase as TestBench; +use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Component\Finder\Exception\DirectoryNotFoundException; +use Symfony\Component\Finder\Finder; abstract class TestCase extends TestBench { - use TestHelper; + use RefreshTestbench; - protected const TEST_APP_TEMPLATE = __DIR__.'/../testbench/template'; - protected const TEST_APP = __DIR__.'/../testbench/laravel'; - - public static function setUpBeforeClass():void + /** + * Tell Testbench to use this package. + * + * @param $app + * + * @return array + */ + protected function getPackageProviders($app) { - if (! file_exists(self::TEST_APP_TEMPLATE)) { - self::setUpLocalTestbench(); - } - parent::setUpBeforeClass(); + return ['JeroenG\Packager\PackagerServiceProvider']; } - protected function getBasePath() + /** + * @param $expectedText + * @throws ExpectationFailedException + */ + protected function seeInConsoleOutput($expectedText): void { - return self::TEST_APP; + if (!is_array($expectedText)){ + $expectedText = [$expectedText]; + } + $consoleOutput = $this->app[Kernel::class]->output(); + foreach ($expectedText as $string) { + $this->assertStringContainsString($string, $consoleOutput, + "Did not see `{$string}` in console output: `$consoleOutput`"); + } } /** - * Setup before each test. + * @param $unExpectedText + * @throws ExpectationFailedException */ - public function setUp(): void + protected function doNotSeeInConsoleOutput($unExpectedText): void { - $this->installTestApp(); - parent::setUp(); + $consoleOutput = $this->app[Kernel::class]->output(); + $this->assertStringNotContainsString($unExpectedText, $consoleOutput, + "Did not expect to see `{$unExpectedText}` in console output: `$consoleOutput`"); } /** - * Tear down after each test. + * @param string $package + * @throws ExpectationFailedException */ - public function tearDown(): void + protected function assertComposerPackageInstalled(string $package): void { - $this->uninstallTestApp(); - parent::tearDown(); + $composer = $this->getComposerJsonArray(base_path('composer.json')); + $this->assertArrayHasKey(strtolower($package), $composer['require']); + $path = $this->findInstalledPath($package); + $this->assertDirectoryIsReadable($path); + [$vendor, $package] = explode('/', $package); + $fullyQualifiedServiceProvider = sprintf("%s\\%s\\%sServiceProvider", $vendor, $package, $package); + $mentions = Finder::create() + ->in(base_path('vendor/composer')) + ->contains(addslashes($fullyQualifiedServiceProvider)) + ->count(); + // Should be mentioned in 3 different files + $this->assertGreaterThanOrEqual(3, $mentions); } /** - * Tell Testbench to use this package. - * - * @param $app - * - * @return array + * @param string $package + * @throws ExpectationFailedException */ - protected function getPackageProviders($app) + protected function assertComposerPackageNotInstalled(string $package): void { - return ['JeroenG\Packager\PackagerServiceProvider']; + $composer = $this->getComposerJsonArray(base_path('composer.json')); + $this->assertArrayNotHasKey(strtolower($package), $composer['require']); + $this->expectException(DirectoryNotFoundException::class); + $this->findInstalledPath($package); + [$vendor, $package] = explode('/', $package); + $fullyQualifiedServiceProvider = sprintf("%s\\%s\\%sServiceProvider", $vendor, $package, $package); + $mentions = Finder::create() + ->in(base_path('vendor/composer')) + ->contains(addslashes($fullyQualifiedServiceProvider)) + ->count(); + // Should not be mentioned anywhere + $this->assertEquals(0, $mentions); + } + + protected function storePackageAsFake() + { + $fakePackagePath = self::getLocalTestbenchPath().'/fake-package'; + $fakeComposerMetadataPath = self::getLocalTestbenchPath().'/fake-composer'; + if (!$this->pathExists($fakePackagePath) || !$this->pathExists($fakeComposerMetadataPath)){ + $this->copyDir($this->findInstalledPath('MyVendor/MyPackage'), $fakePackagePath.'/MyVendor/MyPackage'); + $this->copyDir(base_path('vendor/composer'), $fakeComposerMetadataPath); + } + } + + protected function installFakePackageFromPath() + { + $fakePath = self::getLocalTestbenchPath().'/fake-package'; + $fakeComposerMetadataPath = self::getLocalTestbenchPath().'/fake-composer'; + $destination = base_path('packages'); + $this->copyDir($fakePath, $destination); + $this->copyDir($fakeComposerMetadataPath, base_path('vendor/composer')); + $packagePath = base_path('packages/MyVendor/MyPackage'); + $this->makeDir(base_path('vendor/myvendor')); + $this->createSymlink($packagePath, base_path('vendor/myvendor/mypackage')); + $this->modifyComposerJson(function (array $composer) use ($packagePath){ + $composer['repositories']['MyVendor/MyPackage'] = [ + 'type' => 'path', + 'url' => $packagePath + ]; + $composer['require']['myvendor/mypackage'] = 'v1.0'; + return $composer; + }, base_path()); + $this->assertComposerPackageInstalled('MyVendor/MyPackage'); } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php deleted file mode 100644 index 18b9658..0000000 --- a/tests/TestHelper.php +++ /dev/null @@ -1,66 +0,0 @@ -app[Kernel::class]->output(); - $this->assertStringContainsString($expectedText, $consoleOutput, - "Did not see `{$expectedText}` in console output: `$consoleOutput`"); - } - - protected function doNotSeeInConsoleOutput($unExpectedText) - { - $consoleOutput = $this->app[Kernel::class]->output(); - $this->assertStringNotContainsString($unExpectedText, $consoleOutput, - "Did not expect to see `{$unExpectedText}` in console output: `$consoleOutput`"); - } - - /** - * Create a modified copy of testbench to be used as a template. - * Before each test, a fresh copy of the template is created. - */ - private static function setUpLocalTestbench() - { - fwrite(STDOUT, "Setting up test environment for first use.\n"); - $files = new Filesystem(); - $files->makeDirectory(self::TEST_APP_TEMPLATE, 0755, true); - $original = __DIR__.'/../vendor/orchestra/testbench-core/laravel/'; - $files->copyDirectory($original, self::TEST_APP_TEMPLATE); - // Modify the composer.json file - $composer = json_decode($files->get(self::TEST_APP_TEMPLATE.'/composer.json'), true); - // Remove "tests/TestCase.php" from autoload (it doesn't exist) - unset($composer['autoload']['classmap'][1]); - // Pre-install illuminate/support - $composer['require'] = ['illuminate/support' => '~5']; - // Install stable version - $composer['minimum-stability'] = 'stable'; - $files->put(self::TEST_APP_TEMPLATE.'/composer.json', json_encode($composer, JSON_PRETTY_PRINT)); - // Install dependencies - fwrite(STDOUT, "Installing test environment dependencies\n"); - (new Process(['composer', 'install', '--no-dev'], self::TEST_APP_TEMPLATE))->run(function ($type, $buffer) { - fwrite(STDOUT, $buffer); - }); - } - - protected function installTestApp() - { - $this->uninstallTestApp(); - $files = new Filesystem(); - $files->copyDirectory(self::TEST_APP_TEMPLATE, self::TEST_APP); - } - - protected function uninstallTestApp() - { - $files = new Filesystem(); - if ($files->exists(self::TEST_APP)) { - $files->deleteDirectory(self::TEST_APP); - } - } -}