TestDatabase.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. <?php
  2. namespace Drupal\Core\Test;
  3. use Drupal\Component\FileSystem\FileSystem;
  4. use Drupal\Core\Database\ConnectionNotDefinedException;
  5. use Drupal\Core\Database\Database;
  6. /**
  7. * Provides helper methods for interacting with the fixture database.
  8. */
  9. class TestDatabase {
  10. /**
  11. * A random number used to ensure that test fixtures are unique to each test
  12. * method.
  13. *
  14. * @var int
  15. */
  16. protected $lockId;
  17. /**
  18. * The test database prefix.
  19. *
  20. * @var string
  21. */
  22. protected $databasePrefix;
  23. /**
  24. * Returns the database connection to the site running Simpletest.
  25. *
  26. * @return \Drupal\Core\Database\Connection
  27. * The database connection to use for inserting assertions.
  28. *
  29. * @see \Drupal\simpletest\TestBase::prepareEnvironment()
  30. */
  31. public static function getConnection() {
  32. // Check whether there is a test runner connection.
  33. // @see run-tests.sh
  34. // @todo Convert Simpletest UI runner to create + use this connection, too.
  35. try {
  36. $connection = Database::getConnection('default', 'test-runner');
  37. }
  38. catch (ConnectionNotDefinedException $e) {
  39. // Check whether there is a backup of the original default connection.
  40. // @see TestBase::prepareEnvironment()
  41. try {
  42. $connection = Database::getConnection('default', 'simpletest_original_default');
  43. }
  44. catch (ConnectionNotDefinedException $e) {
  45. // If TestBase::prepareEnvironment() or TestBase::restoreEnvironment()
  46. // failed, the test-specific database connection does not exist
  47. // yet/anymore, so fall back to the default of the (UI) test runner.
  48. $connection = Database::getConnection('default', 'default');
  49. }
  50. }
  51. return $connection;
  52. }
  53. /**
  54. * TestDatabase constructor.
  55. *
  56. * @param string|null $db_prefix
  57. * If not provided a new test lock is generated.
  58. * @param bool $create_lock
  59. * (optional) Whether or not to create a lock file. Defaults to FALSE. If
  60. * the environment variable RUN_TESTS_CONCURRENCY is greater than 1 it will
  61. * be overridden to TRUE regardless of its initial value.
  62. *
  63. * @throws \InvalidArgumentException
  64. * Thrown when $db_prefix does not match the regular expression.
  65. */
  66. public function __construct($db_prefix = NULL, $create_lock = FALSE) {
  67. if ($db_prefix === NULL) {
  68. $this->lockId = $this->getTestLock($create_lock);
  69. $this->databasePrefix = 'test' . $this->lockId;
  70. }
  71. else {
  72. $this->databasePrefix = $db_prefix;
  73. // It is possible that we're running a test inside a test. In which case
  74. // $db_prefix will be something like test12345678test90123456 and the
  75. // generated lock ID for the running test method would be 90123456.
  76. preg_match('/test(\d+)$/', $db_prefix, $matches);
  77. if (!isset($matches[1])) {
  78. throw new \InvalidArgumentException("Invalid database prefix: $db_prefix");
  79. }
  80. $this->lockId = $matches[1];
  81. }
  82. }
  83. /**
  84. * Gets the relative path to the test site directory.
  85. *
  86. * @return string
  87. * The relative path to the test site directory.
  88. */
  89. public function getTestSitePath() {
  90. return 'sites/simpletest/' . $this->lockId;
  91. }
  92. /**
  93. * Gets the test database prefix.
  94. *
  95. * @return string
  96. * The test database prefix.
  97. */
  98. public function getDatabasePrefix() {
  99. return $this->databasePrefix;
  100. }
  101. /**
  102. * Generates a unique lock ID for the test method.
  103. *
  104. * @param bool $create_lock
  105. * (optional) Whether or not to create a lock file. Defaults to FALSE.
  106. *
  107. * @return int
  108. * The unique lock ID for the test method.
  109. */
  110. protected function getTestLock($create_lock = FALSE) {
  111. // There is a risk that the generated random number is a duplicate. This
  112. // would cause different tests to try to use the same database prefix.
  113. // Therefore, if running with a concurrency of greater than 1, we need to
  114. // create a lock.
  115. if (getenv('RUN_TESTS_CONCURRENCY') > 1) {
  116. $create_lock = TRUE;
  117. }
  118. do {
  119. $lock_id = mt_rand(10000000, 99999999);
  120. if ($create_lock && @symlink(__FILE__, $this->getLockFile($lock_id)) === FALSE) {
  121. // If we can't create a symlink, the lock ID is in use. Generate another
  122. // one. Symlinks are used because they are atomic and reliable.
  123. $lock_id = NULL;
  124. }
  125. } while ($lock_id === NULL);
  126. return $lock_id;
  127. }
  128. /**
  129. * Releases a lock.
  130. *
  131. * @return bool
  132. * TRUE if successful, FALSE if not.
  133. */
  134. public function releaseLock() {
  135. return unlink($this->getLockFile($this->lockId));
  136. }
  137. /**
  138. * Releases all test locks.
  139. *
  140. * This should only be called once all the test fixtures have been cleaned up.
  141. */
  142. public static function releaseAllTestLocks() {
  143. $tmp = FileSystem::getOsTemporaryDirectory();
  144. $dir = dir($tmp);
  145. while (($entry = $dir->read()) !== FALSE) {
  146. if ($entry === '.' || $entry === '..') {
  147. continue;
  148. }
  149. $entry_path = $tmp . '/' . $entry;
  150. if (preg_match('/^test_\d+/', $entry) && is_link($entry_path)) {
  151. unlink($entry_path);
  152. }
  153. }
  154. }
  155. /**
  156. * Gets the lock file path.
  157. *
  158. * @param int $lock_id
  159. * The test method lock ID.
  160. *
  161. * @return string
  162. * A file path to the symbolic link that prevents the lock ID being re-used.
  163. */
  164. protected function getLockFile($lock_id) {
  165. return FileSystem::getOsTemporaryDirectory() . '/test_' . $lock_id;
  166. }
  167. /**
  168. * Store an assertion from outside the testing context.
  169. *
  170. * This is useful for inserting assertions that can only be recorded after
  171. * the test case has been destroyed, such as PHP fatal errors. The caller
  172. * information is not automatically gathered since the caller is most likely
  173. * inserting the assertion on behalf of other code. In all other respects
  174. * the method behaves just like \Drupal\simpletest\TestBase::assert() in terms
  175. * of storing the assertion.
  176. *
  177. * @param string $test_id
  178. * The test ID to which the assertion relates.
  179. * @param string $test_class
  180. * The test class to store an assertion for.
  181. * @param bool|string $status
  182. * A boolean or a string of 'pass' or 'fail'. TRUE means 'pass'.
  183. * @param string $message
  184. * The assertion message.
  185. * @param string $group
  186. * The assertion message group.
  187. * @param array $caller
  188. * The an array containing the keys 'file' and 'line' that represent the
  189. * file and line number of that file that is responsible for the assertion.
  190. *
  191. * @return int
  192. * Message ID of the stored assertion.
  193. *
  194. * @internal
  195. */
  196. public static function insertAssert($test_id, $test_class, $status, $message = '', $group = 'Other', array $caller = []) {
  197. // Convert boolean status to string status.
  198. if (is_bool($status)) {
  199. $status = $status ? 'pass' : 'fail';
  200. }
  201. $caller += [
  202. 'function' => 'Unknown',
  203. 'line' => 0,
  204. 'file' => 'Unknown',
  205. ];
  206. $assertion = [
  207. 'test_id' => $test_id,
  208. 'test_class' => $test_class,
  209. 'status' => $status,
  210. 'message' => $message,
  211. 'message_group' => $group,
  212. 'function' => $caller['function'],
  213. 'line' => $caller['line'],
  214. 'file' => $caller['file'],
  215. ];
  216. return static::getConnection()
  217. ->insert('simpletest')
  218. ->fields($assertion)
  219. ->execute();
  220. }
  221. /**
  222. * Get information about the last test that ran given a test ID.
  223. *
  224. * @param int $test_id
  225. * The test ID to get the last test from.
  226. *
  227. * @return array
  228. * Associative array containing the last database prefix used and the
  229. * last test class that ran.
  230. *
  231. * @internal
  232. */
  233. public static function lastTestGet($test_id) {
  234. $connection = static::getConnection();
  235. // Define a subquery to identify the latest 'message_id' given the
  236. // $test_id.
  237. $max_message_id_subquery = $connection
  238. ->select('simpletest', 'sub')
  239. ->condition('test_id', $test_id);
  240. $max_message_id_subquery->addExpression('MAX(message_id)', 'max_message_id');
  241. // Run a select query to return 'last_prefix' from {simpletest_test_id} and
  242. // 'test_class' from {simpletest}.
  243. $select = $connection->select($max_message_id_subquery, 'st_sub');
  244. $select->join('simpletest', 'st', 'st.message_id = st_sub.max_message_id');
  245. $select->join('simpletest_test_id', 'sttid', 'st.test_id = sttid.test_id');
  246. $select->addField('sttid', 'last_prefix');
  247. $select->addField('st', 'test_class');
  248. return $select->execute()->fetchAssoc();
  249. }
  250. /**
  251. * Reads the error log and reports any errors as assertion failures.
  252. *
  253. * The errors in the log should only be fatal errors since any other errors
  254. * will have been recorded by the error handler.
  255. *
  256. * @param int $test_id
  257. * The test ID to which the log relates.
  258. * @param string $test_class
  259. * The test class to which the log relates.
  260. *
  261. * @return bool
  262. * Whether any fatal errors were found.
  263. *
  264. * @internal
  265. */
  266. public function logRead($test_id, $test_class) {
  267. $log = DRUPAL_ROOT . '/' . $this->getTestSitePath() . '/error.log';
  268. $found = FALSE;
  269. if (file_exists($log)) {
  270. foreach (file($log) as $line) {
  271. if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
  272. // Parse PHP fatal errors for example: PHP Fatal error: Call to
  273. // undefined function break_me() in /path/to/file.php on line 17
  274. $caller = [
  275. 'line' => $match[4],
  276. 'file' => $match[3],
  277. ];
  278. static::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
  279. }
  280. else {
  281. // Unknown format, place the entire message in the log.
  282. static::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error');
  283. }
  284. $found = TRUE;
  285. }
  286. }
  287. return $found;
  288. }
  289. /**
  290. * Defines the database schema for run-tests.sh and simpletest module.
  291. *
  292. * @return array
  293. * Array suitable for use in a hook_schema() implementation.
  294. *
  295. * @internal
  296. */
  297. public static function testingSchema() {
  298. $schema['simpletest'] = [
  299. 'description' => 'Stores simpletest messages',
  300. 'fields' => [
  301. 'message_id' => [
  302. 'type' => 'serial',
  303. 'not null' => TRUE,
  304. 'description' => 'Primary Key: Unique simpletest message ID.',
  305. ],
  306. 'test_id' => [
  307. 'type' => 'int',
  308. 'not null' => TRUE,
  309. 'default' => 0,
  310. 'description' => 'Test ID, messages belonging to the same ID are reported together',
  311. ],
  312. 'test_class' => [
  313. 'type' => 'varchar_ascii',
  314. 'length' => 255,
  315. 'not null' => TRUE,
  316. 'default' => '',
  317. 'description' => 'The name of the class that created this message.',
  318. ],
  319. 'status' => [
  320. 'type' => 'varchar',
  321. 'length' => 9,
  322. 'not null' => TRUE,
  323. 'default' => '',
  324. 'description' => 'Message status. Core understands pass, fail, exception.',
  325. ],
  326. 'message' => [
  327. 'type' => 'text',
  328. 'not null' => TRUE,
  329. 'description' => 'The message itself.',
  330. ],
  331. 'message_group' => [
  332. 'type' => 'varchar_ascii',
  333. 'length' => 255,
  334. 'not null' => TRUE,
  335. 'default' => '',
  336. 'description' => 'The message group this message belongs to. For example: warning, browser, user.',
  337. ],
  338. 'function' => [
  339. 'type' => 'varchar_ascii',
  340. 'length' => 255,
  341. 'not null' => TRUE,
  342. 'default' => '',
  343. 'description' => 'Name of the assertion function or method that created this message.',
  344. ],
  345. 'line' => [
  346. 'type' => 'int',
  347. 'not null' => TRUE,
  348. 'default' => 0,
  349. 'description' => 'Line number on which the function is called.',
  350. ],
  351. 'file' => [
  352. 'type' => 'varchar',
  353. 'length' => 255,
  354. 'not null' => TRUE,
  355. 'default' => '',
  356. 'description' => 'Name of the file where the function is called.',
  357. ],
  358. ],
  359. 'primary key' => ['message_id'],
  360. 'indexes' => [
  361. 'reporter' => ['test_class', 'message_id'],
  362. ],
  363. ];
  364. $schema['simpletest_test_id'] = [
  365. 'description' => 'Stores simpletest test IDs, used to auto-increment the test ID so that a fresh test ID is used.',
  366. 'fields' => [
  367. 'test_id' => [
  368. 'type' => 'serial',
  369. 'not null' => TRUE,
  370. 'description' => 'Primary Key: Unique simpletest ID used to group test results together. Each time a set of tests
  371. are run a new test ID is used.',
  372. ],
  373. 'last_prefix' => [
  374. 'type' => 'varchar',
  375. 'length' => 60,
  376. 'not null' => FALSE,
  377. 'default' => '',
  378. 'description' => 'The last database prefix used during testing.',
  379. ],
  380. ],
  381. 'primary key' => ['test_id'],
  382. ];
  383. return $schema;
  384. }
  385. /**
  386. * Inserts the parsed PHPUnit results into {simpletest}.
  387. *
  388. * @param array[] $phpunit_results
  389. * An array of test results, as returned from
  390. * \Drupal\Core\Test\JUnitConverter::xmlToRows(). These results are in a
  391. * form suitable for inserting into the {simpletest} table of the test
  392. * results database.
  393. *
  394. * @internal
  395. */
  396. public static function processPhpUnitResults($phpunit_results) {
  397. if ($phpunit_results) {
  398. $query = static::getConnection()
  399. ->insert('simpletest')
  400. ->fields(array_keys($phpunit_results[0]));
  401. foreach ($phpunit_results as $result) {
  402. $query->values($result);
  403. }
  404. $query->execute();
  405. }
  406. }
  407. }