/[drupal]/drupal/modules/simpletest/simpletest.module
ViewVC logotype

Contents of /drupal/modules/simpletest/simpletest.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.80 - (show annotations) (download) (as text)
Sat Oct 24 23:12:11 2009 UTC (4 weeks, 6 days ago) by webchick
Branch: MAIN
CVS Tags: DRUPAL-7-0-UNSTABLE-10, HEAD
Changes since 1.79: +5 -3 lines
File MIME type: text/x-php
#608870 by c960657: Fixed notice in simpletest_clean_database().
1 <?php
2 // $Id: simpletest.module,v 1.79 2009/10/23 22:24:17 webchick Exp $
3
4 /**
5 * @file
6 * Provides testing functionality.
7 */
8
9 /**
10 * Implement hook_help().
11 */
12 function simpletest_help($path, $arg) {
13 switch ($path) {
14 case 'admin/help#simpletest':
15 $output = '<p>' . t('The SimpleTest module is a framework for running automated unit tests in Drupal. It can be used to verify a working state of Drupal before and after any code changes, or as a means for developers to write and execute tests for their modules.') . '</p>';
16 $output .= '<p>' . t('Visit <a href="@admin-simpletest">Testing</a> to display a list of available tests. For comprehensive testing, select <em>all</em> tests, or individually select tests for more targeted testing. Note that it might take several minutes for all tests to complete.)', array('@admin-simpletest' => url('admin/config/development/testing'))) . '</p>';
17 $output .= '<p>' . t('After the tests have run, a message will be displayed next to each test group indicating whether tests within it passed, failed, or had exceptions. A pass means that a test returned the expected results, while fail means that it did not. An exception normally indicates an error outside of the test, such as a PHP warning or notice. If there were fails or exceptions, the results are expanded, and the tests that had issues will be indicated in red or pink rows. Use these results to refine your code and tests until all tests return a pass.') . '</p>';
18 $output .= '<p>' . t('For more information on creating and modifying your own tests, see the <a href="@simpletest-api">SimpleTest API Documentation</a> in the Drupal handbook.', array('@simpletest-api' => 'http://drupal.org/simpletest')) . '</p>';
19 $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@simpletest">SimpleTest module</a>.', array('@simpletest' => 'http://drupal.org/handbook/modules/simpletest')) . '</p>';
20 return $output;
21 }
22 }
23
24 /**
25 * Implement hook_menu().
26 */
27 function simpletest_menu() {
28 $items['admin/config/development/testing'] = array(
29 'title' => 'Testing',
30 'page callback' => 'drupal_get_form',
31 'page arguments' => array('simpletest_test_form'),
32 'description' => 'Run tests against Drupal core and your active modules. These tests help assure that your site code is working as designed.',
33 'access arguments' => array('administer unit tests'),
34 'file' => 'simpletest.pages.inc',
35 );
36 $items['admin/config/development/testing/list'] = array(
37 'title' => 'List',
38 'type' => MENU_DEFAULT_LOCAL_TASK,
39 );
40 $items['admin/config/development/testing/settings'] = array(
41 'title' => 'Settings',
42 'page callback' => 'drupal_get_form',
43 'page arguments' => array('simpletest_settings_form'),
44 'access arguments' => array('administer unit tests'),
45 'type' => MENU_LOCAL_TASK,
46 'file' => 'simpletest.pages.inc',
47 );
48 $items['admin/config/development/testing/results/%'] = array(
49 'title' => 'Test result',
50 'page callback' => 'drupal_get_form',
51 'page arguments' => array('simpletest_result_form', 5),
52 'description' => 'View result of tests.',
53 'access arguments' => array('administer unit tests'),
54 'type' => MENU_CALLBACK,
55 'file' => 'simpletest.pages.inc',
56 );
57 return $items;
58 }
59
60 /**
61 * Implement hook_permission().
62 */
63 function simpletest_permission() {
64 return array(
65 'administer unit tests' => array(
66 'title' => t('Administer unit tests'),
67 'description' => t('Manage and run automated testing. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))),
68 ),
69 );
70 }
71
72 /**
73 * Implement hook_theme().
74 */
75 function simpletest_theme() {
76 return array(
77 'simpletest_test_table' => array(
78 'render element' => 'table',
79 'file' => 'simpletest.pages.inc',
80 ),
81 'simpletest_result_summary' => array(
82 'render element' => 'form',
83 'file' => 'simpletest.pages.inc',
84 ),
85 );
86 }
87
88 /**
89 * Implementation of hook_stream_wrappers().
90 */
91 function simpletest_test_stream_wrappers() {
92 return array(
93 'simpletest' => array(
94 'name' => t('Simpletest files'),
95 'class' => 'DrupalSimpleTestStreamWrapper',
96 'description' => t('Stream Wrapper for Simpletest files.'),
97 ),
98 );
99 }
100
101 /**
102 * Implement hook_js_alter().
103 */
104 function simpletest_js_alter(&$javascript) {
105 // Since SimpleTest is a special use case for the table select, stick the
106 // SimpleTest JavaScript above the table select.
107 $simpletest = drupal_get_path('module', 'simpletest') . '/simpletest.js';
108 if (array_key_exists($simpletest, $javascript) && array_key_exists('misc/tableselect.js', $javascript)) {
109 $javascript[$simpletest]['weight'] = $javascript['misc/tableselect.js']['weight'] - 1;
110 }
111 }
112
113 function _simpletest_format_summary_line($summary) {
114 $args = array(
115 '@pass' => format_plural(isset($summary['#pass']) ? $summary['#pass'] : 0, '1 pass', '@count passes'),
116 '@fail' => format_plural(isset($summary['#fail']) ? $summary['#fail'] : 0, '1 fail', '@count fails'),
117 '@exception' => format_plural(isset($summary['#exception']) ? $summary['#exception'] : 0, '1 exception', '@count exceptions'),
118 );
119 if (!$summary['#debug']) {
120 return t('@pass, @fail, and @exception', $args);
121 }
122 $args['@debug'] = format_plural(isset($summary['#debug']) ? $summary['#debug'] : 0, '1 debug message', '@count debug messages');
123 return t('@pass, @fail, @exception, and @debug', $args);
124 }
125
126 /**
127 * Actually runs tests.
128 *
129 * @param $test_list
130 * List of tests to run.
131 * @param $reporter
132 * Which reporter to use. Allowed values are: text, xml, html and drupal,
133 * drupal being the default.
134 */
135 function simpletest_run_tests($test_list, $reporter = 'drupal') {
136 cache_clear_all();
137 $test_id = db_insert('simpletest_test_id')
138 ->useDefaults(array('test_id'))
139 ->execute();
140
141 // Clear out the previous verbose files.
142 file_unmanaged_delete_recursive(file_directory_path() . '/simpletest/verbose');
143
144 // Get the info for the first test being run.
145 $first_test = array_shift($test_list);
146 $first_instance = new $first_test();
147 array_unshift($test_list, $first_test);
148 $info = $first_instance->getInfo();
149
150 $batch = array(
151 'title' => t('Running tests'),
152 'operations' => array(
153 array('_simpletest_batch_operation', array($test_list, $test_id)),
154 ),
155 'finished' => '_simpletest_batch_finished',
156 'progress_message' => '',
157 'css' => array(drupal_get_path('module', 'simpletest') . '/simpletest.css'),
158 'init_message' => t('Processing test @num of @max - %test.', array('%test' => $info['name'], '@num' => '1', '@max' => count($test_list))),
159 );
160 batch_set($batch);
161
162 module_invoke_all('test_group_started');
163
164 // Normally, the forms portion of the batch API takes care of calling
165 // batch_process(), but in the process it saves the whole $form into the
166 // database (which is huge for the test selection form).
167 // By calling batch_process() directly, we skip that behavior and ensure
168 // that we don't exceed the size of data that can be sent to the database
169 // (max_allowed_packet on MySQL).
170 batch_process('admin/config/development/testing/results/' . $test_id);
171 }
172
173 /**
174 * Batch operation callback.
175 */
176 function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
177 // Get working values.
178 if (!isset($context['sandbox']['max'])) {
179 // First iteration: initialize working values.
180 $test_list = $test_list_init;
181 $context['sandbox']['max'] = count($test_list);
182 $test_results = array('#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0);
183 }
184 else {
185 // Nth iteration: get the current values where we last stored them.
186 $test_list = $context['sandbox']['tests'];
187 $test_results = $context['sandbox']['test_results'];
188 }
189 $max = $context['sandbox']['max'];
190
191 // Perform the next test.
192 $test_class = array_shift($test_list);
193 $test = new $test_class($test_id);
194 $test->run();
195 $size = count($test_list);
196 $info = $test->getInfo();
197
198 module_invoke_all('test_finished', $test->results);
199
200 // Gather results and compose the report.
201 $test_results[$test_class] = $test->results;
202 foreach ($test_results[$test_class] as $key => $value) {
203 $test_results[$key] += $value;
204 }
205 $test_results[$test_class]['#name'] = $info['name'];
206 $items = array();
207 foreach (element_children($test_results) as $class) {
208 array_unshift($items, '<div class="simpletest-' . ($test_results[$class]['#fail'] + $test_results[$class]['#exception'] ? 'fail' : 'pass') . '">' . t('@name: @summary', array('@name' => $test_results[$class]['#name'], '@summary' => _simpletest_format_summary_line($test_results[$class]))) . '</div>');
209 }
210 $context['message'] = t('Processed test @num of @max - %test.', array('%test' => $info['name'], '@num' => $max - $size, '@max' => $max));
211 $context['message'] .= '<div class="simpletest-' . ($test_results['#fail'] + $test_results['#exception'] ? 'fail' : 'pass') . '">Overall results: ' . _simpletest_format_summary_line($test_results) . '</div>';
212 $context['message'] .= theme('item_list', array('items' => $items));
213
214 // Save working values for the next iteration.
215 $context['sandbox']['tests'] = $test_list;
216 $context['sandbox']['test_results'] = $test_results;
217 // The test_id is the only thing we need to save for the report page.
218 $context['results']['test_id'] = $test_id;
219
220 // Multistep processing: report progress.
221 $context['finished'] = 1 - $size / $max;
222 }
223
224 function _simpletest_batch_finished($success, $results, $operations, $elapsed) {
225 if ($success) {
226 drupal_set_message(t('The test run finished in @elapsed.', array('@elapsed' => $elapsed)));
227 }
228 else {
229 // Use the test_id passed as a parameter to _simpletest_batch_operation().
230 $test_id = $operations[0][1][1];
231
232 // Retrieve the last database prefix used for testing and the last test
233 // class that was run from. Use the information to read the lgo file
234 // in case any fatal errors caused the test to crash.
235 list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id);
236 simpletest_log_read($test_id, $last_prefix, $last_test_class);
237
238
239 drupal_set_message(t('The test run did not successfully finish.'), 'error');
240 drupal_set_message(t('Please use the <em>Clean environment</em> button to clean-up temporary files and tables.'), 'warning');
241 }
242 module_invoke_all('test_group_finished');
243 }
244
245 /*
246 * Get information about the last test that ran given a test ID.
247 *
248 * @param $test_id
249 * The test ID to get the last test from.
250 * @return
251 * Array containing the last database prefix used and the last test class
252 * that ran.
253 */
254 function simpletest_last_test_get($test_id) {
255 $last_prefix = db_query_range('SELECT last_prefix FROM {simpletest_test_id} WHERE test_id = :test_id', 0, 1, array(':test_id' => $test_id))->fetchField();
256 $last_test_class = db_query_range('SELECT test_class FROM {simpletest} WHERE test_id = :test_id ORDER BY message_id DESC', 0, 1, array(':test_id' => $test_id))->fetchField();
257 return array($last_prefix, $last_test_class);
258 }
259
260 /**
261 * Read the error log and report any errors as assertion failures.
262 *
263 * The errors in the log should only be fatal errors since any other errors
264 * will have been recorded by the error handler.
265 *
266 * @param $test_id
267 * The test ID to which the log relates.
268 * @param $prefix
269 * The database prefix to which the log relates.
270 * @param $test_class
271 * The test class to which the log relates.
272 * @param $during_test
273 * Indicates that the current file directory path is a temporary file
274 * file directory used during testing.
275 * @return
276 * Found any entries in log.
277 */
278 function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALSE) {
279 $log = 'public://' . ($during_test ? '' : '/simpletest/' . substr($prefix, 10)) . '/error.log';
280 $found = FALSE;
281 if (file_exists($log)) {
282 foreach (file($log) as $line) {
283 if (preg_match('/\[.*?\] (.*?): (.*?) in (.*) on line (\d+)/', $line, $match)) {
284 // Parse PHP fatal errors for example: PHP Fatal error: Call to
285 // undefined function break_me() in /path/to/file.php on line 17
286 $caller = array(
287 'line' => $match[4],
288 'file' => $match[3],
289 );
290 DrupalTestCase::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller);
291 }
292 else {
293 // Unkown format, place the entire message in the log.
294 DrupalTestCase::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error');
295 }
296 $found = TRUE;
297 }
298 }
299 return $found;
300 }
301
302 /**
303 * Get a list of all of the tests provided by the system.
304 *
305 * The list of test classes is loaded from the registry where it looks for
306 * files ending in ".test". Once loaded the test list is cached and stored in
307 * a static variable. In order to list tests provided by disabled modules
308 * hook_registry_files_alter() is used to forcefully add them to the registry.
309 *
310 * @return
311 * An array of tests keyed with the groups specified in each of the tests
312 * getInfo() method and then keyed by the test class. An example of the array
313 * structure is provided below.
314 *
315 * @code
316 * $groups['Blog'] => array(
317 * 'BlogTestCase' => array(
318 * 'name' => 'Blog functionality',
319 * 'description' => 'Create, view, edit, delete, ...',
320 * 'group' => 'Blog',
321 * ),
322 * );
323 * @endcode
324 * @see simpletest_registry_files_alter()
325 */
326 function simpletest_test_get_all() {
327 $groups = &drupal_static(__FUNCTION__);
328
329 if (!$groups) {
330 // Load test information from cache if available, otherwise retrieve the
331 // information from each tests getInfo() method.
332 if ($cache = cache_get('simpletest', 'cache')) {
333 $groups = $cache->data;
334 }
335 else {
336 // Select all clases in files ending with .test.
337 $classes = db_query("SELECT name FROM {registry} WHERE type = :type AND filename LIKE :name", array(':type' => 'class', ':name' => '%.test'));
338
339 // Check that each class has a getInfo() method and store the information
340 // in an array keyed with the group specified in the test information.
341 $groups = array();
342 foreach ($classes as $class) {
343 $class = $class->name;
344 // Test classes need to implement getInfo() to be valid.
345 if (class_exists($class) && method_exists($class, 'getInfo')) {
346 $info = call_user_func(array($class, 'getInfo'));
347
348 // If this test class requires a non-existing module, skip it.
349 if (!empty($info['dependencies'])) {
350 foreach ($info['dependencies'] as $module) {
351 if (!drupal_get_filename('module', $module)) {
352 continue 2;
353 }
354 }
355 }
356
357 $groups[$info['group']][$class] = $info;
358 }
359 }
360
361 // Sort the groups and tests within the groups by name.
362 uksort($groups, 'strnatcasecmp');
363 foreach ($groups as $group => &$tests) {
364 uksort($tests, 'strnatcasecmp');
365 }
366
367 cache_set('simpletest', $groups);
368 }
369 }
370 return $groups;
371 }
372
373 /**
374 * Implementation of hook_registry_files_alter().
375 *
376 * Add the test files for disabled modules so that we get a list containing
377 * all the avialable tests.
378 */
379 function simpletest_registry_files_alter(&$files, $modules) {
380 foreach ($modules as $module) {
381 // Only add test files for disabled modules, as enabled modules should
382 // already include any test files they provide.
383 if (!$module->status) {
384 $dir = $module->dir;
385 if (!empty($module->info['files'])) {
386 foreach ($module->info['files'] as $file) {
387 if (substr($file, -5) == '.test') {
388 $files["$dir/$file"] = array('module' => $module->name, 'weight' => $module->weight);
389 }
390 }
391 }
392 }
393 }
394 }
395
396 /**
397 * Remove all temporary database tables and directories.
398 */
399 function simpletest_clean_environment() {
400 simpletest_clean_database();
401 simpletest_clean_temporary_directories();
402 if (variable_get('simpletest_clear_results', TRUE)) {
403 $count = simpletest_clean_results_table();
404 drupal_set_message(format_plural($count, 'Removed 1 test result.', 'Removed @count test results.'));
405 }
406 else {
407 drupal_set_message(t('Clear results is disabled and the test results table will not be cleared.'), 'warning');
408 }
409
410 // Detect test classes that have been added, renamed or deleted.
411 registry_rebuild();
412 cache_clear_all('simpletest', 'cache');
413 }
414
415 /**
416 * Removed prefixed tables from the database that are left over from crashed tests.
417 */
418 function simpletest_clean_database() {
419 $tables = db_find_tables(Database::getConnection()->prefixTables('{simpletest}') . '%');
420 $schema = drupal_get_schema_unprocessed('simpletest');
421 $count = 0;
422 foreach (array_diff_key($tables, $schema) as $table) {
423 // Strip the prefix and skip tables without digits following "simpletest",
424 // e.g. {simpletest_test_id}.
425 if (preg_match('/simpletest\d+.*/', $table, $matches)) {
426 db_drop_table($matches[0]);
427 $count++;
428 }
429 }
430
431 if ($count > 0) {
432 drupal_set_message(format_plural($count, 'Removed 1 leftover table.', 'Removed @count leftover tables.'));
433 }
434 else {
435 drupal_set_message(t('No leftover tables to remove.'));
436 }
437 }
438
439 /**
440 * Find all leftover temporary directories and remove them.
441 */
442 function simpletest_clean_temporary_directories() {
443 $files = scandir('public://');
444 $count = 0;
445 foreach ($files as $file) {
446 $path = 'public://' . $file;
447 if (is_dir($path) && preg_match('/^simpletest\d+/', $file)) {
448 file_unmanaged_delete_recursive($path);
449 $count++;
450 }
451 }
452
453 if ($count > 0) {
454 drupal_set_message(format_plural($count, 'Removed 1 temporary directory.', 'Removed @count temporary directories.'));
455 }
456 else {
457 drupal_set_message(t('No temporary directories to remove.'));
458 }
459 }
460
461 /**
462 * Clear the test result tables.
463 *
464 * @param $test_id
465 * Test ID to remove results for, or NULL to remove all results.
466 * @return
467 * The number of results removed.
468 */
469 function simpletest_clean_results_table($test_id = NULL) {
470 if (variable_get('simpletest_clear_results', TRUE)) {
471 if ($test_id) {
472 $count = db_query('SELECT COUNT(test_id) FROM {simpletest_test_id} WHERE test_id = :test_id', array(':test_id' => $test_id))->fetchField();
473
474 db_delete('simpletest')
475 ->condition('test_id', $test_id)
476 ->execute();
477 db_delete('simpletest_test_id')
478 ->condition('test_id', $test_id)
479 ->execute();
480 }
481 else {
482 $count = db_query('SELECT COUNT(test_id) FROM {simpletest_test_id}')->fetchField();
483
484 // Clear test results.
485 db_delete('simpletest')->execute();
486 db_delete('simpletest_test_id')->execute();
487 }
488
489 return $count;
490 }
491 return 0;
492 }

  ViewVC Help
Powered by ViewVC 1.1.2