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(); } } }