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);
- }
- }
-}