class PackageManagerKernelTestBase
Base class for kernel tests of Package Manager's functionality.
@internal
Hierarchy
- class \Drupal\KernelTests\KernelTestBase extends \PHPUnit\Framework\TestCase implements \Drupal\Core\DependencyInjection\ServiceProviderInterface uses \Drupal\KernelTests\AssertContentTrait, \Drupal\Tests\RandomGeneratorTrait, \Drupal\Tests\ConfigTestTrait, \Drupal\Tests\ExtensionListTestTrait, \Drupal\Tests\TestRequirementsTrait, \Drupal\Tests\PhpUnitCompatibilityTrait, \Prophecy\PhpUnit\ProphecyTrait, \Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait
- class \Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase extends \Drupal\KernelTests\KernelTestBase uses \Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait, \Drupal\Tests\package_manager\Traits\ComposerStagerTestTrait, \Drupal\Tests\package_manager\Traits\FixtureManipulatorTrait, \Drupal\Tests\package_manager\Traits\FixtureUtilityTrait, \Drupal\package_manager\StatusCheckTrait, \Drupal\Tests\package_manager\Traits\ValidationTestTrait
Expanded class hierarchy of PackageManagerKernelTestBase
8 files declare their use of PackageManagerKernelTestBase
- GitExcluderTest.php in core/
modules/ package_manager/ tests/ src/ Kernel/ PathExcluder/ GitExcluderTest.php - NodeModulesExcluderTest.php in core/
modules/ package_manager/ tests/ src/ Kernel/ PathExcluder/ NodeModulesExcluderTest.php - SiteConfigurationExcluderTest.php in core/
modules/ package_manager/ tests/ src/ Kernel/ PathExcluder/ SiteConfigurationExcluderTest.php - SiteFilesExcluderTest.php in core/
modules/ package_manager/ tests/ src/ Kernel/ PathExcluder/ SiteFilesExcluderTest.php - SqliteDatabaseExcluderTest.php in core/
modules/ package_manager/ tests/ src/ Kernel/ PathExcluder/ SqliteDatabaseExcluderTest.php
File
-
core/
modules/ package_manager/ tests/ src/ Kernel/ PackageManagerKernelTestBase.php, line 45
Namespace
Drupal\Tests\package_manager\KernelView source
abstract class PackageManagerKernelTestBase extends KernelTestBase {
use AssertPreconditionsTrait;
use ComposerStagerTestTrait;
use FixtureManipulatorTrait;
use FixtureUtilityTrait;
use StatusCheckTrait;
use ValidationTestTrait;
/**
* The mocked HTTP client that returns metadata about available updates.
*
* We need to preserve this as a class property so that we can re-inject it
* into the container when a rebuild is triggered by module installation.
*
* @var \GuzzleHttp\Client
*
* @see ::register()
*/
private $client;
/**
* {@inheritdoc}
*/
protected static $modules = [
'fixture_manipulator',
'package_manager',
'package_manager_bypass',
'system',
'update',
'update_test',
];
/**
* The service IDs of any validators to disable.
*
* @var string[]
*/
protected $disableValidators = [];
/**
* The test root directory, if any, created by ::createTestProject().
*
* @var string|null
*
* @see ::createTestProject()
* @see ::tearDown()
*/
protected ?string $testProjectRoot = NULL;
/**
* The Symfony filesystem class.
*
* @var \Symfony\Component\Filesystem\Filesystem
*/
private Filesystem $fileSystem;
/**
* A logger that will fail the test if Package Manager logs any errors.
*
* @var \ColinODell\PsrTestLogger\TestLogger
*
* @see ::tearDown()
*/
protected TestLogger $failureLogger;
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
$this->installConfig('package_manager');
$this->fileSystem = new Filesystem();
$this->createTestProject();
// The Update module's default configuration must be installed for our
// fake release metadata to be fetched, and the System module's to ensure
// the site has a name.
$this->installConfig([
'system',
'update',
]);
// Make the update system think that all of System's post-update functions
// have run.
$this->registerPostUpdateFunctions();
// Ensure we can fail the test if any warnings, or worse, are logged by
// Package Manager.
// @see ::tearDown()
$this->failureLogger = new TestLogger();
$this->container
->get('logger.channel.package_manager')
->addLogger($this->failureLogger);
}
/**
* {@inheritdoc}
*/
protected function enableModules(array $modules) : void {
parent::enableModules($modules);
$this->registerPostUpdateFunctions();
}
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) : void {
parent::register($container);
// If we previously set up a mock HTTP client in ::setReleaseMetadata(),
// re-inject it into the container.
if ($this->client) {
$container->set('http_client', $this->client);
}
// When the test project is used, the disk space validator is replaced with
// a mock. When staged changes are applied, the container is rebuilt, which
// destroys the mocked service and can cause unexpected side effects. The
// 'persist' tag prevents the mock from being destroyed during a container
// rebuild.
// @see ::createTestProject()
$container->getDefinition(DiskSpaceValidator::class)
->addTag('persist');
// Ensure that our failure logger will survive container rebuilds.
$container->getDefinition('logger.channel.package_manager')
->addTag('persist');
array_walk($this->disableValidators, $container->removeDefinition(...));
}
/**
* Creates a stage object for testing purposes.
*
* @return \Drupal\Tests\package_manager\Kernel\TestStage
* A stage object, with test-only modifications.
*/
protected function createStage() : TestStage {
return new TestStage($this->container
->get(PathLocator::class), $this->container
->get(BeginnerInterface::class), $this->container
->get(StagerInterface::class), $this->container
->get(CommitterInterface::class), $this->container
->get(QueueFactory::class), $this->container
->get('event_dispatcher'), $this->container
->get('tempstore.shared'), $this->container
->get('datetime.time'), $this->container
->get(PathFactoryInterface::class), $this->container
->get(FailureMarker::class));
}
/**
* Asserts validation results are returned from a stage life cycle event.
*
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The expected validation results.
* @param string|null $event_class
* (optional) The class of the event which should return the results. Must
* be passed if $expected_results is not empty.
*
* @return \Drupal\package_manager\StageBase
* The stage that was used to collect the validation results.
*/
protected function assertResults(array $expected_results, ?string $event_class = NULL) : StageBase {
$stage = $this->createStage();
try {
$stage->create();
$stage->require([
'drupal/core:9.8.1',
]);
$stage->apply();
$stage->postApply();
$stage->destroy();
// If we did not get an exception, ensure we didn't expect any results.
$this->assertValidationResultsEqual([], $expected_results);
} catch (StageEventException $e) {
$this->assertNotEmpty($expected_results);
$this->assertInstanceOf($event_class, $e->event);
$this->assertExpectedResultsFromException($expected_results, $e);
}
return $stage;
}
/**
* Asserts validation results are returned from the status check event.
*
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The expected validation results.
* @param \Drupal\Tests\package_manager\Kernel\TestStage|null $stage
* (optional) The test stage to use to create the status check event. If
* none is provided a new stage will be created.
*/
protected function assertStatusCheckResults(array $expected_results, ?StageBase $stage = NULL) : void {
$actual_results = $this->runStatusCheck($stage ?? $this->createStage(), $this->container
->get('event_dispatcher'));
$this->assertValidationResultsEqual($expected_results, $actual_results);
}
/**
* Marks all pending post-update functions as completed.
*
* Since kernel tests don't normally install modules and register their
* updates, this method makes sure that we are testing from a clean, fully
* up-to-date state.
*/
protected function registerPostUpdateFunctions() : void {
static $updates = [];
$updates = array_merge($updates, $this->container
->get('update.post_update_registry')
->getPendingUpdateFunctions());
$this->container
->get('keyvalue')
->get('post_update')
->set('existing_updates', $updates);
}
/**
* Creates a test project.
*
* This will create a temporary uniques root directory and then creates two
* directories in it:
* 'active', which is the active directory containing a fake Drupal code base,
* and 'stage', which is the root directory used to stage changes. The path
* locator service will also be mocked so that it points to the test project.
*
* @param string|null $source_dir
* (optional) The path of a directory which should be copied into the
* test project and used as the active directory.
*/
protected function createTestProject(?string $source_dir = NULL) : void {
static $called;
if (isset($called)) {
throw new \LogicException('Only one test project should be created per kernel test method!');
}
else {
$called = TRUE;
}
$this->testProjectRoot = DrupalFileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . 'package_manager_testing_root' . $this->databasePrefix;
if (is_dir($this->testProjectRoot)) {
$this->fileSystem
->remove($this->testProjectRoot);
}
$this->fileSystem
->mkdir($this->testProjectRoot);
// Create the active directory and copy its contents from a fixture.
$active_dir = $this->testProjectRoot . DIRECTORY_SEPARATOR . 'active';
$this->assertTrue(mkdir($active_dir));
static::copyFixtureFilesTo($source_dir ?? __DIR__ . '/../../fixtures/fake_site', $active_dir);
// Removing 'vfs://root/' from site path set in
// \Drupal\KernelTests\KernelTestBase::setUpFilesystem as we don't use vfs.
$test_site_path = str_replace('vfs://root/', '', $this->siteDirectory);
// Copy directory structure from vfs site directory to our site directory.
$this->fileSystem
->mirror($this->siteDirectory, $active_dir . DIRECTORY_SEPARATOR . $test_site_path);
// Override siteDirectory to point to root/active/... instead of root/... .
$this->siteDirectory = $active_dir . DIRECTORY_SEPARATOR . $test_site_path;
// Override KernelTestBase::setUpFilesystem's Settings object.
$settings = Settings::getInstance() ? Settings::getAll() : [];
$settings['file_public_path'] = $this->siteDirectory . '/files';
$settings['config_sync_directory'] = $this->siteDirectory . '/files/config/sync';
new Settings($settings);
// Create a stage root directory alongside the active directory.
$staging_root = $this->testProjectRoot . DIRECTORY_SEPARATOR . 'stage';
$this->assertTrue(mkdir($staging_root));
// Ensure the path locator points to the test project. We assume that is its
// own web root and the vendor directory is at its top level.
/** @var \Drupal\package_manager_bypass\MockPathLocator $path_locator */
$path_locator = $this->container
->get(PathLocator::class);
$path_locator->setPaths($active_dir, $active_dir . '/vendor', '', $staging_root);
// This validator will persist through container rebuilds.
// @see ::register()
$validator = new TestDiskSpaceValidator($path_locator);
// By default, the validator should report that the root, vendor, and
// temporary directories have basically infinite free space.
$validator->freeSpace = [
$path_locator->getProjectRoot() => PHP_INT_MAX,
$path_locator->getVendorDirectory() => PHP_INT_MAX,
$validator->temporaryDirectory() => PHP_INT_MAX,
];
$this->container
->set(DiskSpaceValidator::class, $validator);
}
/**
* Sets the current (running) version of core, as known to the Update module.
*
* @todo Remove this function with use of the trait from the Update module in
* https://drupal.org/i/3348234.
*
* @param string $version
* The current version of core.
*/
protected function setCoreVersion(string $version) : void {
$this->config('update_test.settings')
->set('system_info.#all.version', $version)
->save();
}
/**
* Sets the release metadata file to use when fetching available updates.
*
* @param string[] $files
* The paths of the XML metadata files to use, keyed by project name.
*/
protected function setReleaseMetadata(array $files) : void {
$responses = [];
foreach ($files as $project => $file) {
$metadata = Utils::tryFopen($file, 'r');
$responses["/release-history/{$project}/current"] = new Response(200, [], Utils::streamFor($metadata));
}
$callable = function (RequestInterface $request) use ($responses) : Response {
return $responses[$request->getUri()
->getPath()] ?? new Response(404);
};
// The mock handler's queue consist of same callable as many times as the
// number of requests we expect to be made for update XML because it will
// retrieve one item off the queue for each request.
// @see \GuzzleHttp\Handler\MockHandler::__invoke()
$handler = new MockHandler(array_fill(0, 100, $callable));
$this->client = new Client([
'handler' => HandlerStack::create($handler),
]);
$this->container
->set('http_client', $this->client);
}
/**
* Adds an event listener on an event for testing purposes.
*
* @param callable $listener
* The listener to add.
* @param string $event_class
* (optional) The event to listen to. Defaults to PreApplyEvent.
* @param int $priority
* (optional) The priority. Defaults to PHP_INT_MAX.
*/
protected function addEventTestListener(callable $listener, string $event_class = PreApplyEvent::class, int $priority = PHP_INT_MAX) : void {
$this->container
->get('event_dispatcher')
->addListener($event_class, $listener, $priority);
}
/**
* Asserts event propagation is stopped by a certain event subscriber.
*
* @param string $event_class
* The event during which propagation is expected to stop.
* @param callable $expected_propagation_stopper
* The event subscriber (which subscribes to the given event class) which is
* expected to stop propagation. This event subscriber must have been
* registered by one of the installed Drupal module.
*/
protected function assertEventPropagationStopped(string $event_class, callable $expected_propagation_stopper) : void {
$priority = $this->container
->get('event_dispatcher')
->getListenerPriority($event_class, $expected_propagation_stopper);
// Ensure the event subscriber was actually a listener for the event.
$this->assertIsInt($priority);
// Add a listener with a priority that is 1 less than priority of the
// event subscriber. This listener would be called after
// $expected_propagation_stopper if the event propagation was not stopped
// and cause the test to fail.
$this->addEventTestListener(function () use ($event_class) : void {
$this->fail('Event propagation should have been stopped during ' . $event_class . '.');
}, $event_class, $priority - 1);
}
/**
* {@inheritdoc}
*/
protected function tearDown() : void {
// Delete the test project root, which contains the active directory and
// the stage directory. First, make it writable in case any permissions were
// changed during the test.
if ($this->testProjectRoot) {
$this->fileSystem
->chmod($this->testProjectRoot, 0777, 00, TRUE);
$this->fileSystem
->remove($this->testProjectRoot);
}
StageFixtureManipulator::handleTearDown();
// Ensure no warnings (or worse) were logged by Package Manager.
$this->assertFalse($this->failureLogger
->hasRecords(RfcLogLevel::EMERGENCY), 'Package Manager logged emergencies.');
$this->assertFalse($this->failureLogger
->hasRecords(RfcLogLevel::ALERT), 'Package Manager logged alerts.');
$this->assertFalse($this->failureLogger
->hasRecords(RfcLogLevel::CRITICAL), 'Package Manager logged critical errors.');
$this->assertFalse($this->failureLogger
->hasRecords(RfcLogLevel::ERROR), 'Package Manager logged errors.');
$this->assertFalse($this->failureLogger
->hasRecords(RfcLogLevel::WARNING), 'Package Manager logged warnings.');
parent::tearDown();
}
/**
* Asserts that a StageEventException has a particular set of results.
*
* @param array $expected_results
* The expected results.
* @param \Drupal\package_manager\Exception\StageEventException $exception
* The exception.
*/
protected function assertExpectedResultsFromException(array $expected_results, StageEventException $exception) : void {
$event = $exception->event;
$this->assertInstanceOf(PreOperationStageEvent::class, $event);
$stage = $event->stage;
$stage_dir = $stage->stageDirectoryExists() ? $stage->getStageDirectory() : NULL;
$this->assertValidationResultsEqual($expected_results, $event->getResults(), NULL, $stage_dir);
}
}
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.