Unverified Commit 0041ba08 authored by larowlan's avatar larowlan
Browse files

Issue #3074043 by Mile23, mondrake, voleger: Move simpletest.module DB-related...

Issue #3074043 by Mile23, mondrake, voleger: Move simpletest.module DB-related functions to TestDatabase, deprecate
parent 00b3ed64
......@@ -179,4 +179,226 @@ 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
* Array containing the last database prefix used and the last test class
* that ran.
*
* @internal
*/
public static function lastTestGet($test_id) {
$connection = static::getConnection();
$last_prefix = $connection
->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, [
':test_id' => $test_id,
])
->fetchField();
$last_test_class = $connection
->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, [
':test_id' => $test_id,
])
->fetchField();
return [$last_prefix, $last_test_class];
}
/**
* 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;
}
}
......@@ -7,6 +7,7 @@
use Drupal\Component\Utility\Environment;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\Test\TestDatabase;
use PHPUnit\Framework\TestCase;
/**
......@@ -78,92 +79,7 @@ function simpletest_requirements($phase) {
* Implements hook_schema().
*/
function simpletest_schema() {
$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;
return TestDatabase::testingSchema();
}
/**
......
......@@ -490,8 +490,8 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
// Retrieve the last database prefix used for testing and the last test
// class that was run from. Use the information to read the lgo file
// in case any fatal errors caused the test to crash.
list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
simpletest_log_read($test_id, $last_prefix, $last_test_class);
list($last_prefix, $last_test_class) = TestDatabase::lastTestGet($test_id);
(new TestDatabase($last_prefix))->logRead($test_id, $last_test_class);
\Drupal::messenger()->addError(t('The test run did not successfully finish.'));
\Drupal::messenger()->addWarning(t('Use the <em>Clean environment</em> button to clean-up temporary files and tables.'));
......@@ -507,19 +507,15 @@ function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
* @return array
* Array containing the last database prefix used and the last test class
* that ran.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\TestDatabase::lastTestGet() instead.
*
* @see https://www.drupal.org/node/3075252
*/
function simpletest_last_test_get($test_id) {
$last_prefix = TestDatabase::getConnection()
->queryRange('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, [
':test_id' => $test_id,
])
->fetchField();
$last_test_class = TestDatabase::getConnection()
->queryRange('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, [
':test_id' => $test_id,
])
->fetchField();
return [$last_prefix, $last_test_class];
@trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDatabase::lastTestGet() instead. See https://www.drupal.org/node/3075252', E_USER_DEPRECATED);
return TestDatabase::lastTestGet($test_id);
}
/**
......@@ -537,30 +533,16 @@ function simpletest_last_test_get($test_id) {
*
* @return bool
* Whether any fatal errors were found.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\TestDatabase::logRead() instead.
*
* @see https://www.drupal.org/node/3075252
*/
function simpletest_log_read($test_id, $database_prefix, $test_class) {
@trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDatabase::logRead() instead. See https://www.drupal.org/node/3075252', E_USER_DEPRECATED);
$test_db = new TestDatabase($database_prefix);
$log = DRUPAL_ROOT . '/' . $test_db->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],
];
simpletest_insert_assert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
}
else {
// Unknown format, place the entire message in the log.
simpletest_insert_assert($test_id, $test_class, FALSE, $line, 'Fatal error');
}
$found = TRUE;
}
}
return $found;
return $test_db->logRead($test_id, $test_class);
}
/**
......@@ -589,34 +571,15 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) {
*
* @return
* Message ID of the stored assertion.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\Core\Test\TestDatabase::insertAssert() instead.
*
* @see https://www.drupal.org/node/3075252
*/
function simpletest_insert_assert($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 TestDatabase::getConnection()
->insert('simpletest')
->fields($assertion)
->execute();
@trigger_error(__FUNCTION__ . ' is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDatabase::insertAssert() instead. See https://www.drupal.org/node/3075252', E_USER_DEPRECATED);
TestDatabase::insertAssert($test_id, $test_class, $status, $message, $group, $caller);
}
/**
......
......@@ -1252,7 +1252,7 @@ private function restoreEnvironment() {
// In case a fatal error occurred that was not in the test process read the
// log to pick up any fatal errors.
simpletest_log_read($this->testId, $this->databasePrefix, get_class($this));
(new TestDatabase($this->databasePrefix))->logRead($this->testId, get_class($this));
// Restore original dependency injection container.
$this->container = $this->originalContainer;
......
......@@ -655,8 +655,7 @@ function simpletest_script_setup_database($new = FALSE) {
exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
}
if ($new && $sqlite) {
require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
foreach (simpletest_schema() as $name => $table_spec) {
foreach (TestDatabase::testingSchema() as $name => $table_spec) {
try {
$table_exists = $schema->tableExists($name);
if (empty($args['keep-results-table']) && $table_exists) {
......@@ -752,14 +751,14 @@ function simpletest_script_execute_batch($test_classes) {
// @see https://www.drupal.org/node/2780087
$total_status = max(SIMPLETEST_SCRIPT_EXIT_FAILURE, $total_status);
// Insert a fail for xml results.
simpletest_insert_assert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
TestDatabase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
// Ensure that an error line is displayed for the class.
simpletest_script_reporter_display_summary(
$child['class'],
['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]
);
if ($args['die-on-fail']) {
list($db_prefix) = simpletest_last_test_get($child['test_id']);
list($db_prefix) = TestDatabase::lastTestGet($child['test_id']);
$test_db = new TestDatabase($db_prefix);
$test_directory = $test_db->getTestSitePath();
echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $db_prefix . ' and config directories in ' . $test_directory . "\n";
......@@ -910,7 +909,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
}
// Retrieve the last database prefix used for testing.
try {
list($db_prefix) = simpletest_last_test_get($test_id);
list($db_prefix) = TestDatabase::lastTestGet($test_id);
}
catch (Exception $e) {
echo (string) $e;
......@@ -931,7 +930,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
// Read the log file in case any fatal errors caused the test to crash.
try {
simpletest_log_read($test_id, $db_prefix, $test_class);
(new TestDatabase($db_prefix))->logRead($test_id, $last_test_class);
}
catch (Exception $e) {
echo (string) $e;
......
......@@ -7,6 +7,9 @@
/**
* @coversDefaultClass \Drupal\Core\Test\TestDatabase
*
* @group Test
* @group simpletest
* @group Template
*/
class TestDatabaseTest extends UnitTestCase {
......@@ -43,4 +46,29 @@ public function providerTestConstructor() {
];
}
/**
* Verify that a test lock is generated if there is no provided prefix.
*
* @covers ::__construct
*/
public function testConstructorNullPrefix() {
// We use a stub class here because we can't mock getTestLock() so that it's
// available before the constructor is called.
$test_db = new TestTestDatabase(NULL);
$this->assertEquals('test23', $test_db->getDatabasePrefix());
$this->assertEquals('sites/simpletest/23', $test_db->getTestSitePath());
}
}
/**
* Stub class supports TestDatabaseTest::testConstructorNullPrefix().
*/
class TestTestDatabase extends TestDatabase {
protected function getTestLock($create_lock = FALSE) {
return 23;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment