class TemplateProjectTestBase

Base class for tests which create a test site from a core project template.

The test site will be created from one of the core Composer project templates (drupal/recommended-project or drupal/legacy-project) and contain complete copies of Drupal core and all installed dependencies, completely independent of the currently running code base.

@internal

Hierarchy

Expanded class hierarchy of TemplateProjectTestBase

File

core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php, line 31

Namespace

Drupal\Tests\package_manager\Build
View source
abstract class TemplateProjectTestBase extends QuickStartTestBase {
  use AssertPreconditionsTrait;
  use FixtureUtilityTrait;
  use RandomGeneratorTrait;
  
  /**
   * The web root of the test site, relative to the workspace directory.
   *
   * @var string
   */
  private string $webRoot;
  
  /**
   * A secondary server instance, to serve XML metadata about available updates.
   *
   * @var \Symfony\Component\Process\Process
   */
  private Process $metadataServer;
  
  /**
   * All output that the PHP web server logs to the error buffer.
   *
   * @var string
   */
  private string $serverErrorLog = '';
  
  /**
   * The PHP web server's max_execution_time value.
   *
   * @var int
   */
  protected const MAX_EXECUTION_TIME = 20;
  
  /**
   * {@inheritdoc}
   */
  protected function setUp() : void {
    // Build tests cannot be run if SQLite minimum version is not met.
    $minimum_version = Tasks::SQLITE_MINIMUM_VERSION;
    $actual_version = (new \PDO('sqlite::memory:'))->query('select sqlite_version()')
      ->fetch()[0];
    if (version_compare($actual_version, $minimum_version, '<')) {
      $this->markTestSkipped("SQLite version {$minimum_version} or later is required, but {$actual_version} was detected.");
    }
    parent::setUp();
  }
  
  /**
   * {@inheritdoc}
   */
  protected function tearDown() : void {
    $this->metadataServer?->stop();
    parent::tearDown();
  }
  
  /**
   * Data provider for tests which use all the core project templates.
   *
   * @return string[][]
   *   The test cases.
   */
  public static function providerTemplate() : array {
    return [
      'RecommendedProject' => [
        'RecommendedProject',
      ],
      'LegacyProject' => [
        'LegacyProject',
      ],
    ];
  }
  
  /**
   * Sets the version of Drupal core to which the test site will be updated.
   *
   * @param string $version
   *   The Drupal core version to set.
   */
  protected function setUpstreamCoreVersion(string $version) : void {
    $this->createVendorRepository([
      'drupal/core' => $version,
      'drupal/core-dev' => $version,
      'drupal/core-dev-pinned' => $version,
      'drupal/core-recommended' => $version,
      'drupal/core-composer-scaffold' => $version,
      'drupal/core-project-message' => $version,
      'drupal/core-vendor-hardening' => $version,
    ]);
    // Change the \Drupal::VERSION constant and put placeholder text in the
    // README so we can ensure that we really updated to the correct version. We
    // also change the default site configuration files so we can ensure that
    // these are updated as well, despite `sites/default` being write-protected.
    // @see ::assertUpdateSuccessful()
    // @see ::createTestProject()
    $core_dir = $this->getWorkspaceDrupalRoot() . '/core';
    Composer::setDrupalVersion($this->getWorkspaceDrupalRoot(), $version);
    file_put_contents("{$core_dir}/README.txt", "Placeholder for Drupal core {$version}.");
    foreach ([
      'default.settings.php',
      'default.services.yml',
    ] as $file) {
      file_put_contents("{$core_dir}/assets/scaffold/files/{$file}", "# This is part of Drupal {$version}.\n", FILE_APPEND);
    }
  }
  
  /**
   * Returns the full path to the test site's document root.
   *
   * @return string
   *   The full path of the test site's document root.
   */
  protected function getWebRoot() : string {
    return $this->getWorkspaceDirectory() . '/' . $this->webRoot;
  }
  
