Newer
Older

Adam G-H
committed
<?php

omkar podey
committed
declare(strict_types = 1);

Adam G-H
committed
namespace Drupal\Tests\package_manager\Kernel;

Adam G-H
committed
use Drupal\Core\DependencyInjection\ContainerBuilder;

Adam G-H
committed
use Drupal\KernelTests\KernelTestBase;
use Drupal\package_manager\Event\StageEvent;
use Drupal\package_manager\StatusCheckTrait;
use Drupal\package_manager\UnusedConfigFactory;
use Drupal\package_manager\Validator\DiskSpaceValidator;
use Drupal\package_manager\Exception\StageException;
use Drupal\package_manager\Exception\StageValidationException;

Adam G-H
committed
use Drupal\package_manager\Stage;

Adam G-H
committed
use Drupal\package_manager_bypass\Beginner;
use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait;

Adam G-H
committed
use Drupal\Tests\package_manager\Traits\ValidationTestTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
use org\bovigo\vfs\vfsStream;
use PHPStan\Rules\Arrays\UnpackIterableInArrayRule;
use PhpTuf\ComposerStager\Domain\Value\Path\PathInterface;
use PhpTuf\ComposerStager\Infrastructure\Factory\Path\PathFactoryInterface;
use PhpTuf\ComposerStager\Infrastructure\Value\Path\AbstractPath;
use Psr\Http\Message\RequestInterface;
use Symfony\Component\DependencyInjection\Definition;

Adam G-H
committed
/**
* Base class for kernel tests of Package Manager's functionality.

Yash Rode
committed
*
* @internal

Adam G-H
committed
*/
abstract class PackageManagerKernelTestBase extends KernelTestBase {
use FixtureUtilityTrait;
use StatusCheckTrait;

Adam G-H
committed
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;

Adam G-H
committed
/**
* {@inheritdoc}
*/
protected static $modules = [
'package_manager',
'package_manager_bypass',
'system',
'update',
'update_test',

Adam G-H
committed
];

Ted Bowman
committed
/**
* The service IDs of any validators to disable.
*
* @var string[]
*/
protected $disableValidators = [];

Ted Bowman
committed
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('package_manager');
$this->createVirtualProject();
// The Update module's default configuration must be installed for our

Adam G-H
committed
// 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();
}

Adam G-H
committed
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
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);
}
// Ensure that Composer Stager uses the test path factory, which is aware
// of the virtual file system.
$definition = new Definition(TestPathFactory::class);
$class = $definition->getClass();
$container->setDefinition($class, $definition->setPublic(FALSE));
$container->setAlias(PathFactoryInterface::class, $class);
// When a virtual 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 ::createVirtualProject()
$container->getDefinition('package_manager.validator.disk_space')
->addTag('persist');

Adam G-H
committed

Ted Bowman
committed
foreach ($this->disableValidators as $service_id) {
if ($container->hasDefinition($service_id)) {
$container->getDefinition($service_id)->clearTag('event_subscriber');
}
}

Adam G-H
committed
}

Adam G-H
committed
/**
* Creates a stage object for testing purposes.

Adam G-H
committed
*
* @return \Drupal\Tests\package_manager\Kernel\TestStage
* A stage object, with test-only modifications.

Adam G-H
committed
*/
protected function createStage(): TestStage {
return new TestStage(
// @todo Remove this in https://www.drupal.org/i/3303167
new UnusedConfigFactory(),

Adam G-H
committed
$this->container->get('package_manager.path_locator'),
$this->container->get('package_manager.beginner'),
$this->container->get('package_manager.stager'),
$this->container->get('package_manager.committer'),

Ted Bowman
committed
$this->container->get('file_system'),

Adam G-H
committed
$this->container->get('event_dispatcher'),

Adam G-H
committed
$this->container->get('tempstore.shared'),
$this->container->get('datetime.time'),
new TestPathFactory(),
$this->container->get('package_manager.failure_marker')

Adam G-H
committed
);
}
/**
* 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\Stage
* The stage that was used to collect the validation results.
*/
protected function assertResults(array $expected_results, string $event_class = NULL): Stage {
$stage = $this->createStage();

Adam G-H
committed
try {
$stage->create();
$stage->require(['drupal/core:9.8.1']);
$stage->apply();
$stage->postApply();

Adam G-H
committed
$stage->destroy();
// If we did not get an exception, ensure we didn't expect any results.
$this->assertEmpty($expected_results);

Adam G-H
committed
}
catch (StageValidationException $e) {
$this->assertNotEmpty($expected_results);

Adam G-H
committed
$this->assertValidationResultsEqual($expected_results, $e->getResults());
// TestStage::dispatch() attaches the event object to the exception so
// that we can analyze it.
$this->assertNotEmpty($event_class);

Adam G-H
committed
$this->assertInstanceOf($event_class, $e->event);
}
return $stage;

Adam G-H
committed
}
/**
* Asserts validation results are returned from the status check event.
*
* @param \Drupal\package_manager\ValidationResult[] $expected_results
* The expected validation results.

Narendra Singh Rathore
committed
* @param \Drupal\package_manager\Stage|null $stage
* (optional) The stage to use to create the status check event. If none is
* provided a new stage will be created.

Narendra Singh Rathore
committed
protected function assertStatusCheckResults(array $expected_results, Stage $stage = NULL): void {
$actual_results = $this->runStatusCheck($stage ?? $this->createStage(), $this->container->get('event_dispatcher'));
$this->assertValidationResultsEqual($expected_results, $actual_results);

Ted Bowman
committed
/**
* 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 {
$updates = $this->container->get('update.post_update_registry')
->getPendingUpdateFunctions();
$this->container->get('keyvalue')
->get('post_update')
->set('existing_updates', $updates);
}
/**
* Creates a test project in a virtual file system.
*
* This will create two directories at the root of the virtual file system:
* '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
* virtual file system and used as the active directory.
protected function createVirtualProject(?string $source_dir = NULL): void {
$source_dir = $source_dir ?? __DIR__ . '/../../fixtures/fake_site';
// Create the active directory and copy its contents from a fixture.
$active_dir = vfsStream::newDirectory('active');
$this->vfsRoot->addChild($active_dir);
$active_dir = $active_dir->url();
static::copyFixtureFilesTo($source_dir, $active_dir);
// Create a staging root directory alongside the active directory.
$stage_dir = vfsStream::newDirectory('stage');
$this->vfsRoot->addChild($stage_dir);
// Ensure the path locator points to the virtual active directory. We assume
// that is its own web root and that the vendor directory is at its top
// level.
/** @var \Drupal\package_manager_bypass\PathLocator $path_locator */
$path_locator = $this->container->get('package_manager.path_locator');
$path_locator->setPaths($active_dir, $active_dir . '/vendor', '', $stage_dir->url());

