123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- <?php
- namespace Drupal\Core\Test;
- use Drupal\Component\FileSystem\FileSystem;
- use Drupal\Core\Database\ConnectionNotDefinedException;
- use Drupal\Core\Database\Database;
- /**
- * Provides helper methods for interacting with the fixture database.
- */
- class TestDatabase {
- /**
- * A random number used to ensure that test fixtures are unique to each test
- * method.
- *
- * @var int
- */
- protected $lockId;
- /**
- * The test database prefix.
- *
- * @var string
- */
- protected $databasePrefix;
- /**
- * Returns the database connection to the site running Simpletest.
- *
- * @return \Drupal\Core\Database\Connection
- * The database connection to use for inserting assertions.
- *
- * @see \Drupal\simpletest\TestBase::prepareEnvironment()
- */
- public static function getConnection() {
- // Check whether there is a test runner connection.
- // @see run-tests.sh
- // @todo Convert Simpletest UI runner to create + use this connection, too.
- try {
- $connection = Database::getConnection('default', 'test-runner');
- }
- catch (ConnectionNotDefinedException $e) {
- // Check whether there is a backup of the original default connection.
- // @see TestBase::prepareEnvironment()
- try {
- $connection = Database::getConnection('default', 'simpletest_original_default');
- }
- catch (ConnectionNotDefinedException $e) {
- // If TestBase::prepareEnvironment() or TestBase::restoreEnvironment()
- // failed, the test-specific database connection does not exist
- // yet/anymore, so fall back to the default of the (UI) test runner.
- $connection = Database::getConnection('default', 'default');
- }
- }
- return $connection;
- }
- /**
- * TestDatabase constructor.
- *
- * @param string|null $db_prefix
- * If not provided a new test lock is generated.
- * @param bool $create_lock
- * (optional) Whether or not to create a lock file. Defaults to FALSE. If
- * the environment variable RUN_TESTS_CONCURRENCY is greater than 1 it will
- * be overridden to TRUE regardless of its initial value.
- *
- * @throws \InvalidArgumentException
- * Thrown when $db_prefix does not match the regular expression.
- */
- public function __construct($db_prefix = NULL, $create_lock = FALSE) {
- if ($db_prefix === NULL) {
- $this->lockId = $this->getTestLock($create_lock);
- $this->databasePrefix = 'test' . $this->lockId;
- }
- else {
- $this->databasePrefix = $db_prefix;
- // It is possible that we're running a test inside a test. In which case
- // $db_prefix will be something like test12345678test90123456 and the
- // generated lock ID for the running test method would be 90123456.
- preg_match('/test(\d+)$/', $db_prefix, $matches);
- if (!isset($matches[1])) {
- throw new \InvalidArgumentException("Invalid database prefix: $db_prefix");
- }
- $this->lockId = $matches[1];
- }
- }
- /**
- * Gets the relative path to the test site directory.
- *
- * @return string
- * The relative path to the test site directory.
- */
- public function getTestSitePath() {
- return 'sites/simpletest/' . $this->lockId;
- }
- /**
- * Gets the test database prefix.
- *
- * @return string
- * The test database prefix.
- */
- public function getDatabasePrefix() {
- return $this->databasePrefix;
- }
- /**
- * Generates a unique lock ID for the test method.
- *
- * @param bool $create_lock
- * (optional) Whether or not to create a lock file. Defaults to FALSE.
- *
- * @return int
- * The unique lock ID for the test method.
- */
- protected function getTestLock($create_lock = FALSE) {
- // There is a risk that the generated random number is a duplicate. This
- // would cause different tests to try to use the same database prefix.
- // Therefore, if running with a concurrency of greater than 1, we need to
- // create a lock.
- if (getenv('RUN_TESTS_CONCURRENCY') > 1) {
- $create_lock = TRUE;
- }
- do {
- $lock_id = mt_rand(10000000, 99999999);
- if ($create_lock && @symlink(__FILE__, $this->getLockFile($lock_id)) === FALSE) {
- // If we can't create a symlink, the lock ID is in use. Generate another
- // one. Symlinks are used because they are atomic and reliable.
- $lock_id = NULL;
- }
- } while ($lock_id === NULL);
- return $lock_id;
- }
- /**
- * Releases a lock.
- *
- * @return bool
- * TRUE if successful, FALSE if not.
- */
- public function releaseLock() {
- return unlink($this->getLockFile($this->lockId));
- }
- /**
- * Releases all test locks.
- *
- * This should only be called once all the test fixtures have been cleaned up.
- */
- public static function releaseAllTestLocks() {
- $tmp = FileSystem::getOsTemporaryDirectory();
- $dir = dir($tmp);
- while (($entry = $dir->read()) !== FALSE) {
- if ($entry === '.' || $entry === '..') {
- continue;
- }
- $entry_path = $tmp . '/' . $entry;
- if (preg_match('/^test_\d+/', $entry) && is_link($entry_path)) {
- unlink($entry_path);
- }
- }
- }
- /**
- * Gets the lock file path.
- *
- * @param int $lock_id
- * The test method lock ID.
- *
- * @return string
- * A file path to the symbolic link that prevents the lock ID being re-used.
- */
- protected function getLockFile($lock_id) {
- return FileSystem::getOsTemporaryDirectory() . '/test_' . $lock_id;
- }
- /**
- * Store an assertion from outside the testing context.
- *
- * This is useful for inserting assertions that can only be recorded after
- * the test case has been destroyed, such as PHP fatal errors. The caller
- * information is not automatically gathered since the caller is most likely
- * inserting the assertion on behalf of other code. In all other respects
- * the method behaves just like \Drupal\simpletest\TestBase::assert() in terms
- * of storing the assertion.
- *
- * @param string $test_id
- * The test ID to which the assertion relates.
- * @param string $test_class
- * The test class to store an assertion for.
- * @param bool|string $status
- * A boolean or a string of 'pass' or 'fail'. TRUE means 'pass'.
- * @param string $message
- * The assertion message.
- * @param string $group
- * The assertion message group.
- * @param array $caller
- * The an array containing the keys 'file' and 'line' that represent the
- * file and line number of that file that is responsible for the assertion.
- *
- * @return int
- * Message ID of the stored assertion.
- *
- * @internal
- */
- public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = []) {
- // Convert boolean status to string status.
- if (is_bool($status)) {
- $status = $status ? 'pass' : 'fail';
- }
- $caller += [
- 'function' => 'Unknown',
- 'line' => 0,
- 'file' => 'Unknown',
- ];
- $assertion = [
- 'test_id' => $test_id,
- 'test_class' => $test_class,
- 'status' => $status,
- 'message' => $message,
- 'message_group' => $group,
- 'function' => $caller['function'],
- 'line' => $caller['line'],
- 'file' => $caller['file'],
- ];
- return static::getConnection()
- ->insert('simpletest')
- ->fields($assertion)
- ->execute();
- }
- /**
- * Get information about the last test that ran given a test ID.
- *
- * @param int $test_id
- * The test ID to get the last test from.
- *
- * @return array
- * Associative array containing the last database prefix used and the
- * last test class that ran.
- *
- * @internal
- */
- public static function lastTestGet($test_id) {
- $connection = static::getConnection();
- // Define a subquery to identify the latest 'message_id' given the
- // $test_id.
- $max_message_id_subquery = $connection
- ->select('simpletest', 'sub')
- ->condition('test_id', $test_id);
- $max_message_id_subquery->addExpression('MAX(message_id)', 'max_message_id');
- // Run a select query to return 'last_prefix' from {simpletest_test_id} and
- // 'test_class' from {simpletest}.
- $select = $connection->select($max_message_id_subquery, 'st_sub');
- $select->join('simpletest', 'st', 'st.message_id = st_sub.max_message_id');
- $select->join('simpletest_test_id', 'sttid', 'st.test_id = sttid.test_id');
- $select->addField('sttid', 'last_prefix');
- $select->addField('st', 'test_class');
- return $select->execute()->fetchAssoc();
- }
- /**
- * Reads the error log and reports any errors as assertion failures.
- *
- * The errors in the log should only be fatal errors since any other errors
- * will have been recorded by the error handler.
- *
- * @param int $test_id
- * The test ID to which the log relates.
- * @param string $test_class
- * The test class to which the log relates.
- *
- * @return bool
- * Whether any fatal errors were found.
- *
- * @internal
- */
- public function logRead($test_id, $test_class) {
- $log = DRUPAL_ROOT . '/' . $this->getTestSitePath() . '/error.log';
- $found = FALSE;
- if (file_exists($log)) {
- foreach (file($log) as $line) {
- if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
- // Parse PHP fatal errors for example: PHP Fatal error: Call to
- // undefined function break_me() in /path/to/file.php on line 17
- $caller = [
- 'line' => $match[4],
- 'file' => $match[3],
- ];
- static::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
- }
- else {
- // Unknown format, place the entire message in the log.
- static::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error');
- }
- $found = TRUE;
- }
- }
- return $found;
- }
- /**
- * Defines the database schema for run-tests.sh and simpletest module.
- *
- * @return array
- * Array suitable for use in a hook_schema() implementation.
- *
- * @internal
- */
- public static function testingSchema() {
- $schema['simpletest'] = [
- 'description' => 'Stores simpletest messages',
- 'fields' => [
- 'message_id' => [
- 'type' => 'serial',
- 'not null' => TRUE,
- 'description' => 'Primary Key: Unique simpletest message ID.',
- ],
- 'test_id' => [
- 'type' => 'int',
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => 'Test ID, messages belonging to the same ID are reported together',
- ],
- 'test_class' => [
- 'type' => 'varchar_ascii',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'The name of the class that created this message.',
- ],
- 'status' => [
- 'type' => 'varchar',
- 'length' => 9,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Message status. Core understands pass, fail, exception.',
- ],
- 'message' => [
- 'type' => 'text',
- 'not null' => TRUE,
- 'description' => 'The message itself.',
- ],
- 'message_group' => [
- 'type' => 'varchar_ascii',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'The message group this message belongs to. For example: warning, browser, user.',
- ],
- 'function' => [
- 'type' => 'varchar_ascii',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Name of the assertion function or method that created this message.',
- ],
- 'line' => [
- 'type' => 'int',
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => 'Line number on which the function is called.',
- ],
- 'file' => [
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Name of the file where the function is called.',
- ],
- ],
- 'primary key' => ['message_id'],
- 'indexes' => [
- 'reporter' => ['test_class', 'message_id'],
- ],
- ];
- $schema['simpletest_test_id'] = [
- 'description' => 'Stores simpletest test IDs, used to auto-increment the test ID so that a fresh test ID is used.',
- 'fields' => [
- 'test_id' => [
- 'type' => 'serial',
- 'not null' => TRUE,
- 'description' => 'Primary Key: Unique simpletest ID used to group test results together. Each time a set of tests
- are run a new test ID is used.',
- ],
- 'last_prefix' => [
- 'type' => 'varchar',
- 'length' => 60,
- 'not null' => FALSE,
- 'default' => '',
- 'description' => 'The last database prefix used during testing.',
- ],
- ],
- 'primary key' => ['test_id'],
- ];
- return $schema;
- }
- /**
- * Inserts the parsed PHPUnit results into {simpletest}.
- *
- * @param array[] $phpunit_results
- * An array of test results, as returned from
- * \Drupal\Core\Test\JUnitConverter::xmlToRows(). These results are in a
- * form suitable for inserting into the {simpletest} table of the test
- * results database.
- *
- * @internal
- */
- public static function processPhpUnitResults($phpunit_results) {
- if ($phpunit_results) {
- $query = static::getConnection()
- ->insert('simpletest')
- ->fields(array_keys($phpunit_results[0]));
- foreach ($phpunit_results as $result) {
- $query->values($result);
- }
- $query->execute();
- }
- }
- }
|