You are here

abstract class UpdateTestBase in Automatic Updates 8.2

Base class for tests that perform in-place updates.

Hierarchy

Expanded class hierarchy of UpdateTestBase

File

tests/src/Build/UpdateTestBase.php, line 14

Namespace

Drupal\Tests\automatic_updates\Build
View source
abstract class UpdateTestBase extends QuickStartTestBase {
  use LocalPackagesTrait {
    getPackagePath as traitGetPackagePath;
    copyPackage as traitCopyPackage;
  }
  use SettingsTrait;

  /**
   * A secondary server instance, to serve XML metadata about available updates.
   *
   * @var \Symfony\Component\Process\Process
   */
  private $metadataServer;

  /**
   * The test site's document root, relative to the workspace directory.
   *
   * @var string
   *
   * @see ::createTestSite()
   */
  private $webRoot;

  /**
   * {@inheritdoc}
   */
  protected function tearDown() : void {
    if ($this->metadataServer) {
      $this->metadataServer
        ->stop();
    }
    parent::tearDown();
  }

  /**
   * {@inheritdoc}
   */
  protected function copyPackage(string $source_dir, string $destination_dir = NULL) : string {
    return $this
      ->traitCopyPackage($source_dir, $destination_dir ?: $this
      ->getWorkspaceDirectory());
  }

  /**
   * {@inheritdoc}
   */
  protected function getPackagePath(array $package) : string {
    if ($package['name'] === 'drupal/core') {
      return 'core';
    }
    [
      $vendor,
      $name,
    ] = explode('/', $package['name']);

    // Assume any contributed module is in modules/contrib/$name.
    if ($vendor === 'drupal' && $package['type'] === 'drupal-module') {
      return implode(DIRECTORY_SEPARATOR, [
        'modules',
        'contrib',
        $name,
      ]);
    }
    return $this
      ->traitGetPackagePath($package);
  }

  /**
   * 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() . DIRECTORY_SEPARATOR . $this->webRoot;
  }

  /**
   * 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\automatic_updates_test\TestController::metadata()
   */
  protected function setReleaseMetadata(array $xml_map) : void {
    $xml_map = var_export($xml_map, TRUE);
    $code = <<<END
\$config['update_test.settings']['xml_map'] = {<span class="php-variable">$xml_map</span>};
END;

    // When checking for updates, we need to be able to make sub-requests, but
    // the built-in PHP server is single-threaded. Therefore, if needed, open a
    // second server instance on another port, which will serve the metadata
    // about available updates.
    if (empty($this->metadataServer)) {
      $port = $this
        ->findAvailablePort();
      $this->metadataServer = $this
        ->instantiateServer($port, $this->webRoot);
      $code .= <<<END
\$config['update.settings']['fetch']['url'] = 'http://localhost:{<span class="php-variable">$port</span>}/automatic-update-test';
END;
    }
    $this
      ->addSettings($code, $this
      ->getWebRoot());
  }

  /**
   * {@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) {
    parent::formLogin($username, $password, $working_dir ?: $this->webRoot);
  }

  /**
   * {@inheritdoc}
   */
  public function installQuickStart($profile, $working_dir = NULL) {
    parent::installQuickStart($profile, $working_dir ?: $this->webRoot);

    // Always allow test modules to be installed in the UI and, for easier
    // debugging, always display errors in their dubious glory.
    $php = <<<END
\$settings['extension_discovery_scan_tests'] = TRUE;
\$config['system.logging']['error_level'] = 'verbose';
END;
    $this
      ->addSettings($php, $this
      ->getWebRoot());
  }

  /**
   * Uses our already-installed dependencies to build a test site to update.
   *
   * @param string $template
   *   The template project from which to build the test site. Can be
   *   'drupal/recommended-project' or 'drupal/legacy-project'.
   */
  protected function createTestSite(string $template) : void {

    // Create the test site using one of the core project templates, but don't
    // install dependencies just yet.
    $template_dir = implode(DIRECTORY_SEPARATOR, [
      $this
        ->getDrupalRoot(),
      'composer',
      'Template',
    ]);
    $recommended_template = $this
      ->createPathRepository($template_dir . DIRECTORY_SEPARATOR . 'RecommendedProject');
    $legacy_template = $this
      ->createPathRepository($template_dir . DIRECTORY_SEPARATOR . 'LegacyProject');
    $dir = $this
      ->getWorkspaceDirectory();
    $command = sprintf("composer create-project %s %s --no-install --stability dev --repository '%s' --repository '%s'", $template, $dir, Json::encode($recommended_template), Json::encode($legacy_template));
    $this
      ->executeCommand($command);
    $this
      ->assertCommandSuccessful();
    $composer = $dir . DIRECTORY_SEPARATOR . 'composer.json';
    $data = $this
      ->readJson($composer);

    // Allow the test to configure the test site as necessary.
    $data = $this
      ->getInitialConfiguration($data);

    // We need to know the path of the web root, relative to the project root,
    // in order to install Drupal or visit the test site at all. Luckily, both
    // template projects define this because the scaffold plugin needs to know
    // it as well.
    // @see ::visit()
    // @see ::formLogin()
    // @see ::installQuickStart()
    $this->webRoot = $data['extra']['drupal-scaffold']['locations']['web-root'];

    // Update the test site's composer.json and install dependencies.
    $this
      ->writeJson($composer, $data);
    $this
      ->executeCommand('composer install --no-dev');
    $this
      ->assertCommandSuccessful();
  }

