diff --git a/composer/Plugin/Scaffold/Handler.php b/composer/Plugin/Scaffold/Handler.php
index e96b6f3820..ce4ca80820 100644
--- a/composer/Plugin/Scaffold/Handler.php
+++ b/composer/Plugin/Scaffold/Handler.php
@@ -161,6 +161,23 @@ public function scaffold() {
// take priority and which are conjoined.
$scaffold_files = new ScaffoldFileCollection($file_mappings, $location_replacements);
+ // Check to see if there are any scaffold files from a previous run that
+ // have been modified.
+ $hash_manager = HashManager::create($this->io, $this->composer->getConfig()->get('vendor-dir'), getcwd());
+
+ // Get the scaffold files that have been modified since they were written.
+ $modified = $hash_manager->getModified($scaffold_files);
+
+ // Ask the user what to do about the modified files (if any).
+ // Prompt the user and ask what to do about modified files.
+ $modified = $hash_manager->decideHowToHandleModified($modified);
+
+ // Remove the modified files if so instructed by the user. If nothing was
+ // modified, or the user did not want to keep any files, then this list
+ // will be empty.
+ $message = ' - Preserve [dest-rel-path]: modified by user.';
+ $scaffold_files->skipFiles($modified, $message);
+
// Process the list of scaffolded files.
$scaffold_results = ScaffoldFileCollection::process($scaffold_files, $this->io, $scaffold_options);
@@ -178,6 +195,9 @@ public function scaffold() {
// Call post-scaffold scripts.
$dispatcher->dispatch(self::POST_DRUPAL_SCAFFOLD_CMD);
+
+ // Save a hash for all of the scaffold result files.
+ $hash_manager->storeResultsHash($scaffold_results);
}
/**
diff --git a/composer/Plugin/Scaffold/HashManager.php b/composer/Plugin/Scaffold/HashManager.php
new file mode 100644
index 0000000000..b8e0e00338
--- /dev/null
+++ b/composer/Plugin/Scaffold/HashManager.php
@@ -0,0 +1,282 @@
+io = $io;
+ $this->vendorDir = $vendor_dir;
+ $this->dir = $dir;
+ }
+
+ /**
+ * Creates a HashManager.
+ *
+ * @param \Composer\IO\IOInterface $io
+ * The input/output object.
+ * @param string $vendor_dir
+ * The location of the vendor directory.
+ * @param string $dir
+ * The directory where the project is located.
+ *
+ * @return static
+ * A loaded hash manager.
+ */
+ public static function create(IOInterface $io, $vendor_dir, $dir) {
+ $hash_manager = new static($io, $vendor_dir, $dir);
+ $hash_path = $hash_manager->pathToHashCache();
+ if (file_exists($hash_path)) {
+ $hash_manager->hashCache = json_decode(file_get_contents($hash_path), TRUE);
+ }
+ return $hash_manager;
+ }
+
+ /**
+ * Returns the list of scaffold files which have been modified.
+ *
+ * If a scaffold file exists on disk, and it also has an entry in the hash
+ * manager (because it was scaffolded on a previous run), then we will check
+ * to see if its hash value has changed. Any file with a modified hash value
+ * will be included in the result of this method.
+ *
+ * @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection $scaffold_files
+ * Collection of destination paths.
+ *
+ * @return string[]
+ * List of modified files.
+ */
+ public function getModified(ScaffoldFileCollection $scaffold_files) {
+ // Iterate over our scaffold files and determine if any were modified.
+ $checked = [];
+ $modified = [];
+ foreach ($scaffold_files as $project_name => $project_files) {
+ foreach ($project_files as $destination_rel_path => $scaffold_file) {
+ if (!in_array($destination_rel_path, $checked)) {
+ $checked[] = $destination_rel_path;
+ if ($this->checkModified($scaffold_file->destination())) {
+ $modified[] = $destination_rel_path;
+ }
+ }
+ }
+ }
+
+ // Let the caller know what the user decided to keep.
+ return $modified;
+ }
+
+ /**
+ * Checks to see if a single scaffold file has been modified on disk.
+ *
+ * If the file existed on the last run and its hash has changed, then add it
+ * to the list of modified files.
+ *
+ * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $scaffold_file
+ * One scaffold file that is about to be placed.
+ *
+ * @return bool
+ * True if file has been modified since it was scaffolded.
+ */
+ protected function checkModified(ScaffoldFilePath $scaffold_file) {
+ $relative = $scaffold_file->relativePath();
+ if (!isset($this->hashCache[$relative])) {
+ return FALSE;
+ }
+ $hash = $this->hashFile($scaffold_file);
+ if (empty($hash)) {
+ return FALSE;
+ }
+ return $hash != $this->hashCache[$relative];
+ }
+
+ /**
+ * Asks the user how to handle modified files.
+ *
+ * As a side effect of this function, the root-level composer.json file will
+ * be modified if the user selects the "keep" option.
+ *
+ * @param string[] $modified
+ * An array of destination paths that were modified.
+ *
+ * @return string[]
+ * An array of modified that should not be preserved.
+ */
+ public function decideHowToHandleModified($modified) {
+ // Nothing modified? Nothing to do.
+ if (empty($modified)) {
+ return [];
+ }
+
+ // Show the user which files are in danger of being overwritten.
+ $show_modified = implode("\n", array_map(function ($item) {
+ return " - $item";
+ }, $modified));
+ $this->io->writeError("The following managed scaffold files have been modified:\n$show_modified\n");
+
+ // Ask the user what they want to do
+ while (TRUE) {
+ $default = $this->defaultAnswerForModifiedPrompt();
+
+ // @todo: Composer also offers 'v' (view) and 'd' (diff); we could add
+ // 'view' as a follow-on task, but 'diff' would be difficult here.
+ switch ($this->io->ask(' Discard changes [y,n,k,a,?]? ', $default)) {
+ case 'y':
+ return [];
+
+ case 'n':
+ return $modified;
+
+ case 'k':
+ $this->overrideModifiedInComposerJson($modified);
+ return $modified;
+
+ case 'a':
+ throw new \RuntimeException('Scaffold aborted');
+
+ case '?':
+ default:
+ $this->io->writeError([
+ ' y - discard changes and rewrite scaffold files',
+ ' n - keep modified files in their current state; ask again the next time Composer runs',
+ ' k - keep modified files and modify composer.json to avoid being asked again',
+ ' a - abort scaffold operation',
+ ' For information on how to manage scaffold modifications, see:',
+ ' https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold',
+ ]);
+ }
+ }
+ }
+
+ /**
+ * Determines what our default action should be for prompt.
+ *
+ * The default value is returned automatically in non-interactive mode, or
+ * if the user hits [RETURN] without making a selection.
+ *
+ * @return string
+ * Default value to return from 'modified' prompt.
+ */
+ protected function defaultAnswerForModifiedPrompt() {
+ // Interactive? Ignore environment variables and always default to '?'.
+ if ($this->io->isInteractive()) {
+ return '?';
+ }
+ // Keep modified files and don't ask again.
+ if (getenv('DRUPAL_SCAFFOLD_KEEP_MODIFIED')) {
+ return 'k';
+ }
+ // Discard modified if instructed.
+ if (getenv('DRUPAL_SCAFFOLD_DISCARD_MODIFIED')) {
+ return 'y';
+ }
+ // Default is preserve modified files and ask again next time.
+ return 'n';
+ }
+
+ /**
+ * Modifies composer.json to preserve modified files on subsequent runs.
+ *
+ * @param array $modified
+ * List of modified packages to disable in the composer.json file mappings.
+ */
+ protected function overrideModifiedInComposerJson(array $modified) {
+ $composer_json_file = new JsonFile($this->dir . '/composer.json');
+
+ $composer_json_data = $composer_json_file->read();
+ foreach ($modified as $keepPackage) {
+ $composer_json_data['extra']['drupal-scaffold']['file-mapping'][$keepPackage] = FALSE;
+ }
+ $composer_json_file->write($composer_json_data);
+ }
+
+ /**
+ * Stores the provided list of file paths to the hash cache file.
+ *
+ * @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[] $files
+ * A list of scaffold results, each of which holds a path.
+ */
+ public function storeResultsHash(array $files) {
+ foreach ($files as $scaffold_result) {
+ if ($scaffold_result->isManaged()) {
+ $hash = $this->hashFile($scaffold_result->destination());
+ $relative = $scaffold_result->destination()->relativePath();
+ $this->hashCache[$relative] = $hash;
+ }
+ }
+ $hash_path = $this->pathToHashCache();
+ file_put_contents($hash_path, json_encode($this->hashCache));
+ }
+
+ /**
+ * Gets the path to the file where the list of hashes is stored.
+ *
+ * @return string
+ * Path where the list of scaffolded files and their hashes are stored.
+ */
+ protected function pathToHashCache() {
+ return "{$this->vendorDir}/drupal/scaffolded.json";
+ }
+
+ /**
+ * Calculates the on-disk hash value of a scaffold file.
+ *
+ * @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $scaffold_file
+ * Path to the scaffold file, at its destination path.
+ *
+ * @return string
+ * Hash value.
+ */
+ protected function hashFile(ScaffoldFilePath $scaffold_file) {
+ $path = $scaffold_file->fullPath();
+ if (!file_exists($path)) {
+ return '';
+ }
+ return sha1_file($path);
+ }
+
+}
diff --git a/composer/Plugin/Scaffold/Operations/ScaffoldFileCollection.php b/composer/Plugin/Scaffold/Operations/ScaffoldFileCollection.php
index c000e64a47..bff67dd32c 100644
--- a/composer/Plugin/Scaffold/Operations/ScaffoldFileCollection.php
+++ b/composer/Plugin/Scaffold/Operations/ScaffoldFileCollection.php
@@ -72,11 +72,33 @@ public function __construct(array $file_mappings, Interpolator $location_replace
}
}
+ /**
+ * Filter out any item in the list whose destination path has been modified.
+ *
+ * @param array $files_to_skip
+ * List of destination paths
+ * @param string $message
+ * Explanation of why files were skipped
+ */
+ public function skipFiles($files_to_skip, $message) {
+ $op = new SkipOp($message);
+
+ // If we were instructed to keep modified files, then filter them out of our
+ // list of scaffold files to process.
+ foreach ($this->scaffoldFilesByProject as $project_name => $scaffold_files) {
+ foreach ($scaffold_files as $destination_rel_path => $scaffold_file) {
+ if (in_array($destination_rel_path, $files_to_skip)) {
+ $this->scaffoldFilesByProject[$project_name][$destination_rel_path] = new ScaffoldFileInfo($scaffold_file->destination(), $op);
+ }
+ }
+ }
+ }
+
/**
* {@inheritdoc}
*/
public function getIterator() {
- return new \RecursiveArrayIterator($this->scaffoldFilesByProject, \RecursiveArrayIterator::CHILD_ARRAYS_ONLY);
+ return new \ArrayIterator($this->scaffoldFilesByProject);
}
/**
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php
index c8e18e1b03..870f5703c7 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php
@@ -26,16 +26,6 @@ class ComposerHookTest extends TestCase {
use ExecTrait;
use AssertUtilsTrait;
- /**
- * The root of this project.
- *
- * Used to substitute this project's base directory into composer.json files
- * so Composer can find it.
- *
- * @var string
- */
- protected $projectRoot;
-
/**
* Directory to perform the tests in.
*
@@ -64,7 +54,9 @@ protected function setUp() {
$this->fileSystem = new Filesystem();
$this->fixtures = new Fixtures();
$this->fixtures->createIsolatedComposerCacheDir();
- $this->projectRoot = $this->fixtures->projectRoot();
+ $this->fixturesDir = $this->fixtures->tmpDir($this->getName());
+ $replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $this->fixtures->projectRoot()];
+ $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
}
/**
@@ -76,46 +68,173 @@ protected function tearDown() {
}
/**
- * Test to see if scaffold operation runs at the correct times.
+ * Test to see if scaffold operation runs after 'composer require'.
*/
- public function testComposerHooks() {
- $this->fixturesDir = $this->fixtures->tmpDir($this->getName());
- $replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $this->projectRoot];
- $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
+ public function testRequireHooks() {
$topLevelProjectDir = 'composer-hooks-fixture';
$sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+
// First test: run composer install. This is the same as composer update
// since there is no lock file. Ensure that scaffold operation ran.
$this->mustExec("composer install --no-ansi", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+
// Run composer required to add in the scaffold-override-fixture. This
// project is "allowed" in our main fixture project, but not required.
// We expect that requiring this library should re-scaffold, resulting
// in a changed default.settings.php file.
$stdout = $this->mustExec("composer require --no-ansi --no-interaction fixtures/scaffold-override-fixture:dev-master", $sut);
$this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
+
// Make sure that the appropriate notice informing us that scaffolding
// is allowed was printed.
$this->assertContains('Package fixtures/scaffold-override-fixture has scaffold operations, and is already allowed in the root-level composer.json file.', $stdout);
+ }
+
+ /**
+ * Tests to see if deleted / modified files are handled correctly.
+ */
+ public function testDeletedAndModifiedScaffoldFiles() {
+ $topLevelProjectDir = 'composer-hooks-fixture';
+ $sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+
+ // Run 'composer install' again to set up for our next set of tests.
+ $this->mustExec("composer install --no-ansi", $sut);
+
// Delete one scaffold file, just for test purposes, then run
// 'composer update' and see if the scaffold file is replaced.
@unlink($sut . '/sites/default/default.settings.php');
$this->assertFileNotExists($sut . '/sites/default/default.settings.php');
$this->mustExec("composer update --no-ansi", $sut);
- $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+
// Delete the same test scaffold file again, then run
// 'composer drupal:scaffold' and see if the scaffold file is
// re-scaffolded.
@unlink($sut . '/sites/default/default.settings.php');
$this->assertFileNotExists($sut . '/sites/default/default.settings.php');
$this->mustExec("composer install --no-ansi", $sut);
- $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+
// Delete the same test scaffold file yet again, then run
// 'composer install' and see if the scaffold file is re-scaffolded.
@unlink($sut . '/sites/default/default.settings.php');
$this->assertFileNotExists($sut . '/sites/default/default.settings.php');
$this->mustExec("composer drupal:scaffold --no-ansi", $sut);
- $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'scaffolded from the scaffold-override-fixture');
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+
+ // Modify the same test scaffold file, then run 'composer drupal:scaffold'
+ // and confirm the modification is not overwritten.
+ $contents = file_get_contents($sut . '/sites/default/default.settings.php');
+ file_put_contents($sut . '/sites/default/default.settings.php', $contents . "\n// Simulated user modification\n");
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut);
+ $this->assertContains("The following managed scaffold files have been modified:\n - [web-root]/sites/default/default.settings.php", $stdout);
+ $post_scaffold_contents = file_get_contents($sut . '/sites/default/default.settings.php');
+ $this->assertContains('Simulated user modification', $post_scaffold_contents);
+
+ // Run 'composer drupal:scaffold' twice in a row to make sure that the
+ // hash file of the modified file is retained, such that drupal:scaffold
+ // still recognizes that the modified file is modified.
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut);
+ $this->assertContains("The following managed scaffold files have been modified:\n - [web-root]/sites/default/default.settings.php", $stdout);
+ $post_scaffold_contents_two = file_get_contents($sut . '/sites/default/default.settings.php');
+ $this->assertContains('Simulated user modification', $post_scaffold_contents_two);
+
+ // Finally, run 'composer drupal:scaffold' yet a third time in a row, this
+ // time with an environment variable set that tells drupal:scaffold to act
+ // as if the user had requested that modified files be overwritten.
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut, ['DRUPAL_SCAFFOLD_DISCARD_MODIFIED' => 1]);
+ $this->assertContains("The following managed scaffold files have been modified:\n - [web-root]/sites/default/default.settings.php", $stdout);
+ $post_scaffold_contents_three = file_get_contents($sut . '/sites/default/default.settings.php');
+ $this->assertNotContains('Simulated user modification', $post_scaffold_contents_three);
+
+ // And if we run it yet again, then the file should not be modified.
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut);
+ $this->assertNotContains("The following managed scaffold files have been modified:\n - [web-root]/sites/default/default.settings.php", $stdout);
+ $post_scaffold_contents_three = file_get_contents($sut . '/sites/default/default.settings.php');
+ $this->assertNotContains('Simulated user modification', $post_scaffold_contents_three);
+
+ // Modify the test scaffold file even again, then run
+ // 'composer drupal:scaffold' with DRUPAL_SCAFFOLD_KEEP_MODIFIED. Confirm
+ // that the file is now excluded in the composer.json file's file-mapping.
+ $contents = file_get_contents($sut . '/sites/default/default.settings.php');
+ file_put_contents($sut . '/sites/default/default.settings.php', $contents . "\n// Simulated user modification\n");
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut, ['DRUPAL_SCAFFOLD_KEEP_MODIFIED' => 1]);
+ $this->assertContains("The following managed scaffold files have been modified:\n - [web-root]/sites/default/default.settings.php", $stdout);
+ $post_scaffold_contents = file_get_contents($sut . '/sites/default/default.settings.php');
+ $this->assertContains('Simulated user modification', $post_scaffold_contents);
+ $post_scaffold_composer_json = file_get_contents($sut . '/composer.json');
+ $this->assertContains('"[web-root]/sites/default/default.settings.php": false', $post_scaffold_composer_json);
+
+ // Scaffold again. We should not modify the files we kept
+ // (default.settings.php) above.
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut, ['DRUPAL_SCAFFOLD_DISCARD_MODIFIED' => 1]);
+ $this->assertNotContains("The following managed scaffold files have been modified:\n - [web-root]/sites/default/default.settings.php", $stdout);
+ }
+
+ /**
+ * Tests deleting and modifying two different scaffold files at the same time.
+ */
+ public function testDeletingAndModifyingAtSameTime() {
+ $topLevelProjectDir = 'composer-hooks-fixture';
+ $sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+
+ // Run 'composer install' again to set up for our next set of tests.
+ $this->mustExec("composer install --no-ansi", $sut);
+
+ // Delete 'default.settings.php' and modify 'robots.txt', then run
+ // the scaffold operation and see if the default settings file comes back,
+ // and the robots.txt file modification is preserved.
+ @unlink($sut . '/sites/default/default.settings.php');
+ $this->assertFileNotExists($sut . '/sites/default/default.settings.php');
+ $contents = file_get_contents($sut . '/robots.txt');
+ file_put_contents($sut . '/robots.txt', $contents . "\n# Simulated user modification\n");
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+ $post_scaffold_contents = file_get_contents($sut . '/robots.txt');
+ $this->assertContains('Simulated user modification', $post_scaffold_contents);
+
+ // Run the same test again to ensure that the scaffold tool still
+ // understands that the deleted file is managed.
+ @unlink($sut . '/sites/default/default.settings.php');
+ $this->assertFileNotExists($sut . '/sites/default/default.settings.php');
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+ $post_scaffold_contents = file_get_contents($sut . '/robots.txt');
+ $this->assertContains('Simulated user modification', $post_scaffold_contents);
+
+ // Make the same modifications again, but this time we will run the scaffold
+ // command with 'DRUPAL_SCAFFOLD_DISCARD_MODIFIED' environment variable,
+ // simulating the user selecting 'y' from the "discard modified" prompt.
+ // Make sure that both files go back the way they were.
+ @unlink($sut . '/sites/default/default.settings.php');
+ $this->assertFileNotExists($sut . '/sites/default/default.settings.php');
+ $contents = file_get_contents($sut . '/robots.txt');
+ file_put_contents($sut . '/robots.txt', $contents . "\n# Simulated user modification\n");
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut, ['DRUPAL_SCAFFOLD_DISCARD_MODIFIED' => 1]);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+ $post_scaffold_contents = file_get_contents($sut . '/robots.txt');
+ $this->assertNotContains('Simulated user modification', $post_scaffold_contents);
+
+ // Now we'll do it again with 'DRUPAL_SCAFFOLD_KEEP_MODIFIED', to simulate
+ // answering 'keep' to the "discard modified" prompt. Confirm that the
+ // composer.json file is modified.
+ @unlink($sut . '/sites/default/default.settings.php');
+ $this->assertFileNotExists($sut . '/sites/default/default.settings.php');
+ $contents = file_get_contents($sut . '/robots.txt');
+ file_put_contents($sut . '/robots.txt', $contents . "\n# Simulated user modification\n");
+ $stdout = $this->mustExec("composer drupal:scaffold --no-ansi --no-interaction 2>&1", $sut, ['DRUPAL_SCAFFOLD_KEEP_MODIFIED' => 1]);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', FALSE, 'Test version of default.settings.php from drupal/core');
+ $post_scaffold_contents = file_get_contents($sut . '/robots.txt');
+ $this->assertContains('Simulated user modification', $post_scaffold_contents);
+ $post_scaffold_composer_json = file_get_contents($sut . '/composer.json');
+ $this->assertContains('"[web-root]/robots.txt": false', $post_scaffold_composer_json);
+ }
+
+ /**
+ * Tests to see if create-project scaffolds correctly.
+ */
+ public function testCreateProject() {
// Run 'composer create-project' to create a new test project called
// 'create-project-test', which is a copy of 'fixtures/drupal-drupal'.
$sut = $this->fixturesDir . '/create-project-test';
@@ -127,8 +246,10 @@ public function testComposerHooks() {
$this->assertScaffoldedFile($sut . '/index.php', FALSE, 'Test version of index.php from drupal/core');
$topLevelProjectDir = 'composer-hooks-nothing-allowed-fixture';
$sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+
// Run composer install on an empty project.
$this->mustExec("composer install --no-ansi", $sut);
+
// Require a project that is not allowed to scaffold and confirm that we
// get a warning, and it does not scaffold.
$stdout = $this->mustExec("composer require --no-ansi --no-interaction fixtures/scaffold-override-fixture:dev-master", $sut);