diff --git a/core/lib/Drupal/Core/Test/TestDatabase.php b/core/lib/Drupal/Core/Test/TestDatabase.php index e126b3aca4..474299276a 100644 --- a/core/lib/Drupal/Core/Test/TestDatabase.php +++ b/core/lib/Drupal/Core/Test/TestDatabase.php @@ -179,4 +179,218 @@ 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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; + } + } diff --git a/core/modules/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install index fa63fb8269..4245027891 100644 --- a/core/modules/simpletest/simpletest.install +++ b/core/modules/simpletest/simpletest.install @@ -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(); } /** diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index ed3641239c..da56752cb5 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -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 Clean environment 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); } /** diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index fccb7893fd..35c758001b 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -1251,7 +1251,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; diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 9b39ae4694..f482d36871 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -651,8 +651,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) { @@ -748,14 +747,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"; @@ -906,7 +905,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; @@ -927,7 +926,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; diff --git a/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php b/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php index 6e9e63ddc3..5431d952b6 100644 --- a/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php +++ b/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php @@ -7,6 +7,9 @@ /** * @coversDefaultClass \Drupal\Core\Test\TestDatabase + * + * @group Test + * @group simpletest * @group Template */ class TestDatabaseTest extends UnitTestCase { @@ -43,4 +46,30 @@ 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; + } + }