  /**
   * Returns the initial data to write to the test site's composer.json.
   *
   * This configuration will be used to build the pre-update test site.
   *
   * @param array $data
   *   The current contents of the test site's composer.json.
   *
   * @return array
   *   The data that should be written to the test site's composer.json.
   */
  protected function getInitialConfiguration(array $data) : array {
    $drupal_root = $this
      ->getDrupalRoot();
    $core_composer_dir = $drupal_root . DIRECTORY_SEPARATOR . 'composer';
    $repositories = [];

    // Add all the metapackages that are provided by Drupal core.
    $metapackage_dir = $core_composer_dir . DIRECTORY_SEPARATOR . 'Metapackage';
    $repositories['drupal/core-recommended'] = $this
      ->createPathRepository($metapackage_dir . DIRECTORY_SEPARATOR . 'CoreRecommended');
    $repositories['drupal/core-dev'] = $this
      ->createPathRepository($metapackage_dir . DIRECTORY_SEPARATOR . 'DevDependencies');

    // Add all the Composer plugins that are provided by Drupal core.
    $plugin_dir = $core_composer_dir . DIRECTORY_SEPARATOR . 'Plugin';
    $repositories['drupal/core-project-message'] = $this
      ->createPathRepository($plugin_dir . DIRECTORY_SEPARATOR . 'ProjectMessage');
    $repositories['drupal/core-composer-scaffold'] = $this
      ->createPathRepository($plugin_dir . DIRECTORY_SEPARATOR . 'Scaffold');
    $repositories['drupal/core-vendor-hardening'] = $this
      ->createPathRepository($plugin_dir . DIRECTORY_SEPARATOR . 'VendorHardening');
    $repositories = array_merge($repositories, $this
      ->getLocalPackageRepositories($drupal_root));

    // To ensure the test runs entirely offline, don't allow Composer to contact
    // Packagist.
    $repositories['packagist.org'] = FALSE;
    $repositories['drupal/automatic_updates'] = [
      'type' => 'path',
      'url' => __DIR__ . '/../../..',
    ];

    // Use whatever the current branch of automatic_updates is.
    $data['require']['drupal/automatic_updates'] = '*';
    $data['repositories'] = $repositories;

    // Since Drupal 9 requires PHP 7.3 or later, these packages are probably
    // not installed, which can cause trouble during dependency resolution.
    // The drupal/drupal package (defined with a composer.json that is part
    // of core's repository) replaces these, so we need to emulate that here.
    $data['replace']['symfony/polyfill-php72'] = '*';
    $data['replace']['symfony/polyfill-php73'] = '*';
    return $data;
  }

  /**
   * Asserts that a specific version of Drupal core is running.
   *
   * Assumes that a user with permission to view the status report is logged in.
   *
   * @param string $expected_version
   *   The version of core that should be running.
   */
  protected function assertCoreVersion(string $expected_version) : void {
    $this
      ->visit('/admin/reports/status');
    $item = $this
      ->getMink()
      ->assertSession()
      ->elementExists('css', 'h3:contains("Drupal Version")')
      ->getParent()
      ->getText();
    $this
      ->assertStringContainsString($expected_version, $item);
  }

  /**
   * 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');
    $form_id = $assert_session
      ->elementExists('css', 'input[type="hidden"][name="form_id"]')
      ->getValue();
    if ($form_id === 'system_modules_confirm_form') {
      $page
        ->pressButton('Continue');
      $assert_session
        ->statusCodeEquals(200);
    }
  }

  /**
   * Checks for available updates.
   *
   * Assumes that a user with the appropriate access is logged in.
   */
  protected function checkForUpdates() : void {
    $this
      ->visit('/admin/reports/updates');
    $this
      ->getMink()
      ->getSession()
      ->getPage()
      ->clickLink('Check manually');
    $this
      ->waitForBatchJob();
  }