Adam G-H
committed

Adam G-H
committed
// Ensure the active directory will be copied into the virtual staging area.
Beginner::setFixturePath($active_dir);
// Since the path locator now points to a virtual file system, we need to
// replace the disk space validator with a test-only version that bypasses
// system calls, like disk_free_space() and stat(), which aren't supported

Adam G-H
committed
// by vfsStream. This validator will persist through container rebuilds.
// @see ::register()
$validator = new TestDiskSpaceValidator(
$this->container->get('string_translation')
);
// 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('package_manager.validator.disk_space', $validator);
}

Kunal Sachdev
committed
/**
* Copies a fixture directory into the active directory.

Kunal Sachdev
committed
*
* @param string $active_fixture_dir
* Path to fixture active directory from which the files will be copied.
*/
protected function copyFixtureFolderToActiveDirectory(string $active_fixture_dir) {

Kunal Sachdev
committed
$active_dir = $this->container->get('package_manager.path_locator')
->getProjectRoot();
static::copyFixtureFilesTo($active_fixture_dir, $active_dir);
}

Kunal Sachdev
committed
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/**
* Sets the current (running) version of core, as known to the Update module.
*
* @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);
}

Adam G-H
committed
}
/**
* Common functions for test stages.

Adam G-H
committed
*/
trait TestStageTrait {

Adam G-H
committed
/**
* {@inheritdoc}
*/

Adam G-H
committed
protected function dispatch(StageEvent $event, callable $on_error = NULL): void {

Adam G-H
committed
try {

Adam G-H
committed
parent::dispatch($event, $on_error);

Adam G-H
committed
}
catch (StageException $e) {
// Attach the event object to the exception so that test code can verify
// that the exception was thrown when a specific event was dispatched.
$e->event = $event;
throw $e;
}
}
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/**
* Defines a path value object that is aware of the virtual file system.
*/
class TestPath extends AbstractPath {
/**
* {@inheritdoc}
*/
protected function doResolve(string $basePath): string {
if (str_starts_with($this->path, vfsStream::SCHEME . '://')) {
return $this->path;
}
return implode(DIRECTORY_SEPARATOR, [$basePath, $this->path]);
}
}
/**
* Defines a path factory that is aware of the virtual file system.
*/
class TestPathFactory implements PathFactoryInterface {
/**
* {@inheritdoc}
*/
public static function create(string $path): PathInterface {
return new TestPath($path);
}
}
/**
* Defines a stage specifically for testing purposes.
*/
class TestStage extends Stage {
use TestStageTrait;

Adam G-H
committed
}
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
/**
* A test version of the disk space validator to bypass system-level functions.
*/
class TestDiskSpaceValidator extends DiskSpaceValidator {
/**
* Whether the root and vendor directories are on the same logical disk.
*
* @var bool
*/
public $sharedDisk = TRUE;
/**
* The amount of free space, keyed by path.
*
* @var float[]
*/
public $freeSpace = [];
/**
* {@inheritdoc}
*/
protected function stat(string $path): array {
return [
'dev' => $this->sharedDisk ? 'disk' : uniqid(),
];
}
/**
* {@inheritdoc}
*/
protected function freeSpace(string $path): float {
return $this->freeSpace[$path];
}
/**
* {@inheritdoc}
*/
public function temporaryDirectory(): string {
return 'temp';
}
}