  /**
   * {@inheritdoc}
   */
  protected function instantiateServer($port, $working_dir = NULL) {
    $working_dir = $working_dir ?: $this->webRoot;
    $finder = new PhpExecutableFinder();
    $working_path = $this->getWorkingPath($working_dir);
    $server = [
      $finder->find(),
      '-S',
      '127.0.0.1:' . $port,
      '-d max_execution_time=' . static::MAX_EXECUTION_TIME,
      '-d disable_functions=set_time_limit',
      '-t',
      $working_path,
    ];
    if (file_exists($working_path . DIRECTORY_SEPARATOR . '.ht.router.php')) {
      $server[] = $working_path . DIRECTORY_SEPARATOR . '.ht.router.php';
    }
    $ps = new Process($server, $working_path);
    $ps->setIdleTimeout(30)
      ->setTimeout(30)
      ->start(function ($output_type, $output) : void {
      if ($output_type === Process::ERR) {
        $this->serverErrorLog .= $output;
      }
    });
    // Wait until the web server has started. It is started if the port is no
    // longer available.
    for ($i = 0; $i < 50; $i++) {
      usleep(100000);
      if (!$this->checkPortIsAvailable($port)) {
        return $ps;
      }
    }
    throw new \RuntimeException(sprintf("Unable to start the web server.\nCMD: %s \nCODE: %d\nSTATUS: %s\nOUTPUT:\n%s\n\nERROR OUTPUT:\n%s", $ps->getCommandLine(), $ps->getExitCode(), $ps->getStatus(), $ps->getOutput(), $ps->getErrorOutput()));
  }
  
  /**
   * {@inheritdoc}
   */
  public function installQuickStart($profile, $working_dir = NULL) : void {
    parent::installQuickStart("{$profile} --no-ansi", $working_dir ?: $this->webRoot);
    // Allow package_manager to be installed, since it is hidden by default.
    // Always allow test modules to be installed in the UI and, for easier
    // debugging, always display errors in their dubious glory.
    $php = <<<END
\$settings['testing_package_manager'] = TRUE;
\$settings['extension_discovery_scan_tests'] = TRUE;
\$config['system.logging']['error_level'] = 'verbose';
END;
    $this->writeSettings($php);
  }
  
  /**
   * {@inheritdoc}
   */
  public function visit($request_uri = '/', $working_dir = NULL) {
    return parent::visit($request_uri, $working_dir ?: $this->webRoot);
  }
  
  /**
   * {@inheritdoc}
   */
  public function formLogin($username, $password, $working_dir = NULL) : void {
    parent::formLogin($username, $password, $working_dir ?: $this->webRoot);
  }
  
  /**
   * Adds a path repository to the test site.
   *
   * @param string $name
   *   An arbitrary name for the repository.
   * @param string $path
   *   The path of the repository. Must exist in the file system.
   * @param string $working_directory
   *   (optional) The Composer working directory. Defaults to 'project'.
   */
  protected function addRepository(string $name, string $path, $working_directory = 'project') : void {
    $this->assertDirectoryExists($path);
    $repository = json_encode([
      'type' => 'path',
      'url' => $path,
      'options' => [
        'symlink' => FALSE,
      ],
    ], JSON_UNESCAPED_SLASHES);
    $this->runComposer("composer config repo.{$name} '{$repository}'", $working_directory);
  }
  
  /**
   * Prepares the test site to serve an XML feed of available release metadata.
   *
   * @param array $xml_map
   *   The update XML map, as used by update_test.settings.
   *
   * @see \Drupal\package_manager_test_release_history\TestController::metadata()
   */
  protected function setReleaseMetadata(array $xml_map) : void {
    foreach ($xml_map as $metadata_file) {
      $this->assertFileIsReadable($metadata_file);
    }
    $xml_map = var_export($xml_map, TRUE);
    $this->writeSettings("\$config['update_test.settings']['xml_map'] = {$xml_map};");
  }
  
