From 2665e3c8e91ab883942ddb8436fbc19e9602bae5 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sun, 9 Jun 2019 17:44:44 +0200 Subject: [PATCH 01/11] Install git packages through composer --- src/Commands/EnablePackage.php | 2 +- src/Commands/GetPackage.php | 2 +- src/Commands/GitPackage.php | 31 +++++-------- src/Commands/ListPackages.php | 25 ++++++++--- src/Commands/NewPackage.php | 2 +- src/Conveyor.php | 80 +++++++++++++++++++++++----------- src/FileHandler.php | 4 ++ 7 files changed, 94 insertions(+), 52 deletions(-) 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..8494317 100644 --- a/src/Commands/GetPackage.php +++ b/src/Commands/GetPackage.php @@ -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..80905c7 100644 --- a/src/Commands/GitPackage.php +++ b/src/Commands/GitPackage.php @@ -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} + {branch? : The branch to install}'; /** * The console command description. @@ -69,6 +70,10 @@ public function handle() // Common variables $source = $this->argument('url'); $origin = rtrim(strtolower($source), '/'); + $version = 'dev-master'; + if ($branch = $this->argument('branch')){ + $version = 'dev-'.$branch; + } if (is_null($this->argument('vendor')) || is_null($this->argument('name'))) { $this->setGitVendorAndPackage($origin); @@ -82,30 +87,18 @@ public function handle() $this->conveyor->checkIfPackageExists(); $this->makeProgress(); + // Install package from VCS + $this->info('Installing package from VCS...'); + $this->conveyor->installPackageFromVcs($origin, $version); + $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->createSymlinks(); $this->makeProgress(); // Finished creating the package, end of the progress bar diff --git a/src/Commands/ListPackages.php b/src/Commands/ListPackages.php index f034171..aa57495 100644 --- a/src/Commands/ListPackages.php +++ b/src/Commands/ListPackages.php @@ -35,14 +35,29 @@ 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)) { + $packages[] = [$name, 'packages/'.$match[1]]; + } + } + else if ($info['type'] === 'vcs'){ + $path = $packages_path . $name; + if (file_exists($path)){ + $pattern = '{'.addslashes($packages_path).'(.*)$}'; + if (preg_match($pattern, $path, $match)) { + $packages[] = [$name, 'packages/'.$match[1]]; + } + } } } - $headers = ['Package', 'Path']; $this->table($headers, $packages); } + + protected function getGitStatus($path) + { + $command = sprintf('git --work-tree=%s status', realpath($path)); + } } 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/Conveyor.php b/src/Conveyor.php index 406c7c3..c382628 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -2,7 +2,6 @@ namespace JeroenG\Packager; -use Illuminate\Support\Str; use RuntimeException; class Conveyor @@ -85,38 +84,54 @@ public function downloadFromGithub($origin, $piece, $branch) rename($this->vendorPath().'/'.$piece.'-'.$branch, $this->packagePath()); } - /** - * Dump Composer's autoloads. - * - * @return void - */ - public function dumpAutoloads() + public function getPackageName() { - shell_exec('composer dump-autoload'); + return $this->vendor . '/' . $this->package; } - public function installPackage() + public function installPackageFromPath() { - $this->addPathRepository(); + $this->addComposerRepository(); $this->requirePackage(); } + public function installPackageFromVcs($url, $version) + { + $this->addComposerRepository('vcs', $url); + $success = $this->requirePackage($version); + if (!$success){ + $this->removeComposerRepository(); + $message = 'No package named ' . $this->getPackageName() . ' with version ' . $version . ' was found in ' .$url; + throw new RuntimeException($message); + } + } + + public function createSymlinks() + { + // Find installed path + $result = $this->runProcess(['composer', 'info', $this->getPackageName(), '--path']); + if (preg_match('{' . $this->getPackageName() . ' (.*)$}m', $result['output'], $match)){ + $path = $match[1]; + symlink($path, $this->packagePath()); + } + } + public function uninstallPackage() { $this->removePackage(); - $this->removePathRepository(); + $this->removeComposerRepository(); } - public function addPathRepository() + protected function addComposerRepository(string $type = 'path', string $url = null) { $params = json_encode([ - 'type' => 'path', - 'url' => $this->packagePath() + 'type' => $type, + 'url' => $url ?: $this->packagePath() ]); $command = [ 'composer', 'config', - 'repositories.'.Str::slug($this->vendor.'-'.$this->package), + 'repositories.'.$this->getPackageName(), $params, '--file', 'composer.json' @@ -124,42 +139,57 @@ public function addPathRepository() return $this->runProcess($command); } - public function removePathRepository() + protected function removeComposerRepository() { return $this->runProcess([ 'composer', 'config', '--unset', - 'repositories.'.Str::slug($this->vendor.'-', $this->package) + 'repositories.'.$this->getPackageName() ]); } - public function requirePackage() + protected function requirePackage(string $version = null) { - return $this->runProcess([ + $package = $this->getPackageName(); + if ($version !== null) { + $package .= ':'.$version; + } + $result = $this->runProcess([ 'composer', 'require', - $this->vendor.'/'.$this->package + $package, + '--prefer-source' ]); + if (!$result['success']){ + if (preg_match('/Could not find a matching version of package/', $result['output'])){ + return false; + } + } + return true; } - public function removePackage() + protected function removePackage() { return $this->runProcess([ 'composer', 'remove', - $this->vendor.'/'.$this->package + $this->getPackageName() ]); } /** * @param array $command - * @return bool + * @return array */ protected function runProcess(array $command) { $process = new \Symfony\Component\Process\Process($command, base_path()); - $process->run(); - return $process->getExitCode() === 0; + $output = ''; + $process->run(function ($type, $buffer) use (&$output) { + $output .= $buffer; + }); + $success = $process->getExitCode() === 0; + return compact('success', 'output'); } } diff --git a/src/FileHandler.php b/src/FileHandler.php index 364048e..e9eb82d 100644 --- a/src/FileHandler.php +++ b/src/FileHandler.php @@ -83,6 +83,10 @@ public function makeDir($path) */ public function removeDir($path) { + if (is_link($path)){ + unlink($path); + return true; + } if ($path == 'packages' || $path == '/') { return false; } From 88da7a3629a2702b9cdbfc1537216c836805aa9d Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sun, 9 Jun 2019 18:14:15 +0200 Subject: [PATCH 02/11] Add git status column to packager:list output --- src/Commands/GitPackage.php | 28 +++++++++----------------- src/Commands/ListPackages.php | 38 ++++++++++++++++++++++++++--------- src/Conveyor.php | 6 +++--- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/Commands/GitPackage.php b/src/Commands/GitPackage.php index 80905c7..0c17a11 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. @@ -25,7 +25,7 @@ class GitPackage extends Command {url : The url of the git repository} {vendor? : The vendor part of the namespace} {name? : The name of package for the namespace} - {branch? : The branch to install}'; + {--branch=dev-master : The version to install}'; /** * The console command description. @@ -66,41 +66,35 @@ public function handle() { // Start the progress bar $this->startProgressBar(4); - // Common variables $source = $this->argument('url'); - $origin = rtrim(strtolower($source), '/'); - $version = 'dev-master'; - if ($branch = $this->argument('branch')){ - $version = 'dev-'.$branch; + $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 (is_null($this->argument('vendor')) || is_null($this->argument('name'))) { + 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, $version); + $this->conveyor->installPackageFromVcs($origin, $this->option('branch')); $this->makeProgress(); // Create the package directory $this->info('Creating packages directory...'); $this->conveyor->makeDir($this->conveyor->packagesPath()); $this->conveyor->makeDir($this->conveyor->vendorPath()); $this->makeProgress(); - - $this->info('Symlinking package to ' . $this->conveyor->packagePath()); + $this->info('Symlinking package to '.$this->conveyor->packagePath()); $this->conveyor->createSymlinks(); $this->makeProgress(); - // Finished creating the package, end of the progress bar $this->finishProgress('Package cloned successfully!'); } @@ -108,7 +102,6 @@ public function handle() protected function setGitVendorAndPackage($origin) { $pieces = explode('/', $origin); - if (Str::contains($origin, 'https')) { $vendor = $pieces[3]; $package = $pieces[4]; @@ -116,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 aa57495..0bab61f 100644 --- a/src/Commands/ListPackages.php +++ b/src/Commands/ListPackages.php @@ -3,6 +3,7 @@ namespace JeroenG\Packager\Commands; use Illuminate\Console\Command; +use Symfony\Component\Process\Process; /** * List all locally installed packages. @@ -35,29 +36,48 @@ public function handle() $repositories = $composer['repositories'] ?? []; $packages = []; foreach ($repositories as $name => $info) { - if ($info['type'] === 'path'){ + if ($info['type'] === 'path') { $path = $info['url']; $pattern = '{'.addslashes($packages_path).'(.*)$}'; if (preg_match($pattern, $path, $match)) { - $packages[] = [$name, 'packages/'.$match[1]]; + $status = $this->getGitStatus($path); + $packages[] = [$name, 'packages/'.$match[1], $status]; } - } - else if ($info['type'] === 'vcs'){ - $path = $packages_path . $name; - if (file_exists($path)){ + } elseif ($info['type'] === 'vcs') { + $path = $packages_path.$name; + if (file_exists($path)) { $pattern = '{'.addslashes($packages_path).'(.*)$}'; if (preg_match($pattern, $path, $match)) { - $packages[] = [$name, 'packages/'.$match[1]]; + $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) { - $command = sprintf('git --work-tree=%s status', realpath($path)); + $status = ''; + (new Process(['git', 'fetch', '--all'], $path))->run(); + $cmd = implode(' ', ['git', '--git-dir='.$path.'/.git', '--work-tree='.$path, 'status', '-sb']); + (new Process(['git', '--git-dir='.$path.'/.git', '--work-tree='.$path, 'status', '-sb'], $path))->run(function ( + $type, + $buffer + ) use (&$status) { + if (preg_match('/not a git repository/', $buffer)) { + $status = 'Not initialized'; + } + if (preg_match('/^##/', $buffer)) { + if (preg_match('/\[(.*)\]$/', $buffer, $match)) { + $status = ''.ucfirst($match[1]).''; + } else { + $status = 'Up to date'; + } + } + }); + return $status; } } diff --git a/src/Conveyor.php b/src/Conveyor.php index c382628..f107c85 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -92,7 +92,7 @@ public function getPackageName() public function installPackageFromPath() { $this->addComposerRepository(); - $this->requirePackage(); + $this->requirePackage(null, false); } public function installPackageFromVcs($url, $version) @@ -149,7 +149,7 @@ protected function removeComposerRepository() ]); } - protected function requirePackage(string $version = null) + protected function requirePackage(string $version = null, bool $prefer_source = true) { $package = $this->getPackageName(); if ($version !== null) { @@ -159,7 +159,7 @@ protected function requirePackage(string $version = null) 'composer', 'require', $package, - '--prefer-source' + '--prefer-' . ($prefer_source ? 'source' : 'dist'), ]); if (!$result['success']){ if (preg_match('/Could not find a matching version of package/', $result['output'])){ From 060085051f504a77e94bdc74a504df641ff641a7 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Fri, 21 Jun 2019 01:00:23 +0200 Subject: [PATCH 03/11] Add test --- phpunit.xml | 4 +--- tests/IntegratedTest.php | 24 +++++++++++++++++++++++- tests/TestHelper.php | 9 +++++++-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index a32f3b4..7121048 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,8 +20,6 @@ - + diff --git a/tests/IntegratedTest.php b/tests/IntegratedTest.php index 45fd514..b74d99a 100644 --- a/tests/IntegratedTest.php +++ b/tests/IntegratedTest.php @@ -3,6 +3,7 @@ namespace JeroenG\Packager\Tests; use Illuminate\Support\Facades\Artisan; +use Symfony\Component\Process\Process; class IntegratedTest extends TestCase { @@ -26,7 +27,7 @@ public function test_list_packages() Artisan::call('packager:new', ['vendor' => 'MyVendor', 'name' => 'MyPackage']); Artisan::call('packager:list'); - $this->seeInConsoleOutput('MyVendor'); + $this->seeInConsoleOutput(['MyVendor', 'Not initialized']); } public function test_removing_package() @@ -37,4 +38,25 @@ public function test_removing_package() Artisan::call('packager:remove', ['vendor' => 'MyVendor', 'name' => 'MyPackage', '--no-interaction' => true]); $this->seeInConsoleOutput('Package removed successfully!'); } + + public function test_adding_git_package() + { + Artisan::call('packager:git', + ['url' => 'Jeroen-G/testassist']); + $this->seeInConsoleOutput('Package cloned successfully!'); + } + + public function test_git_package_tracking() + { + Artisan::call('packager:git', + ['url' => 'jeroen-g/testassist']); + Artisan::call('packager:list'); + $this->seeInConsoleOutput('Up to date'); + $package_path = base_path('packages/jeroen-g/testassist'); + (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('Ahead 1'); + } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index cb79eab..49d3b97 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -11,9 +11,14 @@ trait TestHelper protected function seeInConsoleOutput($expectedText) { + if (!is_array($expectedText)){ + $expectedText = [$expectedText]; + } $consoleOutput = $this->app[Kernel::class]->output(); - $this->assertStringContainsString($expectedText, $consoleOutput, - "Did not see `{$expectedText}` in console output: `$consoleOutput`"); + foreach ($expectedText as $string) { + $this->assertStringContainsString($string, $consoleOutput, + "Did not see `{$string}` in console output: `$consoleOutput`"); + } } protected function doNotSeeInConsoleOutput($unExpectedText) From 3a2bd4a0fde52bcce56ee4dc6c744cd3e64602eb Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Fri, 21 Jun 2019 01:50:59 +0200 Subject: [PATCH 04/11] Update readme --- readme.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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: From f642f7d0eebdc8da609a5df04d057ee3800e6b6c Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sat, 22 Jun 2019 17:20:45 +0200 Subject: [PATCH 05/11] Fix detection of git repository. Allow package-skeleton to be cached. Only test download of skeleton once. --- config/packager.php | 1 + src/Commands/ListPackages.php | 12 ++--- src/Conveyor.php | 95 ++++++++++++++++++++++++++--------- src/FileHandler.php | 4 +- tests/IntegratedTest.php | 10 ++-- tests/TestCase.php | 3 +- tests/TestHelper.php | 14 ++++-- 7 files changed, 94 insertions(+), 45 deletions(-) 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/src/Commands/ListPackages.php b/src/Commands/ListPackages.php index 0bab61f..4cabf2c 100644 --- a/src/Commands/ListPackages.php +++ b/src/Commands/ListPackages.php @@ -3,6 +3,7 @@ namespace JeroenG\Packager\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\File; use Symfony\Component\Process\Process; /** @@ -60,21 +61,18 @@ public function handle() protected function getGitStatus($path) { - $status = ''; + if (! File::exists($path.'/.git')) { + return 'Not initialized'; + } + $status = 'Up to date'; (new Process(['git', 'fetch', '--all'], $path))->run(); - $cmd = implode(' ', ['git', '--git-dir='.$path.'/.git', '--work-tree='.$path, 'status', '-sb']); (new Process(['git', '--git-dir='.$path.'/.git', '--work-tree='.$path, 'status', '-sb'], $path))->run(function ( $type, $buffer ) use (&$status) { - if (preg_match('/not a git repository/', $buffer)) { - $status = 'Not initialized'; - } if (preg_match('/^##/', $buffer)) { if (preg_match('/\[(.*)\]$/', $buffer, $match)) { $status = ''.ucfirst($match[1]).''; - } else { - $status = 'Up to date'; } } }); diff --git a/src/Conveyor.php b/src/Conveyor.php index 6f14619..7675b27 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -3,6 +3,7 @@ namespace JeroenG\Packager; use RuntimeException; +use Illuminate\Support\Facades\File; class Conveyor { @@ -56,6 +57,14 @@ public function package($package = null) return $this->package; } + public static function fetchSkeleton(string $source, string $destination) + { + $zipFilePath = tempnam(getcwd(), 'package'); + (new self())->download($zipFilePath, $source) + ->extract($zipFilePath, $destination) + ->cleanUp($zipFilePath); + } + /** * Download the skeleton package. * @@ -63,10 +72,19 @@ public function package($package = null) */ public function downloadSkeleton() { - $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 = self::getSkeletonCachePath(); + $cacheExists = File::exists($cachePath); + if ($useCached && $cacheExists) { + File::copyDirectory($cachePath, $this->vendorPath()); + } else { + self::fetchSkeleton(config('packager.skeleton'), $this->vendorPath()); + } + $temporaryPath = $this->vendorPath().'/packager-skeleton-master'; + if ($useCached && ! $cacheExists) { + File::copyDirectory($temporaryPath, $cachePath); + } + rename($temporaryPath, $this->packagePath()); } /** @@ -86,22 +104,26 @@ public function downloadFromGithub($origin, $piece, $branch) public function getPackageName() { - return $this->vendor . '/' . $this->package; + return $this->vendor.'/'.$this->package; } public function installPackageFromPath() { + $this->disablePackagistRepo(); $this->addComposerRepository(); $this->requirePackage(null, false); + $this->enablePackagistRepo(); } public function installPackageFromVcs($url, $version) { + $this->disablePackagistRepo(); $this->addComposerRepository('vcs', $url); $success = $this->requirePackage($version); - if (!$success){ + $this->enablePackagistRepo(); + if (! $success) { $this->removeComposerRepository(); - $message = 'No package named ' . $this->getPackageName() . ' with version ' . $version . ' was found in ' .$url; + $message = 'No package named '.$this->getPackageName().' with version '.$version.' was found in '.$url; throw new RuntimeException($message); } } @@ -110,7 +132,7 @@ public function createSymlinks() { // Find installed path $result = $this->runProcess(['composer', 'info', $this->getPackageName(), '--path']); - if (preg_match('{' . $this->getPackageName() . ' (.*)$}m', $result['output'], $match)){ + if (preg_match('{'.$this->getPackageName().' (.*)$}m', $result['output'], $match)) { $path = $match[1]; symlink($path, $this->packagePath()); } @@ -124,17 +146,12 @@ public function uninstallPackage() protected function addComposerRepository(string $type = 'path', string $url = null) { - $params = json_encode([ - 'type' => $type, - 'url' => $url ?: $this->packagePath(), - ]); $command = [ 'composer', 'config', 'repositories.'.$this->getPackageName(), - $params, - '--file', - 'composer.json', + $type, + $url ?: $this->packagePath(), ]; return $this->runProcess($command); @@ -152,21 +169,19 @@ protected function removeComposerRepository() protected function requirePackage(string $version = null, bool $prefer_source = true) { - $package = $this->getPackageName(); - if ($version !== null) { - $package .= ':'.$version; - } + $package = sprintf('%s:%s', strtolower($this->getPackageName()), $version ?? '@dev'); $result = $this->runProcess([ 'composer', 'require', $package, - '--prefer-' . ($prefer_source ? 'source' : 'dist'), + '--prefer-'.($prefer_source ? 'source' : 'dist'), ]); - if (!$result['success']){ - if (preg_match('/Could not find a matching version of package/', $result['output'])){ + if (! $result['success']) { + if (preg_match('/Could not find a matching version of package/', $result['output'])) { return false; } } + return true; } @@ -175,7 +190,7 @@ protected function removePackage() return $this->runProcess([ 'composer', 'remove', - $this->getPackageName(), + strtolower($this->getPackageName()), ]); } @@ -193,4 +208,38 @@ protected function runProcess(array $command) $success = $process->getExitCode() === 0; return compact('success', 'output'); } + + protected function disablePackagistRepo() + { + $result = $this->runProcess([ + 'composer', + 'config', + 'repo.packagist', + 'false', + ]); + + return $result['success']; + } + + private function enablePackagistRepo() + { + $result = $this->runProcess([ + 'composer', + 'config', + 'repo.packagist', + 'true', + ]); + + return $result['success']; + } + + public static function getSkeletonCachePath(): string + { + if (defined('PHPUNIT_COMPOSER_INSTALL')) { + // Running PhpUnit + return __DIR__.'/../testbench/skeleton-cache'; + } + + return storage_path('app/laravel-packager/cache'); + } } diff --git a/src/FileHandler.php b/src/FileHandler.php index e9eb82d..d8a563c 100644 --- a/src/FileHandler.php +++ b/src/FileHandler.php @@ -83,11 +83,11 @@ public function makeDir($path) */ public function removeDir($path) { - if (is_link($path)){ + if (is_link($path)) { unlink($path); return true; } - if ($path == 'packages' || $path == '/') { + if ($path === 'packages' || $path === '/'){ return false; } diff --git a/tests/IntegratedTest.php b/tests/IntegratedTest.php index b74d99a..916d46b 100644 --- a/tests/IntegratedTest.php +++ b/tests/IntegratedTest.php @@ -9,6 +9,8 @@ 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!'); @@ -40,16 +42,10 @@ public function test_removing_package() } public function test_adding_git_package() - { - Artisan::call('packager:git', - ['url' => 'Jeroen-G/testassist']); - $this->seeInConsoleOutput('Package cloned successfully!'); - } - - public function test_git_package_tracking() { Artisan::call('packager:git', ['url' => 'jeroen-g/testassist']); + $this->seeInConsoleOutput('Package cloned successfully!'); Artisan::call('packager:list'); $this->seeInConsoleOutput('Up to date'); $package_path = base_path('packages/jeroen-g/testassist'); diff --git a/tests/TestCase.php b/tests/TestCase.php index a940d31..eb84922 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,7 +11,7 @@ abstract class TestCase extends TestBench protected const TEST_APP_TEMPLATE = __DIR__.'/../testbench/template'; protected const TEST_APP = __DIR__.'/../testbench/laravel'; - public static function setUpBeforeClass():void + public static function setUpBeforeClass(): void { if (! file_exists(self::TEST_APP_TEMPLATE)) { self::setUpLocalTestbench(); @@ -31,6 +31,7 @@ public function setUp(): void { $this->installTestApp(); parent::setUp(); + config()->set('packager.cache_skeleton', true); } /** diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 0f534f3..e3a4f40 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -2,6 +2,7 @@ namespace JeroenG\Packager\Tests; +use JeroenG\Packager\Conveyor; use Illuminate\Filesystem\Filesystem; use Symfony\Component\Process\Process; use Illuminate\Contracts\Console\Kernel; @@ -44,14 +45,17 @@ private static function setUpLocalTestbench() 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)); + $files->put(self::TEST_APP_TEMPLATE.'/composer.json', json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + // Pre-download the skeleton package + fwrite(STDOUT, "Downloading local copy of packager-skeleton\n"); + $skeleton_url = 'http://github.com/Jeroen-G/packager-skeleton/archive/master.zip'; + Conveyor::fetchSkeleton($skeleton_url, Conveyor::getSkeletonCachePath()); // 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); + (new Process(['composer', 'install', '--prefer-dist'], self::TEST_APP_TEMPLATE))->run(function ($t, $b) { + fwrite(STDOUT, $b); }); + fwrite(STDOUT, "Test environment installed\n"); } protected function installTestApp() From eb9c99d7f8247456a2847b43c543d868e9bf3b09 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sat, 22 Jun 2019 17:46:27 +0200 Subject: [PATCH 06/11] Use static cache folder regardless of environment --- .gitignore | 1 + src/Conveyor.php | 7 +------ tests/TestHelper.php | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) 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/src/Conveyor.php b/src/Conveyor.php index 7675b27..9fb0554 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -235,11 +235,6 @@ private function enablePackagistRepo() public static function getSkeletonCachePath(): string { - if (defined('PHPUNIT_COMPOSER_INSTALL')) { - // Running PhpUnit - return __DIR__.'/../testbench/skeleton-cache'; - } - - return storage_path('app/laravel-packager/cache'); + return __DIR__.'/../skeleton-cache'; } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php index e3a4f40..0ce2e36 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -52,9 +52,7 @@ private static function setUpLocalTestbench() Conveyor::fetchSkeleton($skeleton_url, Conveyor::getSkeletonCachePath()); // Install dependencies fwrite(STDOUT, "Installing test environment dependencies\n"); - (new Process(['composer', 'install', '--prefer-dist'], self::TEST_APP_TEMPLATE))->run(function ($t, $b) { - fwrite(STDOUT, $b); - }); + (new Process(['composer', 'install', '--prefer-dist'], self::TEST_APP_TEMPLATE))->run(); fwrite(STDOUT, "Test environment installed\n"); } From 09e173a063832098d7305e035cc8fa411a15c900 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sun, 23 Jun 2019 16:50:34 +0200 Subject: [PATCH 07/11] Refactor helper methods to traits. Reuse package created by first test. Ensure composer commands are run without Xdebug enabled. --- src/Commands/CheckPackage.php | 12 ++- src/Commands/GetPackage.php | 2 +- src/Commands/GitPackage.php | 6 +- src/Commands/ListPackages.php | 2 +- src/ComposerHandler.php | 121 ++++++++++++++++++++++++++++ src/Conveyor.php | 147 ++++++---------------------------- src/FileHandler.php | 30 +++++++ src/ProcessRunner.php | 26 ++++++ tests/IntegratedTest.php | 54 +++++++++---- tests/RefreshTestbench.php | 116 +++++++++++++++++++++++++++ tests/TestCase.php | 132 +++++++++++++++++++++++------- tests/TestHelper.php | 73 ----------------- 12 files changed, 477 insertions(+), 244 deletions(-) create mode 100644 src/ComposerHandler.php create mode 100644 src/ProcessRunner.php create mode 100644 tests/RefreshTestbench.php delete mode 100644 tests/TestHelper.php 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/GetPackage.php b/src/Commands/GetPackage.php index 8494317..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') { diff --git a/src/Commands/GitPackage.php b/src/Commands/GitPackage.php index 0c17a11..b4f8b80 100644 --- a/src/Commands/GitPackage.php +++ b/src/Commands/GitPackage.php @@ -25,7 +25,7 @@ class GitPackage extends Command {url : The url of the git repository} {vendor? : The vendor part of the namespace} {name? : The name of package for the namespace} - {--branch=dev-master : The version to install}'; + {--constraint=dev-master : The version to install}'; /** * The console command description. @@ -85,7 +85,7 @@ public function handle() $this->makeProgress(); // Install package from VCS $this->info('Installing package from VCS...'); - $this->conveyor->installPackageFromVcs($origin, $this->option('branch')); + $this->conveyor->installPackageFromVcs($origin, $this->option('constraint')); $this->makeProgress(); // Create the package directory $this->info('Creating packages directory...'); @@ -93,7 +93,7 @@ public function handle() $this->conveyor->makeDir($this->conveyor->vendorPath()); $this->makeProgress(); $this->info('Symlinking package to '.$this->conveyor->packagePath()); - $this->conveyor->createSymlinks(); + $this->conveyor->symlinkInstalledPackage(); $this->makeProgress(); // Finished creating the package, end of the progress bar $this->finishProgress('Package cloned successfully!'); diff --git a/src/Commands/ListPackages.php b/src/Commands/ListPackages.php index 4cabf2c..44d4334 100644 --- a/src/Commands/ListPackages.php +++ b/src/Commands/ListPackages.php @@ -61,7 +61,7 @@ public function handle() protected function getGitStatus($path) { - if (! File::exists($path.'/.git')) { + if (!File::exists($path.'/.git')) { return 'Not initialized'; } $status = 'Up to date'; diff --git a/src/ComposerHandler.php b/src/ComposerHandler.php new file mode 100644 index 0000000..5ef189e --- /dev/null +++ b/src/ComposerHandler.php @@ -0,0 +1,121 @@ + $type, + 'url' => $url + ]; + return self::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 + */ + protected function findInstalledPath(string $packageName): string + { + $packageName = strtolower($packageName); + $result = self::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 + */ + protected static function getComposerJsonArray(string $path): array + { + return json_decode(file_get_contents($path), true); + } + + protected static function modifyComposerJson(Closure $callback, string $composer_path) + { + $composer_path = rtrim($composer_path, '/'); + if (!preg_match('/composer\.json$/', $composer_path)){ + $composer_path .= '/composer.json'; + } + $original = self::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 + */ + protected static function runComposerCommand(array $command, string $cwd = null): array + { + array_unshift($command, 'php', '-n', self::getComposerExecutable()); + return self::runProcess($command, $cwd); + } +} diff --git a/src/Conveyor.php b/src/Conveyor.php index 9fb0554..f8ea58d 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -3,11 +3,10 @@ namespace JeroenG\Packager; use RuntimeException; -use Illuminate\Support\Facades\File; class Conveyor { - use FileHandler; + use FileHandler, ComposerHandler; /** * Package vendor namespace. @@ -57,7 +56,7 @@ public function package($package = null) return $this->package; } - public static function fetchSkeleton(string $source, string $destination) + public static function fetchSkeleton(string $source, string $destination): void { $zipFilePath = tempnam(getcwd(), 'package'); (new self())->download($zipFilePath, $source) @@ -70,167 +69,71 @@ public static function fetchSkeleton(string $source, string $destination) * * @return void */ - public function downloadSkeleton() + public function downloadSkeleton(): void { $useCached = config('packager.cache_skeleton'); $cachePath = self::getSkeletonCachePath(); - $cacheExists = File::exists($cachePath); + $cacheExists = $this->pathExists($cachePath); if ($useCached && $cacheExists) { - File::copyDirectory($cachePath, $this->vendorPath()); + $this->copyDir($cachePath, $this->vendorPath()); } else { self::fetchSkeleton(config('packager.skeleton'), $this->vendorPath()); } $temporaryPath = $this->vendorPath().'/packager-skeleton-master'; if ($useCached && ! $cacheExists) { - File::copyDirectory($temporaryPath, $cachePath); + $this->copyDir($temporaryPath, $cachePath); } - rename($temporaryPath, $this->packagePath()); + $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()); } - public function getPackageName() + public function getPackageName(): string { return $this->vendor.'/'.$this->package; } - public function installPackageFromPath() + public function installPackageFromPath(): void { - $this->disablePackagistRepo(); - $this->addComposerRepository(); - $this->requirePackage(null, false); - $this->enablePackagistRepo(); + $this->addComposerRepository($this->getPackageName(), 'path', $this->packagePath()); + $this->requirePackage($this->getPackageName(), null, false); } - public function installPackageFromVcs($url, $version) + public function installPackageFromVcs($url, $version): void { - $this->disablePackagistRepo(); - $this->addComposerRepository('vcs', $url); - $success = $this->requirePackage($version); - $this->enablePackagistRepo(); - if (! $success) { - $this->removeComposerRepository(); + $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 createSymlinks() + public function symlinkInstalledPackage(): bool { - // Find installed path - $result = $this->runProcess(['composer', 'info', $this->getPackageName(), '--path']); - if (preg_match('{'.$this->getPackageName().' (.*)$}m', $result['output'], $match)) { - $path = $match[1]; - symlink($path, $this->packagePath()); - } - } - - public function uninstallPackage() - { - $this->removePackage(); - $this->removeComposerRepository(); - } - - protected function addComposerRepository(string $type = 'path', string $url = null) - { - $command = [ - 'composer', - 'config', - 'repositories.'.$this->getPackageName(), - $type, - $url ?: $this->packagePath(), - ]; - - return $this->runProcess($command); - } - - protected function removeComposerRepository() - { - return $this->runProcess([ - 'composer', - 'config', - '--unset', - 'repositories.'.$this->getPackageName(), - ]); - } - - protected function requirePackage(string $version = null, bool $prefer_source = true) - { - $package = sprintf('%s:%s', strtolower($this->getPackageName()), $version ?? '@dev'); - $result = $this->runProcess([ - 'composer', - 'require', - $package, - '--prefer-'.($prefer_source ? 'source' : 'dist'), - ]); - if (! $result['success']) { - if (preg_match('/Could not find a matching version of package/', $result['output'])) { - return false; - } - } - - return true; - } - - protected function removePackage() - { - return $this->runProcess([ - 'composer', - 'remove', - strtolower($this->getPackageName()), - ]); - } - - /** - * @param array $command - * @return array - */ - protected function runProcess(array $command) - { - $process = new \Symfony\Component\Process\Process($command, base_path()); - $output = ''; - $process->run(function ($type, $buffer) use (&$output) { - $output .= $buffer; - }); - $success = $process->getExitCode() === 0; - return compact('success', 'output'); - } - - protected function disablePackagistRepo() - { - $result = $this->runProcess([ - 'composer', - 'config', - 'repo.packagist', - 'false', - ]); - - return $result['success']; + $sourcePath = $this->findInstalledPath($this->getPackageName()); + return $this->createSymlink($sourcePath, $this->packagePath()); } - private function enablePackagistRepo() + public function uninstallPackage(): void { - $result = $this->runProcess([ - 'composer', - 'config', - 'repo.packagist', - 'true', - ]); - - return $result['success']; + $this->removePackage($this->getPackageName()); + $this->removeComposerRepository($this->getPackageName()); } public static function getSkeletonCachePath(): string diff --git a/src/FileHandler.php b/src/FileHandler.php index d8a563c..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. * @@ -187,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..8570b65 --- /dev/null +++ b/src/ProcessRunner.php @@ -0,0 +1,26 @@ +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 916d46b..e361d8c 100644 --- a/tests/IntegratedTest.php +++ b/tests/IntegratedTest.php @@ -12,47 +12,71 @@ 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']); - + 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', 'Not initialized']); + $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' => 'jeroen-g/testassist']); + Artisan::call('packager:git', [ + 'url' => 'mortenscheel/mypackage', + 'vendor' => 'MyVendor', + 'name' => 'MyPackage' + ]); $this->seeInConsoleOutput('Package cloned successfully!'); + $this->assertComposerPackageInstalled('MyVendor/MyPackage'); Artisan::call('packager:list'); - $this->seeInConsoleOutput('Up to date'); - $package_path = base_path('packages/jeroen-g/testassist'); + $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('Ahead 1'); + $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..eaae8cf --- /dev/null +++ b/tests/RefreshTestbench.php @@ -0,0 +1,116 @@ +makeDir(self::getTestbenchTemplatePath()); + $original = __DIR__.'/../vendor/orchestra/testbench-core/laravel/'; + $instance->copyDir($original, self::getTestbenchTemplatePath()); + self::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"); + self::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 eb84922..cbae2a1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,56 +2,132 @@ 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 +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() + use ComposerHandler, FileHandler; + + /** + * @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(); - config()->set('packager.cache_skeleton', true); + $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 = self::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 = self::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')); + self::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'); + } + + public function testLocalTestbenchInstalled() + { + $this->assertDirectoryIsReadable(self::getLocalTestbenchPath()); } } diff --git a/tests/TestHelper.php b/tests/TestHelper.php deleted file mode 100644 index 0ce2e36..0000000 --- a/tests/TestHelper.php +++ /dev/null @@ -1,73 +0,0 @@ -app[Kernel::class]->output(); - foreach ($expectedText as $string) { - $this->assertStringContainsString($string, $consoleOutput, - "Did not see `{$string}` 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']; - $files->put(self::TEST_APP_TEMPLATE.'/composer.json', json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - // Pre-download the skeleton package - fwrite(STDOUT, "Downloading local copy of packager-skeleton\n"); - $skeleton_url = 'http://github.com/Jeroen-G/packager-skeleton/archive/master.zip'; - Conveyor::fetchSkeleton($skeleton_url, Conveyor::getSkeletonCachePath()); - // Install dependencies - fwrite(STDOUT, "Installing test environment dependencies\n"); - (new Process(['composer', 'install', '--prefer-dist'], self::TEST_APP_TEMPLATE))->run(); - fwrite(STDOUT, "Test environment installed\n"); - } - - 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); - } - } -} From 3d6b2e5e7406a24a2be57778f5924f566cc3cae0 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sun, 23 Jun 2019 17:25:10 +0200 Subject: [PATCH 08/11] Fix trait collision. --- tests/TestCase.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index cbae2a1..93cec80 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,8 +26,6 @@ protected function getPackageProviders($app) return ['JeroenG\Packager\PackagerServiceProvider']; } - use ComposerHandler, FileHandler; - /** * @param $expectedText * @throws ExpectationFailedException From 7b0f6bdfde3f791ce7cf69f7b3c7b6720747711e Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sun, 23 Jun 2019 17:47:23 +0200 Subject: [PATCH 09/11] Use packager-skeleton for git test --- tests/IntegratedTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/IntegratedTest.php b/tests/IntegratedTest.php index e361d8c..380fe63 100644 --- a/tests/IntegratedTest.php +++ b/tests/IntegratedTest.php @@ -20,6 +20,7 @@ public function test_new_package_is_created() public function test_get_existing_package() { + $this->assertComposerPackageNotInstalled('MyVendor/MyPackage'); Artisan::call('packager:get', [ 'url' => 'https://github.com/Jeroen-G/packager-skeleton', 'vendor' => 'MyVendor', @@ -53,7 +54,7 @@ public function test_removing_package() public function test_adding_git_package() { Artisan::call('packager:git', [ - 'url' => 'mortenscheel/mypackage', + 'url' => 'https://github.com/Jeroen-G/packager-skeleton', 'vendor' => 'MyVendor', 'name' => 'MyPackage' ]); From e434f1bb8e60c3419932f83b8153c2da12e91fb6 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sun, 23 Jun 2019 21:58:52 +0200 Subject: [PATCH 10/11] Refactor static methods --- src/ComposerHandler.php | 27 +++++++++++++-------------- src/Conveyor.php | 4 ++-- tests/RefreshTestbench.php | 10 +++++----- tests/TestCase.php | 13 ++++--------- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/ComposerHandler.php b/src/ComposerHandler.php index 5ef189e..3c4e5c7 100644 --- a/src/ComposerHandler.php +++ b/src/ComposerHandler.php @@ -12,7 +12,7 @@ trait ComposerHandler protected function removeComposerRepository($name) { - return self::modifyComposerJson(function (array $composer) use ($name){ + return $this->modifyComposerJson(function (array $composer) use ($name){ unset($composer['repositories'][$name]); return $composer; }, base_path()); @@ -20,17 +20,16 @@ protected function removeComposerRepository($name) /** * Determines the path to Composer executable - * @todo Might not work on Windows * @return string */ - protected static function getComposerExecutable(): string + public function getComposerExecutable(): string { return trim(shell_exec('which composer')) ?: 'composer'; } protected function removePackage(string $packageName): array { - return self::runComposerCommand([ + return $this->runComposerCommand([ 'remove', strtolower($packageName), '--no-progress', @@ -43,7 +42,7 @@ protected function requirePackage(string $packageName, string $version = null, b if ($version) { $package .= ':'.$version; } - $result = self::runComposerCommand([ + $result = $this->runComposerCommand([ 'require', $package, '--prefer-'.($prefer_source ? 'source' : 'dist'), @@ -63,7 +62,7 @@ protected function addComposerRepository(string $name, string $type = 'path', st 'type' => $type, 'url' => $url ]; - return self::modifyComposerJson(function (array $composer) use ($params, $name){ + return $this->modifyComposerJson(function (array $composer) use ($params, $name){ $composer['repositories'][$name] = $params; return $composer; }, base_path()); @@ -75,10 +74,10 @@ protected function addComposerRepository(string $name, string $type = 'path', st * @return string * @throws RuntimeException */ - protected function findInstalledPath(string $packageName): string + public function findInstalledPath(string $packageName): string { $packageName = strtolower($packageName); - $result = self::runComposerCommand([ + $result = $this->runComposerCommand([ 'info', $packageName, '--path']); @@ -92,18 +91,18 @@ protected function findInstalledPath(string $packageName): string * @param string $path * @return array */ - protected static function getComposerJsonArray(string $path): array + public function getComposerJsonArray(string $path): array { return json_decode(file_get_contents($path), true); } - protected static function modifyComposerJson(Closure $callback, string $composer_path) + 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 = self::getComposerJsonArray($composer_path); + $original = $this->getComposerJsonArray($composer_path); $modified = $callback($original); return file_put_contents($composer_path, json_encode($modified, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); } @@ -113,9 +112,9 @@ protected static function modifyComposerJson(Closure $callback, string $composer * @param string|null $cwd * @return array */ - protected static function runComposerCommand(array $command, string $cwd = null): array + public function runComposerCommand(array $command, string $cwd = null): array { - array_unshift($command, 'php', '-n', self::getComposerExecutable()); - return self::runProcess($command, $cwd); + array_unshift($command, 'php', '-n', $this->getComposerExecutable()); + return $this->runProcess($command, $cwd); } } diff --git a/src/Conveyor.php b/src/Conveyor.php index f8ea58d..9848403 100644 --- a/src/Conveyor.php +++ b/src/Conveyor.php @@ -72,12 +72,12 @@ public static function fetchSkeleton(string $source, string $destination): void public function downloadSkeleton(): void { $useCached = config('packager.cache_skeleton'); - $cachePath = self::getSkeletonCachePath(); + $cachePath = $this->getSkeletonCachePath(); $cacheExists = $this->pathExists($cachePath); if ($useCached && $cacheExists) { $this->copyDir($cachePath, $this->vendorPath()); } else { - self::fetchSkeleton(config('packager.skeleton'), $this->vendorPath()); + $this->fetchSkeleton(config('packager.skeleton'), $this->vendorPath()); } $temporaryPath = $this->vendorPath().'/packager-skeleton-master'; if ($useCached && ! $cacheExists) { diff --git a/tests/RefreshTestbench.php b/tests/RefreshTestbench.php index eaae8cf..eed8982 100644 --- a/tests/RefreshTestbench.php +++ b/tests/RefreshTestbench.php @@ -34,15 +34,15 @@ private static function setUpLocalTestbench(): void { try { fwrite(STDOUT, "Setting up test environment for first use.\n"); - Conveyor::fetchSkeleton( + $instance = new Conveyor; + $instance->fetchSkeleton( 'http://github.com/Jeroen-G/packager-skeleton/archive/master.zip', - Conveyor::getSkeletonCachePath() + $instance->getSkeletonCachePath() ); - $instance = new self; $instance->makeDir(self::getTestbenchTemplatePath()); $original = __DIR__.'/../vendor/orchestra/testbench-core/laravel/'; $instance->copyDir($original, self::getTestbenchTemplatePath()); - self::modifyComposerJson(function (array $composer) { + $instance->modifyComposerJson(function (array $composer) { // Remove "tests/TestCase.php" from autoload (it doesn't exist) unset($composer['autoload']['classmap'][1]); // Pre-install dependencies @@ -56,7 +56,7 @@ private static function setUpLocalTestbench(): void return $composer; }, self::getTestbenchTemplatePath()); fwrite(STDOUT, "Installing test environment dependencies\n"); - self::runComposerCommand([ + $instance->runComposerCommand([ 'install', '--prefer-dist', '--no-progress' diff --git a/tests/TestCase.php b/tests/TestCase.php index 93cec80..8a4147b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,7 +10,7 @@ use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; -class TestCase extends TestBench +abstract class TestCase extends TestBench { use RefreshTestbench; @@ -59,7 +59,7 @@ protected function doNotSeeInConsoleOutput($unExpectedText): void */ protected function assertComposerPackageInstalled(string $package): void { - $composer = self::getComposerJsonArray(base_path('composer.json')); + $composer = $this->getComposerJsonArray(base_path('composer.json')); $this->assertArrayHasKey(strtolower($package), $composer['require']); $path = $this->findInstalledPath($package); $this->assertDirectoryIsReadable($path); @@ -79,7 +79,7 @@ protected function assertComposerPackageInstalled(string $package): void */ protected function assertComposerPackageNotInstalled(string $package): void { - $composer = self::getComposerJsonArray(base_path('composer.json')); + $composer = $this->getComposerJsonArray(base_path('composer.json')); $this->assertArrayNotHasKey(strtolower($package), $composer['require']); $this->expectException(DirectoryNotFoundException::class); $this->findInstalledPath($package); @@ -113,7 +113,7 @@ protected function installFakePackageFromPath() $packagePath = base_path('packages/MyVendor/MyPackage'); $this->makeDir(base_path('vendor/myvendor')); $this->createSymlink($packagePath, base_path('vendor/myvendor/mypackage')); - self::modifyComposerJson(function (array $composer) use ($packagePath){ + $this->modifyComposerJson(function (array $composer) use ($packagePath){ $composer['repositories']['MyVendor/MyPackage'] = [ 'type' => 'path', 'url' => $packagePath @@ -123,9 +123,4 @@ protected function installFakePackageFromPath() }, base_path()); $this->assertComposerPackageInstalled('MyVendor/MyPackage'); } - - public function testLocalTestbenchInstalled() - { - $this->assertDirectoryIsReadable(self::getLocalTestbenchPath()); - } } From 42261ac4398c9f20ee23062078ff8ae4b9deb757 Mon Sep 17 00:00:00 2001 From: Morten Scheel Date: Sat, 6 Jul 2019 13:43:52 +0200 Subject: [PATCH 11/11] Run process without timeout. Default timeout is 60 seconds, which is usually enough. Sometimes running composer require takes longer, especially when resolving a package version, which exists in multiple repositories --- src/ProcessRunner.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ProcessRunner.php b/src/ProcessRunner.php index 8570b65..5a2c19a 100644 --- a/src/ProcessRunner.php +++ b/src/ProcessRunner.php @@ -16,6 +16,7 @@ protected static function runProcess(array $command, string $cwd = null): array $cwd = base_path(); } $process = new \Symfony\Component\Process\Process($command, $cwd); + $process->setTimeout(null); $output = ''; $process->run(static function ($type, $buffer) use (&$output) { $output .= $buffer;