  /**
   * Waits for an active batch job to finish.
   */
  protected function waitForBatchJob() : void {
    $refresh = $this
      ->getMink()
      ->getSession()
      ->getPage()
      ->find('css', 'meta[http-equiv="Refresh"], meta[http-equiv="refresh"]');
    if ($refresh) {

      // Parse the content attribute of the meta tag for the format:
      // "[delay]: URL=[page_to_redirect_to]".
      if (preg_match('/\\d+;\\s*URL=\'?(?<url>[^\']*)/i', $refresh
        ->getAttribute('content'), $match)) {
        $url = Html::decodeEntities($match['url']);
        $this
          ->visit($url);
        $this
          ->waitForBatchJob();
      }
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
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::$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'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::checkPortIsAvailable protected function Checks whether a port is available.
BuildTestBase::copyCodebase public function Copy the current working codebase into a workspace.
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::getDrupalRoot protected 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::getWorkspaceDirectory public function Full path to the workspace where this test can build.
BuildTestBase::initMink protected function Set up the Mink session manager.
BuildTestBase::instantiateServer protected function Do the work of making a server process.
BuildTestBase::setUp protected function
BuildTestBase::setUpBeforeClass public static function
BuildTestBase::standUpServer protected function Makes a local test server using PHP's internal HTTP server.
BuildTestBase::stopServer protected function Stop the HTTP server, zero out all necessary variables.
ExternalCommandRequirementsTrait::$existingCommands private static property A list of existing external commands we've already discovered.
ExternalCommandRequirementsTrait::checkClassCommandRequirements private static function Checks whether required external commands are available per test class.
ExternalCommandRequirementsTrait::checkExternalCommandRequirements private static function Checks missing external command requirements.
ExternalCommandRequirementsTrait::checkMethodCommandRequirements private static function Checks whether required external commands are available per method.
ExternalCommandRequirementsTrait::externalCommandIsAvailable private static function Determine if an external command is available. 3
JsonTrait::readJson protected function Reads JSON data from a file and returns it as an array.
JsonTrait::writeJson protected function Writes an array of data to a file as JSON.
LocalPackagesTrait::$copiedPackages private property The paths of temporary copies of packages.
LocalPackagesTrait::alterPackage protected function Alters a package's composer.json file.
LocalPackagesTrait::copyPackage protected function Copies a package's entire directory to another location. Aliased as: traitCopyPackage
LocalPackagesTrait::createPathRepository protected function Defines a local path repository for a given path.
LocalPackagesTrait::deleteCopiedPackages protected function Deletes all copied packages.
LocalPackagesTrait::getLocalPackageRepositories protected function Generates local path repositories for a set of installed packages.
LocalPackagesTrait::getPackagePath protected function Returns the path of an installed package, relative to composer.json. Aliased as: traitGetPackagePath
LocalPackagesTrait::getPackagesFromLockFile private function Reads all package information from a composer.lock file.
PhpunitCompatibilityTrait::getMock Deprecated public function Returns a mock object for the specified class using the available method.
PhpunitCompatibilityTrait::setExpectedException Deprecated public function Compatibility layer for PHPUnit 6 to support PHPUnit 4 code.
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.
SettingsTrait::addSettings protected function Appends some PHP code to settings.php.
SettingsTrait::makeSettingsWritable private function Ensures that settings.php is writable.
UpdateTestBase::$metadataServer private property A secondary server instance, to serve XML metadata about available updates.
UpdateTestBase::$webRoot private property The test site's document root, relative to the workspace directory.
UpdateTestBase::assertCoreVersion protected function Asserts that a specific version of Drupal core is running.
UpdateTestBase::checkForUpdates protected function Checks for available updates.
UpdateTestBase::copyPackage protected function
UpdateTestBase::createTestSite protected function Uses our already-installed dependencies to build a test site to update. 1
UpdateTestBase::formLogin public function Helper that uses Drupal's user/login form to log in. Overrides QuickStartTestBase::formLogin
UpdateTestBase::getInitialConfiguration protected function Returns the initial data to write to the test site's composer.json.
UpdateTestBase::getPackagePath protected function
UpdateTestBase::getWebRoot protected function Returns the full path to the test site's document root.
UpdateTestBase::installModules protected function Installs modules in the UI.
UpdateTestBase::installQuickStart public function Install a Drupal site using the quick start feature. Overrides QuickStartTestBase::installQuickStart
UpdateTestBase::setReleaseMetadata protected function Prepares the test site to serve an XML feed of available release metadata.
UpdateTestBase::tearDown protected function Overrides BuildTestBase::tearDown 1
UpdateTestBase::visit public function Visit a URI on the HTTP server. Overrides BuildTestBase::visit
UpdateTestBase::waitForBatchJob protected function Waits for an active batch job to finish.