Skip to content

Commit

Permalink
Merge pull request #180 from lemberg/issue/172-broken-composer
Browse files Browse the repository at this point in the history
Ensure that composer.json is not broken after running updates
  • Loading branch information
T2L committed Apr 16, 2020
2 parents fa5dd15 + 314024e commit 9efba1c
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 151 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Updates:

Fixes:

- [GH-172](https://github.com/lemberg/draft-environment/issues/172) - Ensure that composer.json is not broken after running updates; remove Configurer:setUp listener from all events
- [GH-168](https://github.com/lemberg/draft-environment/issues/168) - Ansible role geerlingguy.mysql @ 3.0.0 was failing to install due to incorrect python configuration in certain cases (fixed by setting `ansible_python_interpreter` to `/usr/bin/python3`

## Draft Environment 3.0.0-rc2, 2020-02-12
Expand Down
55 changes: 30 additions & 25 deletions src/Config/Update/Step/RemoveConfigurerComposerScript.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
namespace Lemberg\Draft\Environment\Config\Update\Step;

use Composer\Factory;
use Composer\Json\JsonFile;
use Composer\Script\ScriptEvents;
use Lemberg\Draft\Environment\Config\Update\UpdateStepInterface;

/**
Expand All @@ -27,45 +25,52 @@ public function getWeight(): int {
public function update(array &$config): void {
/** @var \Composer\Package\RootPackage $rootPackage */
$rootPackage = $this->composer->getPackage();
$scripts = $rootPackage->getScripts();
$this->removeScript(ScriptEvents::POST_INSTALL_CMD, $scripts);
$this->removeScript(ScriptEvents::POST_UPDATE_CMD, $scripts);
$scripts = $this->removeScript($rootPackage->getScripts());
$rootPackage->setScripts($scripts);
$this->composer->setPackage($rootPackage);

// Composer won't update composer.json automatically. Do it manually.
$file = Factory::getComposerFile();
$json = new JsonFile($file);
$composerDefinition = $json->read();
$contents = file_get_contents($file);
if ($contents === FALSE) {
throw new \RuntimeException(sprintf('File %s could not be read', $file));
}

// Parse composer.json contents into the object in order to preserve empty
// object that might be there. If converted to the associative array,
// empty objects later will be exported as empty arrays producing invalid
// composer.json file.
$decoded_contents = json_decode($contents);

if (count($scripts) > 0) {
$composerDefinition['scripts'] = $scripts;
$decoded_contents->scripts = $scripts;
}
else {
unset($composerDefinition['scripts']);
unset($decoded_contents->scripts);
}
$json->write($composerDefinition);

$this->getFilesystem()->dumpFile($file, json_encode($decoded_contents, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL);
}

/**
* Removes Configurer::setUp event listener from a given event.
* Removes Configurer::setUp event listener from all events.
*
* @param string $event
* @param array<string,array> $scripts
* @param array<string,array<int,string>> $scripts
*
* @return array<string,array<int,string>>
*/
private function removeScript(string $event, array &$scripts): void {
if (!array_key_exists($event, $scripts)) {
return;
private function removeScript(array $scripts): array {
// Remove script from all events.
foreach (array_keys($scripts) as $event) {
$scripts[$event] = array_values(
array_filter($scripts[$event], function (string $value): bool {
return $value !== 'Lemberg\Draft\Environment\Configurer::setUp';
})
);
}

$scripts[$event] = array_values(
array_filter($scripts[$event], function (string $value): bool {
return $value !== 'Lemberg\Draft\Environment\Configurer::setUp';
})
);

if (count($scripts[$event]) === 0) {
unset($scripts[$event]);
}
// Remove all empty events altogether.
return array_filter($scripts);
}

}
185 changes: 59 additions & 126 deletions tests/Config/Update/Step/RemoveConfigurerComposerScriptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
use Composer\Config as ComposerConfig;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Package\RootPackage;
use Composer\Script\ScriptEvents;
use Lemberg\Draft\Environment\App;
use Lemberg\Draft\Environment\Config\Config;
use Lemberg\Draft\Environment\Config\Manager\UpdateManager;
Expand Down Expand Up @@ -42,6 +40,18 @@ final class RemoveConfigurerComposerScriptTest extends TestCase {
*/
private $root;

/**
* @var \Symfony\Component\Filesystem\Filesystem
*/
private $fs;

/**
* Path to a directory with test composer.json files.
*
* @var string
*/
private $basePath;

/**
* @var \Lemberg\Draft\Environment\Config\Manager\UpdateManagerInterface
*/
Expand All @@ -59,12 +69,19 @@ protected function setUp(): void {

// Mock source and target configuration directories.
$this->root = vfsStream::setup()->url();
$fs = new Filesystem();
$fs->mkdir(["$this->root/source", "$this->root/target", "$this->root/wd"]);
$this->fs = new Filesystem();
$this->fs->mkdir([
"$this->root/source",
"$this->root/target",
"$this->root/wd",
]);

// Point composer to a test composer.json.
putenv("COMPOSER=$this->root/wd/composer.json");

// Build path to the test composer.json files based on this class name.
$this->basePath = './tests/fixtures/' . str_replace('\\', DIRECTORY_SEPARATOR, substr(__CLASS__, strlen('Lemberg\Tests\Draft\Enviaronment')));

$configObject = new Config("$this->root/source", "$this->root/target");
$this->configUpdateManager = new UpdateManager($this->composer, $this->io, $configObject);
}
Expand All @@ -80,155 +97,71 @@ final public function testGetWeight(): void {
/**
* Tests update step execution.
*
* @param array<int|string,array> $before
* @param array<int|string,array> $after
* @param string $composer_before
* @param string $composer_after
*
* @dataProvider updateDataProvider
*/
final public function testUpdate(array $before, array $after): void {
final public function testUpdate(string $composer_before, string $composer_after): void {

// Copy test composer,json to the vritual working directory.
$this->fs->copy("$this->basePath/$composer_before", "$this->root/wd/composer.json", TRUE);

$composer_wd = Factory::getComposerFile();
$before_content = file_get_contents($composer_wd);
if ($before_content === FALSE) {
throw new \RuntimeException(sprintf('File %s could not be read', $composer_wd));
}
$decoded_before_content = json_decode($before_content, TRUE);

/** @var \Composer\Package\RootPackage $rootPackage */
$rootPackage = $this->composer->getPackage();
$rootPackage->setScripts($before);
$rootPackage->setScripts($decoded_before_content['scripts'] ?? []);
$this->composer->setPackage($rootPackage);

// Run update.
$step = new RemoveConfigurerComposerScript($this->composer, $this->io, $this->configUpdateManager);
$config = [];

$filename = Factory::getComposerFile();
$json = new JsonFile($filename);
$json->write(['name' => App::PACKAGE_NAME, 'scripts' => $before]);

$step->update($config);

self::assertSame($after, $this->composer->getPackage()->getScripts());
$expected = count($after) > 0 ? ['name' => App::PACKAGE_NAME, 'scripts' => $after] : ['name' => App::PACKAGE_NAME];
self::assertSame(JsonFile::encode($expected) . "\n", file_get_contents($filename));
// Verify that Composer root package has the script removed.
$after_content = file_get_contents("$this->basePath/$composer_after");
if ($after_content === FALSE) {
throw new \RuntimeException(sprintf('File %s could not be read', "$this->basePath/$composer_after"));
}
$decoded_after_content = json_decode($after_content, TRUE);
self::assertSame($decoded_after_content['scripts'] ?? [], $this->composer->getPackage()->getScripts());

// Verify that composer.json has the script removed.
self::assertFileEquals("$this->basePath/$composer_after", $composer_wd);
}

/**
* Data provider for the ::testUpdate().
*
* @return array<int|string,array>
* @return array<int,array<int,string>>
*/
final public function updateDataProvider(): array {
return [
[
[],
[],
],
[
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
],
[
[
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
[
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
],
[
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
],
[
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
],
[
[
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
[
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
'composer-no-scripts.json',
'composer-no-scripts.json',
],
[
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
'Lemberg\Draft\Environment\Dummy::setUp',
],
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
'Lemberg\Draft\Environment\Configurer::setUp',
],
],
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Dummy::setUp',
],
],
'composer-empty-scripts.json',
'composer-no-scripts.json',
],
[
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
],
],
[],
'composer-with-only-scripts-in-the-middle.json',
'composer-no-scripts.json',
],
[
[
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
],
],
[],
'composer-with-only-scripts-in-the-end.json',
'composer-no-scripts.json',
],
[
[
ScriptEvents::POST_INSTALL_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
],
ScriptEvents::POST_UPDATE_CMD => [
'Lemberg\Draft\Environment\Configurer::setUp',
],
],
[],
'composer-with-mixed-scripts-before.json',
'composer-with-mixed-scripts-after.json',
],
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "lemberg/draft-environment",
"type": "composer-plugin",
"description": "Development environment for Draft Drupal.",
"license": "GPL-2.0-or-later",
"require": {
"php": "^7.2",
"composer-plugin-api": "^1.1",
"consolidation/comments": "dev-master",
"nette/robot-loader": "^3.2",
"symfony/filesystem": "^3.4 || ^4",
"symfony/yaml": "^3.4 || ^4"
},
"conflict": {},
"require-dev": {
"composer/composer": "^1.9",
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"dg/bypass-finals": "^1.1",
"drupal/coder": "^8.3",
"ergebnis/phpstan-rules": "^0.14.0",
"mikey179/vfsstream": "^1.6",
"php-mock/php-mock-phpunit": "^2.5",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^6.5 || ^7",
"slam/phpstan-extensions": "^4.0",
"thecodingmachine/phpstan-strict-rules": "^0.12"
},
"config": {
"sort-packages": true
},
"extra": {
"class": "Lemberg\\Draft\\Environment\\Composer\\Plugin"
},
"autoload": {
"psr-4": {
"Lemberg\\Draft\\Environment\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Lemberg\\Tests\\Draft\\Environment\\": "tests/"
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/T2L/comments.git"
}
],
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {}
}

0 comments on commit 9efba1c

Please sign in to comment.