  /**
   * Creates a test project from a given template and installs Drupal.
   *
   * @param string $template
   *   The template to use. Can be 'RecommendedProject' or 'LegacyProject'.
   */
  protected function createTestProject(string $template) : void {
    // Create a copy of core (including its Composer plugins, templates, and
    // metapackages) which we can modify.
    $this->copyCodebase();
    $workspace_dir = $this->getWorkspaceDirectory();
    $project_dir = $workspace_dir . '/project';
    mkdir($project_dir);
    $data = file_get_contents("{$workspace_dir}/composer/Template/{$template}/composer.json");
    $data = json_decode($data, TRUE, flags: JSON_THROW_ON_ERROR);
    // Allow pre-release versions of dependencies.
    $data['minimum-stability'] = 'dev';
    // Remove all repositories and replace them with a single local one that
    // provides all dependencies.
    $data['repositories'] = [
      'vendor' => [
        'type' => 'composer',
        'url' => 'file://' . $workspace_dir . '/vendor.json',
      ],
      // Disable Packagist entirely so that we don't test the Internet.
'packagist.org' => FALSE,
    ];
    // Allow any version of the Drupal core packages in the template project.
    self::unboundCoreConstraints($data['require']);
    self::unboundCoreConstraints($data['require-dev']);
    // Do not run development Composer plugin, since it tries to run an
    // executable that might not exist while dependencies are being installed.
    // It adds no value to this test.
    $data['config']['allow-plugins']['dealerdirect/phpcodesniffer-composer-installer'] = FALSE;
    // Always force Composer to mirror path repositories. This is necessary
    // because dependencies are installed from a Composer-type repository, which
    // will normally try to symlink packages which are installed from local
    // directories. This breaks Package Manager, because it does not support
    // symlinks pointing outside the main code base.
    $script = '@putenv COMPOSER_MIRROR_PATH_REPOS=1';
    $data['scripts']['pre-install-cmd'] = $script;
    $data['scripts']['pre-update-cmd'] = $script;
    file_put_contents($project_dir . '/composer.json', json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    // Because we set the COMPOSER_MIRROR_PATH_REPOS=1 environment variable when
    // creating the project, none of the dependencies should be symlinked.
    $this->assertStringNotContainsString('Symlinking', $this->runComposer('composer install', 'project'));
    // If using the drupal/recommended-project template, we don't expect there
    // to be an .htaccess file at the project root. One would normally be
    // generated by Composer when Package Manager or other code creates a
    // ComposerInspector object in the active directory, except that Package
    // Manager takes specific steps to prevent that. So, here we're just
    // confirming that, in fact, Composer's .htaccess protection was disabled.
    // We don't do this for the drupal/legacy-project template because its
    // project root, which is also the document root, SHOULD contain a .htaccess
    // generated by Drupal core.
    // We do this check because this test uses PHP's built-in web server, which
    // ignores .htaccess files and everything in them, so a Composer-generated
    // .htaccess file won't cause this test to fail.
    if ($template === 'RecommendedProject') {
      $this->assertFileDoesNotExist("{$workspace_dir}/project/.htaccess");
    }
    // Now that we know the project was created successfully, we can set the
    // web root with confidence.
    $this->webRoot = 'project/' . $data['extra']['drupal-scaffold']['locations']['web-root'];
    // Install Drupal.
    $this->installQuickStart('standard');
    $this->formLogin($this->adminUsername, $this->adminPassword);
    // When checking for updates, we need to be able to make sub-requests, but
    // the built-in PHP server is single-threaded. Therefore, open a second
    // server instance on another port, which will serve the metadata about
    // available updates.
    $port = $this->findAvailablePort();
    $this->metadataServer = $this->instantiateServer($port);
    $code = <<<END
\$config['update.settings']['fetch']['url'] = 'http://localhost:{<span class="php-variable">$port</span>}/test-release-history';
END;
    // Ensure Package Manager logs Composer Stager's process output to a file
    // named for the current test.
    $log = $this->getDrupalRoot() . '/sites/simpletest/browser_output';
    @mkdir($log, recursive: TRUE);
    $this->assertDirectoryIsWritable($log);
    $log .= '/' . str_replace('\\', '_', static::class) . '-' . $this->name();
    if ($this->usesDataProvider()) {
      $log .= '-' . preg_replace('/[^a-z0-9]+/i', '_', (string) $this->dataName());
    }
    $code .= <<<END
\$config['package_manager.settings']['log'] = '{<span class="php-variable">$log</span>}-package_manager.log';
END;
    $this->writeSettings($code);
    // Install helpful modules.
    $this->installModules([
      'package_manager_test_api',
      'package_manager_test_event_logger',
      'package_manager_test_release_history',
    ]);
    // Confirm the server time out settings.
    // @see \Drupal\Tests\package_manager\Build\TemplateProjectTestBase::instantiateServer()
    $this->visit('/package-manager-test-api/check-setup');
    $this->getMink()
      ->assertSession()
      ->pageTextContains("max_execution_time=" . static::MAX_EXECUTION_TIME . ":set_time_limit-exists=no");
  }
  
  /**
   * Changes constraints for core packages to `*`.
   *
   * @param string[] $constraints
   *   A set of version constraints, like you'd find in the `require` or
   *   `require-dev` sections of `composer.json`. This array is modified by
   *   reference.
   */
  private static function unboundCoreConstraints(array &$constraints) : void {
    $names = preg_grep('/^drupal\\/core-?/', array_keys($constraints));
    foreach ($names as $name) {
      $constraints[$name] = '*';
    }
  }
  
  /**
   * Creates a Composer repository for all dependencies of the test project.
   *
   * We always reference third-party dependencies (i.e., any package that isn't
   * part of Drupal core) from the main project which is running this test.
   *
   * Packages that are part of Drupal core -- such as `drupal/core`,
   * `drupal/core-composer-scaffold`, and so on -- are expected to have been
   * copied into the workspace directory, so that we can modify them as needed.
   *
   * The file will be written to WORKSPACE_DIR/vendor.json.
   *
   * @param string[] $versions
   *   (optional) The versions of specific packages, keyed by package name.
   *   Versions of packages not in this array will be determined first by
   *   looking for a `version` key in the package's composer.json, then by
   *   calling \Composer\InstalledVersions::getPrettyVersion(). If none of that
   *   works, `dev-main` will be used as the package's version.
   */
  protected function createVendorRepository(array $versions = []) : void {
    $packages = [];
    $class_loaders = ClassLoader::getRegisteredLoaders();
    $workspace_dir = $this->getWorkspaceDirectory();
    $finder = Finder::create()->in([
      $this->getWorkspaceDrupalRoot() . '/core',
      "{$workspace_dir}/composer/Metapackage",
      "{$workspace_dir}/composer/Plugin",
      key($class_loaders),
    ])
      ->depth('< 3')
      ->files()
      ->name('composer.json');
    /** @var \Symfony\Component\Finder\SplFileInfo $file */
    foreach ($finder as $file) {
      $package_info = json_decode($file->getContents(), TRUE, flags: JSON_THROW_ON_ERROR);
      $name = $package_info['name'];
      $requirements = $package_info['require'] ?? [];
      // These polyfills are dependencies of some packages, but for reasons we
      // don't understand, they are not installed in code bases built on PHP
      // versions that are newer than the ones being polyfilled, which means we
      // won't be able to build our test project because these polyfills aren't
      // available in the local code base. Since we're guaranteed to be on PHP
      // 8.3 or later, no package should need to polyfill older versions.
      unset($requirements['symfony/polyfill-php72'], $requirements['symfony/polyfill-php73'], $requirements['symfony/polyfill-php74'], $requirements['symfony/polyfill-php80'], $requirements['symfony/polyfill-php81'], $requirements['symfony/polyfill-php82'], $requirements['symfony/polyfill-php83'], $requirements['symfony/polyfill-php84']);
      // If this package requires any Drupal core packages, ensure it allows
      // any version.
      self::unboundCoreConstraints($requirements);
      // In certain situations, like specific CI environments, auto_updates
      // might be required into the code base by Composer. This may cause it to
      // be added to the drupal/core-recommended metapackage, which can prevent
      // the test site from being built correctly, among other deleterious
      // effects. To prevent such shenanigans, always remove drupal/auto_updates
      // from drupal/core-recommended.
      if ($name === 'drupal/core-recommended') {
        unset($requirements['drupal/auto_updates']);
      }
      try {
        $version = $versions[$name] ?? $package_info['version'] ?? InstalledVersions::getPrettyVersion($name);
      } catch (\OutOfBoundsException) {
        $version = 'dev-main';
      }
      // Create a pared-down package definition that has just enough information
      // for Composer to install the package from the local copy: the name,
      // version, package type, source path ("dist" in Composer terminology),
      // and the autoload information, so that the classes provided by the
      // package will actually be loadable in the test site we're building.
      $path = $file->getPath();
      $packages[$name][$version] = [
        'name' => $name,
        'version' => $version,
        'type' => $package_info['type'] ?? 'library',
        // Disabling symlinks in the transport options doesn't seem to have an
        // effect, so we use the COMPOSER_MIRROR_PATH_REPOS environment
        // variable to force mirroring in ::createTestProject().
'dist' => [
          'type' => 'path',
          'url' => $path,
        ],
        'require' => $requirements,
        'autoload' => $package_info['autoload'] ?? [],
        'provide' => $package_info['provide'] ?? [],
        // Composer plugins are loaded and activated as early as possible, and
        // they must have a `class` key defined in their `extra` section, along
        // with a dependency on `composer-plugin-api` (plus any other real
        // runtime dependencies). This is also necessary for packages that ship
        // scaffold files, like Drupal core.
'extra' => $package_info['extra'] ?? [],
      ];
    }
    $data = json_encode([
      'packages' => $packages,
    ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
    file_put_contents($workspace_dir . '/vendor.json', $data);
  }
  
  /**
   * Runs a Composer command and returns its output.
   *
   * Always asserts that the command was executed successfully.
   *
   * @param string $command
   *   The command to execute, including the `composer` invocation.
   * @param string|null $working_dir
   *   (optional) A working directory relative to the workspace, within which to
   *   execute the command. Defaults to the workspace directory.
   * @param bool $json
   *   (optional) Whether to parse the command's output as JSON before returning
   *   it. Defaults to FALSE.
   *
   * @return mixed|string|null
   *   The command's output, optionally parsed as JSON.
   */
  protected function runComposer(string $command, ?string $working_dir = NULL, bool $json = FALSE) {
    $process = $this->executeCommand($command, $working_dir);
    $this->assertCommandSuccessful();
    $output = trim($process->getOutput());
    if ($json) {
      $output = json_decode($output, TRUE, flags: JSON_THROW_ON_ERROR);
    }
    return $output;
  }
  
  /**
   * Appends PHP code to the test site's settings.php.
   *
   * @param string $php
   *   The PHP code to append to the test site's settings.php.
   */
  protected function writeSettings(string $php) : void {
    // Ensure settings are writable, since this is the only way we can set
    // configuration values that aren't accessible in the UI.
    $file = $this->getWebRoot() . '/sites/default/settings.php';
    $this->assertFileExists($file);
    chmod(dirname($file), 0744);
    chmod($file, 0744);
    $this->assertFileIsWritable($file);
    $this->assertIsInt(file_put_contents($file, $php, FILE_APPEND));
  }
  
  /**
   * Installs modules in the UI.
   *
   * Assumes that a user with the appropriate permissions is logged in.
   *
   * @param string[] $modules
   *   The machine names of the modules to install.
   */
  protected function installModules(array $modules) : void {
    $mink = $this->getMink();
    $page = $mink->getSession()
      ->getPage();
    $assert_session = $mink->assertSession();
    $this->visit('/admin/modules');
    foreach ($modules as $module) {
      $page->checkField("modules[{$module}][enable]");
    }
    $page->pressButton('Install');
    // If there is a confirmation form warning about additional dependencies
    // or non-stable modules, submit it.
    $form_id = $assert_session->elementExists('css', 'input[type="hidden"][name="form_id"]')
      ->getValue();
    if (preg_match('/^system_modules_(experimental_|non_stable_)?confirm_form$/', $form_id)) {
      $page->pressButton('Continue');
      $assert_session->statusCodeEquals(200);
    }
  }
  
  /**
   * Copies a fixture directory to a temporary directory and returns its path.
   *
   * @param string $fixture_directory
   *   The fixture directory.
   *
   * @return string
   *   The temporary directory.
   */
  protected function copyFixtureToTempDirectory(string $fixture_directory) : string {
    $temp_directory = $this->getWorkspaceDirectory() . '/fixtures_temp_' . $this->randomMachineName(20);
    static::copyFixtureFilesTo($fixture_directory, $temp_directory);
    return $temp_directory;
  }
  
  /**
   * Asserts stage events were fired in a specific order.
   *
   * @param string $expected_stage_class
   *   The expected stage class for the events.
   * @param array|null $expected_events
   *   (optional) The expected stage events that should have been fired in the
   *   order in which they should have been fired. Events can be specified more
   *   that once if they will be fired multiple times. If there are no events
   *   specified all life cycle events from PreCreateEvent to PostApplyEvent
   *   will be asserted.
   * @param int $wait
   *   (optional) How many seconds to wait for the events to be fired. Defaults
   *   to 0.
   * @param string $message
   *   (optional) A message to display with the assertion.
   *
   * @see \Drupal\package_manager_test_event_logger\EventSubscriber\EventLogSubscriber::logEventInfo
   */
  protected function assertExpectedStageEventsFired(string $expected_stage_class, ?array $expected_events = NULL, int $wait = 0, string $message = '') : void {
    if ($expected_events === NULL) {
      $expected_events = EventLogSubscriber::getSubscribedEvents();
      // The event subscriber uses this event to ensure the log file is excluded
      // from Package Manager operations, but it's not relevant for our purposes
      // because it's not part of the stage life cycle.
      unset($expected_events[CollectPathsToExcludeEvent::class]);
      $expected_events = array_keys($expected_events);
    }
    $this->assertNotEmpty($expected_events);
    $log_file = $this->getWorkspaceDirectory() . '/project/' . EventLogSubscriber::LOG_FILE_NAME;
    $max_wait = time() + $wait;
    do {
      $this->assertFileIsReadable($log_file);
      $log_data = file_get_contents($log_file);
      $log_data = json_decode($log_data, TRUE, flags: JSON_THROW_ON_ERROR);
      // Filter out events logged by any other stage.
      $log_data = array_filter($log_data, fn(array $event): bool => $event['stage'] === $expected_stage_class);
      // If we've logged at least the expected number of events, stop waiting.
      // Break out of the loop and assert the expected events were logged.
      if (count($log_data) >= count($expected_events)) {
        break;

      }
      // Wait a bit before checking again.
      sleep(5);
    } while ($max_wait > time());
    $this->assertSame($expected_events, array_column($log_data, 'event'), $message);
  }
  
  /**
   * Visits the 'admin/reports/dblog' and selects Package Manager's change log.
   */
  private function visitPackageManagerChangeLog() : void {
    $mink = $this->getMink();
    $assert_session = $mink->assertSession();
    $page = $mink->getSession()
      ->getPage();
    $this->visit('/admin/reports/dblog');
    $assert_session->statusCodeEquals(200);
    $page->selectFieldOption('Type', 'package_manager_change_log');
    $page->pressButton('Filter');
    $assert_session->statusCodeEquals(200);
  }
  
  /**
   * Asserts changes requested during the stage life cycle were logged.
   *
   * This method specifically asserts changes that were *requested* (i.e.,
   * during the require phase) rather than changes that were actually applied.
   * The requested and applied changes may be exactly the same, or they may
   * differ (for example, if a secondary dependency was added or updated in the
   * stage directory).
   *
   * @param string[] $expected_requested_changes
   *   The expected requested changes.
   *
   * @see ::assertAppliedChangesWereLogged()
   * @see \Drupal\package_manager\EventSubscriber\ChangeLogger
   */
  protected function assertRequestedChangesWereLogged(array $expected_requested_changes) : void {
    $this->visitPackageManagerChangeLog();
    $assert_session = $this->getMink()
      ->assertSession();
    $assert_session->elementExists('css', 'a[href*="/admin/reports/dblog/event/"]:contains("Requested changes:")')
      ->click();
    array_walk($expected_requested_changes, $assert_session->pageTextContains(...));
  }
  
  /**
   * Asserts that changes applied during the stage life cycle were logged.
   *
   * This method specifically asserts changes that were *applied*, rather than
   * the changes that were merely requested. For example, if a package was
   * required into the stage and it added a secondary dependency, that change
   * will be considered one of the applied changes, not a requested change.
   *
   * @param string[] $expected_applied_changes
   *   The expected applied changes.
   *
   * @see ::assertRequestedChangesWereLogged()
   * @see \Drupal\package_manager\EventSubscriber\ChangeLogger
   */
  protected function assertAppliedChangesWereLogged(array $expected_applied_changes) : void {
    $this->visitPackageManagerChangeLog();
    $assert_session = $this->getMink()
      ->assertSession();
    $assert_session->elementExists('css', 'a[href*="/admin/reports/dblog/event/"]:contains("Applied changes:")')
      ->click();
    array_walk($expected_applied_changes, $assert_session->pageTextContains(...));
  }
  
  /**
   * Gets a /package-manager-test-api response.
   *
   * @param string $url
   *   The package manager test API URL to fetch.
   * @param array $query_data
   *   The query data.
   */
  protected function makePackageManagerTestApiRequest(string $url, array $query_data) : void {
    $url .= '?' . http_build_query($query_data);
    $this->visit($url);
    $session = $this->getMink()
      ->getSession();
    // Ensure test failures provide helpful debug output when there's a fatal
    // PHP error: don't use \Behat\Mink\WebAssert::statusCodeEquals().
    $message = sprintf("Error response: %s\n\nHeaders: %s\n\nServer error log: %s", $session->getPage()
      ->getContent(), var_export($session->getResponseHeaders(), TRUE), $this->serverErrorLog);
    $this->assertSame(200, $session->getStatusCode(), $message);
    // Sometimes we get a 200 response after a PHP timeout or OOM error, so we
    // also check the page content to ensure it's what we expect.
    $this->assertSame('Finished', $session->getPage()
      ->getText());
  }
  
  /**
   * {@inheritdoc}
   */
  public function copyCodebase(?\Iterator $iterator = NULL, $working_dir = NULL) : void {
    parent::copyCodebase($iterator, $working_dir);
    // Create a local Composer repository for all third-party dependencies and
    // core packages.
    $this->createVendorRepository();
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
AssertPreconditionsTrait::assertNoFailureMarker private static function Asserts that there is no failure marker present.
AssertPreconditionsTrait::failIfUnmetPreConditions protected static function Asserts universal test preconditions before any setup is done.
AssertPreconditionsTrait::getProjectRoot private static function Returns the absolute path of the project root.
AssertPreconditionsTrait::setUpBeforeClass public static function Invokes the test preconditions assertion before the first test is run.
BuildTestBase::$commandProcess private property The most recent command process.
BuildTestBase::$destroyBuild protected property Default to destroying build artifacts after a test finishes.
BuildTestBase::$hostName private static property Our native host name, used by PHP when it starts up the server.
BuildTestBase::$hostPort private property Port that will be tested.
BuildTestBase::$mink private property The Mink session manager.
BuildTestBase::$phpFinder private property The PHP executable finder.
BuildTestBase::$portLocks private property A list of ports used by the test.
BuildTestBase::$serverDocroot private property The docroot for the server process.
BuildTestBase::$serverProcess private property The process that&#039;s running the HTTP server.
BuildTestBase::$workspaceDir private property The working directory where this test will manipulate files.
BuildTestBase::assertCommandExitCode public function Asserts that the last command returned the specified exit code.
BuildTestBase::assertCommandOutputContains public function Assert that text is present in the output of the most recent command.
BuildTestBase::assertCommandSuccessful public function Asserts that the last command ran without error.
BuildTestBase::assertDrupalVisit public function Helper function to assert that the last visit was a Drupal site.
BuildTestBase::assertErrorOutputContains public function Assert that text is present in the error output of the most recent command.
BuildTestBase::assertErrorOutputNotContains public function Assert text is not present in the error output of the most recent command.
BuildTestBase::checkPortIsAvailable protected function Checks whether a port is available.
BuildTestBase::executeCommand public function Run a command.
BuildTestBase::findAvailablePort protected function Discover an available port number.
BuildTestBase::getCodebaseFinder public function Get a default Finder object for a Drupal codebase.
BuildTestBase::getComposerRoot public function Gets the path to the Composer root directory.
BuildTestBase::getDrupalRoot public function Get the root path of this Drupal codebase.
BuildTestBase::getDrupalRootStatic public static function Get the root path of this Drupal codebase.
BuildTestBase::getMink public function Get the Mink instance.
BuildTestBase::getPortNumber protected function Get the port number for requests.
BuildTestBase::getWorkingPath protected function Get the working directory within the workspace, creating if necessary.
BuildTestBase::getWorkingPathDrupalRoot public function Gets the working path for Drupal core.
BuildTestBase::getWorkspaceDirectory public function Full path to the workspace where this test can build.
BuildTestBase::getWorkspaceDrupalRoot public function Gets the path to Drupal root in the workspace directory.
BuildTestBase::initMink protected function Set up the Mink session manager.
BuildTestBase::standUpServer protected function Makes a local test server using PHP&#039;s internal HTTP server.
BuildTestBase::stopServer protected function Stop the HTTP server, zero out all necessary variables.
FixtureUtilityTrait::copyFixtureFilesTo protected static function Mirrors a fixture directory to the given path.
FixtureUtilityTrait::renameGitDirectories private static function Renames _git directories to .git.
FixtureUtilityTrait::renameInfoYmlFiles protected static function Renames all files that end with .info.yml.hide.
QuickStartTestBase::$adminPassword protected property Password of the admin account generated during install.
QuickStartTestBase::$adminUsername protected property User name of the admin account generated during install.
RandomGeneratorTrait::getRandomGenerator protected function Gets the random generator for the utility methods.
RandomGeneratorTrait::randomMachineName protected function Generates a unique random string containing letters and numbers.
RandomGeneratorTrait::randomObject public function Generates a random PHP object.
RandomGeneratorTrait::randomString public function Generates a pseudo-random string of ASCII characters of codes 32 to 126.
RequiresComposerTrait::requiresComposer public static function #[BeforeClass]
TemplateProjectTestBase::$metadataServer private property A secondary server instance, to serve XML metadata about available updates.
TemplateProjectTestBase::$serverErrorLog private property All output that the PHP web server logs to the error buffer.
TemplateProjectTestBase::$webRoot private property The web root of the test site, relative to the workspace directory.
TemplateProjectTestBase::addRepository protected function Adds a path repository to the test site.
TemplateProjectTestBase::assertAppliedChangesWereLogged protected function Asserts that changes applied during the stage life cycle were logged.
TemplateProjectTestBase::assertExpectedStageEventsFired protected function Asserts stage events were fired in a specific order.
TemplateProjectTestBase::assertRequestedChangesWereLogged protected function Asserts changes requested during the stage life cycle were logged.
TemplateProjectTestBase::copyCodebase public function Copy the current working codebase into a workspace. Overrides BuildTestBase::copyCodebase
TemplateProjectTestBase::copyFixtureToTempDirectory protected function Copies a fixture directory to a temporary directory and returns its path.
TemplateProjectTestBase::createTestProject protected function Creates a test project from a given template and installs Drupal.
TemplateProjectTestBase::createVendorRepository protected function Creates a Composer repository for all dependencies of the test project.
TemplateProjectTestBase::formLogin public function Helper that uses Drupal&#039;s user/login form to log in. Overrides QuickStartTestBase::formLogin
TemplateProjectTestBase::getWebRoot protected function Returns the full path to the test site&#039;s document root.
TemplateProjectTestBase::installModules protected function Installs modules in the UI.
TemplateProjectTestBase::installQuickStart public function Install a Drupal site using the quick start feature. Overrides QuickStartTestBase::installQuickStart
TemplateProjectTestBase::instantiateServer protected function Do the work of making a server process. Overrides BuildTestBase::instantiateServer
TemplateProjectTestBase::makePackageManagerTestApiRequest protected function Gets a /package-manager-test-api response.
TemplateProjectTestBase::MAX_EXECUTION_TIME protected constant The PHP web server&#039;s max_execution_time value.
TemplateProjectTestBase::providerTemplate public static function Data provider for tests which use all the core project templates.
TemplateProjectTestBase::runComposer protected function Runs a Composer command and returns its output.
TemplateProjectTestBase::setReleaseMetadata protected function Prepares the test site to serve an XML feed of available release metadata.
TemplateProjectTestBase::setUp protected function Overrides BuildTestBase::setUp
TemplateProjectTestBase::setUpstreamCoreVersion protected function Sets the version of Drupal core to which the test site will be updated.
TemplateProjectTestBase::tearDown protected function Invokes the test preconditions assertion after each test run. Overrides AssertPreconditionsTrait::tearDown
TemplateProjectTestBase::unboundCoreConstraints private static function Changes constraints for core packages to `*`.
TemplateProjectTestBase::visit public function Visit a URI on the HTTP server. Overrides BuildTestBase::visit
TemplateProjectTestBase::visitPackageManagerChangeLog private function Visits the &#039;admin/reports/dblog&#039; and selects Package Manager&#039;s change log.
TemplateProjectTestBase::writeSettings protected function Appends PHP code to the test site&#039;s settings.php.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.