Skip to content
Snippets Groups Projects
Commit 25d3dcd5 authored by Kunal Sachdev's avatar Kunal Sachdev Committed by Ted Bowman
Browse files

Issue #3305568 by kunal.sachdev, tedbow: Create a validator that detects...

Issue #3305568 by kunal.sachdev, tedbow: Create a validator that detects duplicate info.yml files in the stage on apply
parent a5fafcd9
No related branches found
No related tags found
No related merge requests found
......@@ -104,6 +104,12 @@ services:
- '@package_manager.path_locator'
tags:
- { name: event_subscriber }
package_manager.validator.duplicate_info_file:
class: Drupal\package_manager\Validator\DuplicateInfoFileValidator
arguments:
- '@package_manager.path_locator'
tags:
- { name: event_subscriber }
package_manager.test_site_excluder:
class: Drupal\package_manager\PathExcluder\TestSiteExcluder
arguments:
......
<?php
namespace Drupal\package_manager\Validator;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Finder\Finder;
/**
* Validates the stage does not have duplicate info.yml not present in active.
*
* @internal
* This is an internal part of Package Manager and may be changed or removed
* at any time without warning. External code should not interact with this
* class.
*/
class DuplicateInfoFileValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The path locator service.
*
* @var \Drupal\package_manager\PathLocator
*/
protected $pathLocator;
/**
* Constructs a DuplicateInfoFileValidator object.
*
* @param \Drupal\package_manager\PathLocator $path_locator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
/**
* Validates the stage does not have duplicate info.yml not present in active.
*/
public function validateDuplicateInfoFileInStage(PreApplyEvent $event): void {
$active_dir = $this->pathLocator->getProjectRoot();
$stage_dir = $event->getStage()->getStageDirectory();
$active_info_files = $this->findInfoFiles($active_dir);
$stage_info_files = $this->findInfoFiles($stage_dir);
foreach ($stage_info_files as $stage_info_file => $stage_info_count) {
if (isset($active_info_files[$stage_info_file])) {
// Check if stage directory has more info.yml files matching
// $stage_info_file than in the active directory.
if ($stage_info_count > $active_info_files[$stage_info_file]) {
$event->addError([
$this->t('The staging directory has @stage_count instances of @stage_info_file as compared to @active_count in the active directory. This likely indicates that a duplicate extension was installed.', [
'@stage_info_file' => $stage_info_file,
'@stage_count' => $stage_info_count,
'@active_count' => $active_info_files[$stage_info_file],
]),
]);
}
}
// Check if stage directory has two or more info.yml files matching
// $stage_info_file which are not in active directory.
elseif ($stage_info_count > 1) {
$event->addError([
$this->t('The staging directory has @stage_count instances of @stage_info_file. This likely indicates that a duplicate extension was installed.', [
'@stage_info_file' => $stage_info_file,
'@stage_count' => $stage_info_count,
]),
]);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
PreApplyEvent::class => 'validateDuplicateInfoFileInStage',
];
}
/**
* Recursively finds info.yml files in a directory.
*
* @param string $dir
* The path of the directory to check.
*
* @return int[]
* Array of count of info.yml files in the directory keyed by file name.
*/
protected function findInfoFiles(string $dir): array {
$info_files_finder = Finder::create()
->in($dir)
->ignoreUnreadableDirs()
->name('*.info.yml');
$info_files = [];
/** @var \Symfony\Component\Finder\SplFileInfo $info_file */
foreach (iterator_to_array($info_files_finder) as $info_file) {
// Skipping info.yml files in tests/fixtures because Drupal will not scan
// these directories when doing extension discovery.
//
// @todo We should also skip info.yml files in tests/modules,
// tests/themes, and tests/profiles directories in
// https://www.drupal.org/i/3306163.
if (strpos($info_file->getPath(), DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'fixtures') !== FALSE) {
continue;
}
$file_name = $info_file->getFilename();
$info_files[$file_name] = ($info_files[$file_name] ?? 0) + 1;
}
return $info_files;
}
}
<?php
namespace Drupal\Tests\package_manager\Kernel;
use Drupal\package_manager\Exception\StageValidationException;
use Drupal\package_manager\ValidationResult;
use Symfony\Component\Filesystem\Filesystem;
/**
* @covers \Drupal\package_manager\Validator\DuplicateInfoFileValidator
*
* @group package_manager
*/
class DuplicateInfoFileValidatorTest extends PackageManagerKernelTestBase {
/**
* Data provider for testDuplicateInfoFilesInStage.
*
* @return mixed[][]
* The test cases.
*/
public function providerDuplicateInfoFilesInStage(): array {
return [
'Duplicate info.yml files in stage' => [
[
'/module.info.yml',
],
[
'/module.info.yml',
'/modules/module.info.yml',
],
[
ValidationResult::createError([
'The staging directory has 2 instances of module.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
]),
],
],
// Duplicate files in stage but having different extension which we don't
// care about.
'Duplicate info files in stage' => [
[
'/my_file.info',
],
[
'/my_file.info',
'/modules/my_file.info',
],
[],
],
'Duplicate info.yml files in stage with one file in tests folder' => [
[
'/tests/fixtures/module.info.yml',
],
[
'/tests/fixtures/module.info.yml',
'/modules/module.info.yml',
],
[],
],
'Duplicate info.yml files in stage not present in active' => [
[],
[
'/module.info.yml',
'/modules/module.info.yml',
],
[
ValidationResult::createError([
'The staging directory has 2 instances of module.info.yml. This likely indicates that a duplicate extension was installed.',
]),
],
],
'Duplicate info.yml files in active' => [
[
'/module.info.yml',
'/modules/module.info.yml',
],
[
'/module.info.yml',
],
[],
],
'Same number of info.yml files in active and stage' => [
[
'/module.info.yml',
'/modules/module.info.yml',
],
[
'/module.info.yml',
'/modules/module.info.yml',
],
[],
],
'Multiple duplicate info.yml files in stage' => [
[
'/module1.info.yml',
'/module2.info.yml',
],
[
'/module1.info.yml',
'/modules/module1.info.yml',
'/module2.info.yml',
'/modules/module2.info.yml',
'/module2/module2.info.yml',
],
[
ValidationResult::createError([
'The staging directory has 2 instances of module1.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
]),
ValidationResult::createError([
'The staging directory has 3 instances of module2.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
]),
],
],
'Multiple duplicate info.yml files in stage not present in active' => [
[],
[
'/module1.info.yml',
'/modules/module1.info.yml',
'/module2.info.yml',
'/modules/module2.info.yml',
'/module2/module2.info.yml',
],
[
ValidationResult::createError([
'The staging directory has 2 instances of module1.info.yml. This likely indicates that a duplicate extension was installed.',
]),
ValidationResult::createError([
'The staging directory has 3 instances of module2.info.yml. This likely indicates that a duplicate extension was installed.',
]),
],
],
'Multiple duplicate info.yml files in stage with one info.yml file not present in active' => [
[
'/module1.info.yml',
],
[
'/module1.info.yml',
'/modules/module1.info.yml',
'/module2.info.yml',
'/modules/module2.info.yml',
'/module2/module2.info.yml',
],
[
ValidationResult::createError([
'The staging directory has 2 instances of module1.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
]),
ValidationResult::createError([
'The staging directory has 3 instances of module2.info.yml. This likely indicates that a duplicate extension was installed.',
]),
],
],
];
}
/**
* Tests that duplicate info.yml in stage raise an error.
*
* @param string[] $active_info_files
* An array of info.yml files in active directory.
* @param string[] $stage_info_files
* An array of info.yml files in stage directory.
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* An array of expected results.
*
* @dataProvider providerDuplicateInfoFilesInStage
*/
public function testDuplicateInfoFilesInStage(array $active_info_files, array $stage_info_files, array $expected_results): void {
$stage = $this->createStage();
$stage->create();
$stage->require(['composer/semver:^3']);
$active_dir = $this->container->get('package_manager.path_locator')
->getProjectRoot();
$stage_dir = $stage->getStageDirectory();
foreach ($active_info_files as $active_info_file) {
$this->createFileAtPath($active_dir, $active_info_file);
}
foreach ($stage_info_files as $stage_info_file) {
$this->createFileAtPath($stage_dir, $stage_info_file);
}
try {
$stage->apply();
$this->assertEmpty($expected_results);
}
catch (StageValidationException $e) {
$this->assertNotEmpty($expected_results);
$this->assertValidationResultsEqual($expected_results, $e->getResults());
}
}
/**
* Creates the file at the root directory.
*
* @param string $root_directory
* The base directory in which the file will be created.
* @param string $file_path
* The path of the file to create.
*/
private function createFileAtPath(string $root_directory, string $file_path): void {
$parts = explode(DIRECTORY_SEPARATOR, $file_path);
$filename = array_pop($parts);
$file_dir = str_replace($filename, '', $file_path);
$fs = new Filesystem();
if (!file_exists($file_dir)) {
$fs->mkdir($root_directory . $file_dir);
}
file_put_contents($root_directory . $file_path, ' ');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment