diff options
author | Dries Buytaert <dries@buytaert.net> | 2008-04-20 18:24:07 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2008-04-20 18:24:07 +0000 |
commit | af474609e3e80db9ba1d16b9ad2eae89775f51c8 (patch) | |
tree | e525479dd6381b94d21943c3d2decf1c749c028c /modules/simpletest | |
parent | fe7b9baff62379f5a0c901d27cbb677345791bd0 (diff) | |
download | brdo-af474609e3e80db9ba1d16b9ad2eae89775f51c8.tar.gz brdo-af474609e3e80db9ba1d16b9ad2eae89775f51c8.tar.bz2 |
- Added a test framework to Drupal along with a first batch of tests for
Drupal core! This is an important milestone for the project so enable
the module and check it out ... :)
Thanks to Rok Žlender, Károly Négyesi, Jimmy Berry, Kevin Bridges, Charlie
Gordon, Douglas Hubler, Miglius Alaburda, Andy Kirkham, Dimitri13, Kieran
Lal, Moshe Weitzman, and the many other people that helped with testing
over the past years and that drove this home.
It all works but it is still rough around the edges (i.e. documentation
is still being written, the coding style is not 100% yet, a number of
tests still fail) but we spent the entire weekend working on it in Paris
and made a ton of progress. The best way to help and to get up to speed,
is to start writing and contributing some tests ... as well as fixing
some of the failures.
For those willing to help with improving the test framework, here are
some next steps and issues to resolve:
- How to best approach unit tests and mock functions?
- How to test drupal_mail() and drupal_http_request()?
- How to improve the admin UI so we have a nice progress bar?
- How best to do code coverage?
- See http://g.d.o/node/10099 for more ...
Diffstat (limited to 'modules/simpletest')
-rw-r--r-- | modules/simpletest/files/README.txt | 5 | ||||
-rw-r--r-- | modules/simpletest/files/html-1.txt | 1 | ||||
-rw-r--r-- | modules/simpletest/files/html-2.html | 1 | ||||
-rw-r--r-- | modules/simpletest/files/image-1.png | bin | 0 -> 64027 bytes | |||
-rw-r--r-- | modules/simpletest/files/image-2.jpg | bin | 0 -> 6218 bytes | |||
-rw-r--r-- | modules/simpletest/files/javascript-1.txt | 3 | ||||
-rw-r--r-- | modules/simpletest/files/javascript-2.script | 3 | ||||
-rw-r--r-- | modules/simpletest/files/php-1.txt | 3 | ||||
-rw-r--r-- | modules/simpletest/files/php-2.php | 3 | ||||
-rw-r--r-- | modules/simpletest/files/sql-1.txt | 1 | ||||
-rw-r--r-- | modules/simpletest/files/sql-2.sql | 1 | ||||
-rw-r--r-- | modules/simpletest/simpletest.css | 62 | ||||
-rw-r--r-- | modules/simpletest/simpletest.info | 6 | ||||
-rw-r--r-- | modules/simpletest/simpletest.install | 98 | ||||
-rw-r--r-- | modules/simpletest/simpletest.js | 58 | ||||
-rw-r--r-- | modules/simpletest/simpletest.module | 478 | ||||
-rw-r--r-- | modules/simpletest/simpletest.php | 369 |
17 files changed, 1092 insertions, 0 deletions
diff --git a/modules/simpletest/files/README.txt b/modules/simpletest/files/README.txt new file mode 100644 index 000000000..d808510da --- /dev/null +++ b/modules/simpletest/files/README.txt @@ -0,0 +1,5 @@ +$Id$ + +These files are use in some tests that upload files or other operations were +a file is useful. These files are copied to the files directory as specified +in the site settings. Other tests files are generated in order to save space.
\ No newline at end of file diff --git a/modules/simpletest/files/html-1.txt b/modules/simpletest/files/html-1.txt new file mode 100644 index 000000000..494470d17 --- /dev/null +++ b/modules/simpletest/files/html-1.txt @@ -0,0 +1 @@ +<h1>SimpleTest HTML</h1>
\ No newline at end of file diff --git a/modules/simpletest/files/html-2.html b/modules/simpletest/files/html-2.html new file mode 100644 index 000000000..494470d17 --- /dev/null +++ b/modules/simpletest/files/html-2.html @@ -0,0 +1 @@ +<h1>SimpleTest HTML</h1>
\ No newline at end of file diff --git a/modules/simpletest/files/image-1.png b/modules/simpletest/files/image-1.png Binary files differnew file mode 100644 index 000000000..f2aac9800 --- /dev/null +++ b/modules/simpletest/files/image-1.png diff --git a/modules/simpletest/files/image-2.jpg b/modules/simpletest/files/image-2.jpg Binary files differnew file mode 100644 index 000000000..645c76b50 --- /dev/null +++ b/modules/simpletest/files/image-2.jpg diff --git a/modules/simpletest/files/javascript-1.txt b/modules/simpletest/files/javascript-1.txt new file mode 100644 index 000000000..e0206ba83 --- /dev/null +++ b/modules/simpletest/files/javascript-1.txt @@ -0,0 +1,3 @@ +<script> +alert('SimpleTest PHP was executed!'); +</script>
\ No newline at end of file diff --git a/modules/simpletest/files/javascript-2.script b/modules/simpletest/files/javascript-2.script new file mode 100644 index 000000000..e0206ba83 --- /dev/null +++ b/modules/simpletest/files/javascript-2.script @@ -0,0 +1,3 @@ +<script> +alert('SimpleTest PHP was executed!'); +</script>
\ No newline at end of file diff --git a/modules/simpletest/files/php-1.txt b/modules/simpletest/files/php-1.txt new file mode 100644 index 000000000..dc8e64213 --- /dev/null +++ b/modules/simpletest/files/php-1.txt @@ -0,0 +1,3 @@ +<?php +print 'SimpleTest PHP was executed!'; +?>
\ No newline at end of file diff --git a/modules/simpletest/files/php-2.php b/modules/simpletest/files/php-2.php new file mode 100644 index 000000000..dc8e64213 --- /dev/null +++ b/modules/simpletest/files/php-2.php @@ -0,0 +1,3 @@ +<?php +print 'SimpleTest PHP was executed!'; +?>
\ No newline at end of file diff --git a/modules/simpletest/files/sql-1.txt b/modules/simpletest/files/sql-1.txt new file mode 100644 index 000000000..22017e972 --- /dev/null +++ b/modules/simpletest/files/sql-1.txt @@ -0,0 +1 @@ +SELECT invalid_field FROM {invalid_table}
\ No newline at end of file diff --git a/modules/simpletest/files/sql-2.sql b/modules/simpletest/files/sql-2.sql new file mode 100644 index 000000000..22017e972 --- /dev/null +++ b/modules/simpletest/files/sql-2.sql @@ -0,0 +1 @@ +SELECT invalid_field FROM {invalid_table}
\ No newline at end of file diff --git a/modules/simpletest/simpletest.css b/modules/simpletest/simpletest.css new file mode 100644 index 000000000..6ceda6111 --- /dev/null +++ b/modules/simpletest/simpletest.css @@ -0,0 +1,62 @@ +/* $Id$ */ + +/* Addon for the simpletest module */ +#simpletest { +} +/* Test Table */ +th.simpletest_run { + width: 50px; +} +th.simpletest_test { + width: 160px; +} + +table#simpletest-form-table td div.form-item, +td.simpletest-select-all { + text-align: center; +} + +table#simpletest-form-table tr td { + background-color: white !important; +} + +table#simpletest-form-table tr.simpletest-group td { + background-color: #EDF5FA !important; +} + +div.simpletest-pass { + background: #b6ffb6; +} + +div.simpletest-fail { + background: #ffc9c9; +} + +tr.simpletest-pass.odd { + background: #b6ffb6; +} + +tr.simpletest-pass.even { + background: #9bff9b; +} + +tr.simpletest-fail.odd { + background: #ffc9c9; +} + +tr.simpletest-fail.even { + background: #ffacac; +} + +tr.simpletest-exception.odd { + background: #f4ea71; +} + +tr.simpletest-exception.even { + background: #f5e742; +} + +div.simpletest-image { + display: inline; + cursor: pointer; +} diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info new file mode 100644 index 000000000..9a162e714 --- /dev/null +++ b/modules/simpletest/simpletest.info @@ -0,0 +1,6 @@ +; $Id$ +name = "SimpleTest" +description = "Provides a framework for unit and functional testing." +package = Core - optional +version = VERSION +core = 7.x diff --git a/modules/simpletest/simpletest.install b/modules/simpletest/simpletest.install new file mode 100644 index 000000000..4287b2a5d --- /dev/null +++ b/modules/simpletest/simpletest.install @@ -0,0 +1,98 @@ +<?php +// $Id$ + +/** + * Implementation of hook_install(). + */ +function simpletest_install() { + // Check for files directory. + $path = file_directory_path() . '/simpletest'; + if (file_check_directory($path, FILE_CREATE_DIRECTORY)) { + // Generate binary and text test files. + $generated = FALSE; + if (simpletest_get_file_count($path, 'binary') == 0) { + $lines = array(64, 1024); + foreach ($lines as $line) { + simpletest_generate_file('binary', 64, $line, 'binary'); + } + $generated = TRUE; + } + + if (simpletest_get_file_count($path, 'text') == 0) { + $lines = array(16, 256, 1024, 2048, 20480); + foreach ($lines as $line) { + simpletest_generate_file('text', 64, $line); + } + $generated = TRUE; + } + + // Copy other test files for consistency. + $files = file_scan_directory($path, '(html|image|javascript|php|sql)-.*'); + if (count($files) == 0) { + $original = drupal_get_path('module', 'simpletest') .'/files'; + $files = file_scan_directory($original, '(html|image|javascript|php|sql)-.*'); + foreach ($files as $file) { + file_copy($file->filename, $path .'/'. $file->basename); + } + $generated = TRUE; + } + + if ($generated) { + drupal_set_message('Extra test files generated.'); + } + } +} + +/** + * Generate test file. + */ +function simpletest_generate_file($filename, $width, $lines, $type = 'binary-text') { + $size = $width * $lines - $lines; + + // Generate random text + $text = ''; + for ($i = 0; $i < $size; $i++) { + switch ($type) { + case 'text': + $text .= chr(rand(32, 126)); + break; + case 'binary': + $text .= chr(rand(0, 31)); + break; + case 'binary-text': + default: + $text .= rand(0, 1); + break; + } + } + $text = wordwrap($text, $width - 1, "\n", TRUE) ."\n"; // Add \n for symetrical file. + + // Create filename. + $path = file_directory_path() . '/simpletest/'; + $count = simpletest_get_file_count($path, $filename); + file_put_contents($path . $filename .'-'. ($count + 1) .'.txt', $text); +} + +/** + * Get the number of files that have the specified filename base. + */ +function simpletest_get_file_count($directory, $filename) { + $files = scandir($directory); + $count = 0; + foreach ($files as $file) { + if (preg_match('/'. $filename .'.*?/', $file)) { + $count++; + } + } + return $count; +} + +/** + * Implementation of hook_uninstall(). + */ +function simpletest_uninstall() { + variable_del('simpletest_httpauth'); + variable_del('simpletest_httpauth_username'); + variable_del('simpletest_httpauth_pass'); + variable_del('simpletest_devel'); +} diff --git a/modules/simpletest/simpletest.js b/modules/simpletest/simpletest.js new file mode 100644 index 000000000..db9352a24 --- /dev/null +++ b/modules/simpletest/simpletest.js @@ -0,0 +1,58 @@ +// $Id$ + +Drupal.behaviors.simpleTestMenuCollapse = function() { + // Adds expand-collapse functionality. + $('div.simpletest-image').click(function() { + // Toggle all of the trs. + if (!Drupal.settings.simpleTest[$(this).attr('id')].clickActive) { + Drupal.settings.simpleTest[$(this).attr('id')].clickActive = true; + var trs = $(this).parents('tbody').children().filter('.'+ Drupal.settings.simpleTest[$(this).attr('id')].testClass), trs_formatted = [], direction = Drupal.settings.simpleTest[$(this).attr('id')].imageDirection, self = $(this); + for (var i = 0; i < trs.length; i++) { + trs_formatted.push(trs[i]); + } + var toggleTrs = function(trs, action, action2) { + tr = trs[action](); + if (tr) { + $(tr)[action2](1, function() { + toggleTrs(trs, action, action2); + }); + } + else { + Drupal.settings.simpleTest[self.attr('id')].clickActive = false; + } + } + toggleTrs(trs_formatted, (direction? 'pop' : 'shift'), (direction? 'fadeOut' : 'fadeIn')); + Drupal.settings.simpleTest[$(this).attr('id')].imageDirection = !direction; + $(this).html(Drupal.settings.simpleTest.images[(direction? 0 : 1)]); + } + }); +} +Drupal.behaviors.simpleTestSelectAll = function() { + $('td.simpletest-select-all').each(function() { + var checkboxes = Drupal.settings.simpleTest['simpletest-test-group-'+ $(this).attr('id')].testNames, + checkbox = $('<input type="checkbox" class="form-checkbox" id="'+ $(this).attr('id') +'-select-all" />').change(function() { + var checked = !!($(this).attr('checked')); + for (var i = 0; i < checkboxes.length; i++) { + $('#'+ checkboxes[i]).attr('checked', checked); + } + self.data('simpletest-checked-tests', (checked? checkboxes.length : 0)); + }).data('simpletest-checked-tests', 0); + var self = $(this); + for (var i = 0; i < checkboxes.length; i++) { + $('#'+ checkboxes[i]).change(function() { + if (checkbox.attr('checked') == 'checked') { + checkbox.attr('checked', ''); + } + var data = (!self.data('simpletest-checked-tests') ? 0 : self.data('simpletest-checked-tests')) + (!!($(this).attr('checked')) ? 1 : -1); + self.data('simpletest-checked-tests', data); + if (data == checkboxes.length) { + checkbox.attr('checked', 'checked'); + } + else { + checkbox.attr('checked', ''); + } + }); + } + $(this).append(checkbox); + }); +};
\ No newline at end of file diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module new file mode 100644 index 000000000..c61c30f83 --- /dev/null +++ b/modules/simpletest/simpletest.module @@ -0,0 +1,478 @@ +<?php +// $Id$ + +/** + * Implementation of hook_help(). + */ +function simpletest_help($path, $arg) { + switch ($path) { + case 'admin/help#simpletest': + $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>'; + $output .= '<p>'. t('Visit <a href="@admin-simpletest">Administer >> Site building >> SimpleTest</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/build/testing'))) .'</p>'; + $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>'; + $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>'; + $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>'; + return $output; + } +} + +/** + * Implementation of hook_menu(). + */ +function simpletest_menu() { + $items['admin/build/testing'] = array( + 'title' => 'Testing', + 'page callback' => 'simpletest_entrypoint', + 'description' => 'Run tests against Drupal core and your active modules. These tests help assure that your site code is working as designed.', + 'access arguments' => array('administer unit tests'), + ); + $items['admin/settings/testing'] = array( + 'title' => 'Testing', + 'description' => 'Configure SimpleTest framework.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('simpletest_settings'), + 'access arguments' => array('access administration pages'), + ); + return $items; +} + +/** + * Implementation of hook_perm(). + */ +function simpletest_perm() { + return array( + 'administer unit tests' => t('Manage and run automated testing. %warning', array('%warning' => t('Warning: Give to trusted roles only; this permission has security implications.'))), + ); +} + +/** + * Implemenation of hook_theme(). + */ +function simpletest_theme() { + return array( + 'simpletest_overview_form' => array( + 'arguments' => array('form' => NULL) + ), + ); +} + +/** + * Try to load the simepletest + * @return boolean TRUE if the load succeeded + */ +function simpletest_load() { + global $user; + static $loaded; + if (!$loaded) { + $loaded = TRUE; + if ($user->uid != 1) { + drupal_set_message(t('It is strongly suggested to run the tests with the first user!')); + } + $path = drupal_get_path('module', 'simpletest') .'/'; + foreach (array('simpletest.php', 'unit_tester.php', 'reporter.php', 'drupal_reporter.php', 'drupal_web_test_case.php', 'drupal_test_suite.php') as $file) { + require_once($path . $file); + } + } +} + +/** + * Menu callback for both running tests and listing possible tests + */ +function simpletest_entrypoint() { + simpletest_load(); + drupal_add_css(drupal_get_path('module', 'simpletest') .'/simpletest.css', 'module'); + drupal_add_js(drupal_get_path('module', 'simpletest') .'/simpletest.js', 'module'); + $output = drupal_get_form('simpletest_overview_form'); + + if (simpletest_running_output()) { + return simpletest_running_output() . $output; + } + else { + return $output; + } +} + +function simpletest_running_output($output = NULL) { + static $o; + if ($output != NULL) { + $o = $output; + } + return $o; +} + +/** + * Form callback; make the form to run tests + */ +function simpletest_overview_form() { + $output = array( + '#theme' => 'simpletest_overview_form' + ); + + $total_test = &simpletest_get_total_test(); + + $test_instances = $total_test->getTestInstances(); + uasort($test_instances, 'simpletest_compare_instances'); + + foreach ($test_instances as $group_test) { + $group = array(); + $tests = $group_test->getTestInstances(); + $group_class = str_replace(' ', '-', strtolower($group_test->getLabel())); + $group['tests'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => 'Tests', + '#attributes' => array('class' => $group_class), + ); + foreach ($tests as $test) { + $test_info = $test->getInfo(); + $group['tests'][get_class($test)] = array( + '#type' => 'checkbox', + '#title' => $test_info['name'], + '#default_value' => 0, + '#description' => $test_info['description'], + ); + } + $output[] = $group + array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#title' => $group_test->getLabel(), + '#attributes' => array('class' => 'all-tests'), + ); + } + + $output['run'] = array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#title' => t('Run tests'), + ); + $output['run']['running_options'] = array( + '#type' => 'radios', + '#default_value' => 'selected_tests', + '#options' => array( + 'all_tests' => t('Run all tests (WARNING, this may take a long time)'), + 'selected_tests' => t('Run selected tests'), + ), + ); + $output['run']['op'] = array( + '#type' => 'submit', + '#value' => t('Run tests'), + '#submit' => array('simpletest_run_selected_tests') + ); + + $output['reset'] = array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#title' => t('Clean test environment'), + '#description' => t('Remove tables with the prefix "simpletest" and temporary directories that are left over from tests that crashed.') + ); + $output['reset']['op'] = array( + '#type' => 'submit', + '#value' => t('Clean environment'), + '#submit' => array('simpletest_clean_environment') + ); + return $output; +} + +/** + * Theme the SimpleTest form that provides test selection. + * + * @ingroup themeable + */ +function theme_simpletest_overview_form($form) { + $header = array( + array('data' => t('Run'), 'class' => 'simpletest_run checkbox'), + array('data' => t('Test'), 'class' => 'simpletest_test'), + array('data' => t('Description'), 'class' => 'simpletest_description'), + ); + $js = array( + 'images' => array( + theme('image', 'misc/menu-collapsed.png', 'Expand', 'Expand'), + theme('image', 'misc/menu-expanded.png', 'Collapsed', 'Collapsed'), + ), + ); + + // Go through each test group and create a row: + $rows = array(); + foreach (element_children($form) as $gid) { + if (isset($form[$gid]['tests'])) { + $element = &$form[$gid]; + $test_class = strtolower(trim(preg_replace("/[^\w\d]/", "-",$element["#title"]))); + + $row = array(); + $row[] = array('id' => $test_class, 'class' => 'simpletest-select-all'); + $row[] = array( + 'data' => '<div class="simpletest-image" id="simpletest-test-group-'. $test_class .'">'. $js['images'][0] .'</div> <label for="'. $test_class .'-select-all" class="simpletest-group-label">'. $element['#title'] .'</label>', + 'style' => 'font-weight: bold;' + ); + $row[] = $element['#description']; + $rows[] = array('data' => $row, 'class' => 'simpletest-group'); + $current_js = array('testClass' => $test_class .'-test', 'testNames' => array(), 'imageDirection' => 0, 'clickActive' => FALSE); + + // Go through each test in the group and create table rows setting them to invisible: + foreach (element_children($element['tests']) as $test_name) { + $current_js['testNames'][] = 'edit-'. $test_name; + $test = $element['tests'][$test_name]; + foreach (array('title', 'description') as $key) { + $$key = $test['#'. $key]; + unset($test['#'. $key]); + } + $test['#name'] = $test_name; + $themed_test = drupal_render($test); + $row = array(); + $row[] = $themed_test; + $row[] = theme('indentation', 1) .'<label for="edit-'. $test_name .'">'. $title .'</label>'; + $row[] = '<div class="description">'. $description .'</div>'; + $rows[] = array('data' => $row, 'style' => 'display: none;', 'class' => $test_class .'-test'); + } + $js['simpletest-test-group-'. $test_class] = $current_js; + unset($form[$gid]); // Remove test group from form. + } + } + drupal_add_js(array('simpleTest' => $js), 'setting'); + + // Output test groups: + $output = ''; + if (count($rows)) { + $output .= theme('table', $header, $rows, array('id' => 'simpletest-form-table')); + } + + // Output the rest of the form, excluded test groups which have been removed: + $output .= drupal_render($form); + + return $output; +} + +/** + * Compare two test instance objects for use in sorting. + */ +function simpletest_compare_instances(&$a, &$b) { + if (substr_compare($a->_label, $b->_label, 0) > 0) { + return 1; + } + return -1; +} + +/** + * Run selected tests. + */ +function simpletest_run_selected_tests($form, &$form_state) { + $form_state['redirect'] = FALSE; + $output = ''; + switch ($form_state['values']['running_options']) { + case 'all_tests': + $output = simpletest_run_tests(); + break; + case 'selected_tests': + $tests_list = array(); + foreach ($form_state['values'] as $item => $value) { + if ($value === 1 && strpos($item, 'selectall') === FALSE) { + $tests_list[] = $item; + } + } + if (count($tests_list) > 0 ) { + $output = simpletest_run_tests($tests_list); + break; + } + // Fall through + default: + drupal_set_message(t('No test has been selected.'), 'error'); + } + + simpletest_running_output($output); + return FALSE; +} + +/** + * Remove all temporary database tables and directories. + */ +function simpletest_clean_environment() { + simpletest_clean_database(); + simpletest_clean_temporary_directories(); +} + +/** + * Removed prefixed talbes from the database that are left over from crashed tests. + */ +function simpletest_clean_database() { + $tables = simpletest_get_like_tables(); + + $ret = array(); + foreach ($tables as $table) { + db_drop_table($ret, $table); + } + + if (count($ret) > 0) { + drupal_set_message(t('Removed @count left over tables.', array('@count' => count($ret)))); + } + else { + drupal_set_message(t('No left over tables to remove.')); + } +} + +/** + * Find all tables that are like the specified base table name. + * + * @param string $base_table Base table name. + * @param boolean $count Return the table count instead of list of tables. + * @return mixed Array of matching tables or count of tables. + */ +function simpletest_get_like_tables($base_table = 'simpletest', $count = FALSE) { + global $db_url, $db_prefix; + $url = parse_url($db_url); + $database = substr($url['path'], 1); + $select = $count ? 'COUNT(table_name)' : 'table_name'; + $result = db_query("SELECT $select FROM information_schema.tables WHERE table_schema = '$database' AND table_name LIKE '$db_prefix$base_table%'"); + + if ($count) { + return db_result($result); + } + $tables = array(); + while ($table = db_result($result)) { + $tables[] = $table; + } + return $tables; +} + +/** + * Find all left over temporary directories and remove them. + */ +function simpletest_clean_temporary_directories() { + $files = scandir(file_directory_path()); + $count = 0; + foreach ($files as $file) { + $path = file_directory_path() .'/'. $file; + if (is_dir($path) && preg_match('/^simpletest\d+/', $file)) { + simpletest_clean_temporary_directory($path); + $count++; + } + } + + if ($count > 0) { + drupal_set_message(t('Removed @count temporary directories.', array('@count' => $count))); + } + else { + drupal_set_message(t('No temporary directories to remove.')); + } +} + +/** + * Remove all files from specified firectory and then remove directory. + * + * @param string $path Directory path. + */ +function simpletest_clean_temporary_directory($path) { + $files = scandir($path); + foreach ($files as $file) { + if ($file != '.' && $file != '..') { + $file_path = "$path/$file"; + if (is_dir($file_path)) { + simpletest_clean_temporary_directory($file_path); + } + else { + file_delete($file_path); + } + } + } + rmdir($path); +} + +/** + * Actually runs tests + * @param array $test_list list of tests to run or DEFAULT NULL run all tests + * @param boolean $html_reporter TRUE if you want results in simple html, FALSE for full drupal page + */ +function simpletest_run_tests($test_list = NULL, $reporter = 'drupal') { + static $test_running; + if (!$test_running) { + $test_running = TRUE; + $test = simpletest_get_total_test($test_list); + switch ($reporter) { + case 'text': + $reporter = &new TextReporter(); + break; + case 'xml': + $reporter = &new XMLReporter(); + break; + case 'html': + $reporter = &new HtmlReporter(); + break; + case 'drupal': + $reporter = &new DrupalReporter(); + break; + } + + cache_clear_all(); + $results = $test->run($reporter); + $test_running = FALSE; + + switch (get_class($reporter)) { + case 'TextReporter': + case 'XMLReporter': + case 'HtmlReporter': + return $results; + case 'DrupalReporter': + return $reporter->getOutput(); + } + } +} + +/** + * This function makes sure no unnecessary copies of the DrupalTests object are instantiated + * @param array $classes list of all classes the test should concern or + * DEFAULT NULL + * @return DrupalTests object + */ +function &simpletest_get_total_test($classes = NULL) { + static $total_test; + if (!$total_test) { + simpletest_load(); + $total_test = &new DrupalTests(); + } + if (!is_null($classes)) { + $dut = new DrupalTests($classes); + return $dut; + } + return $total_test; +} + +function simpletest_settings() { + $form = array(); + + $form['http_auth'] = array( + '#type' => 'fieldset', + '#title' => t('HTTP authentication'), + '#description' => t('If needed, enter a username and password for reaching your web site. This is not a drupal username/password.') . + t('This is a login presented by your web server. Most sites may leave this section empty.'), + ); + $form['http_auth']['simpletest_httpauth'] = array( + '#type' => 'checkbox', + '#title' => t('Use http authentication'), + '#default_value' => variable_get('simpletest_httpauth', FALSE), + ); + $form['http_auth']['simpletest_httpauth_username'] = array( + '#type' => 'textfield', + '#title' => t('Username'), + '#default_value' => variable_get('simpletest_httpauth_username', ''), + ); + $form['http_auth']['simpletest_httpauth_pass'] = array( + '#title' => t('Password'), + '#type' => 'password', + '#default_value' => variable_get('simpletest_httpauth_pass', ''), + ); + $form['devel'] = array( + '#type' => 'fieldset', + '#title' => t('Devel module settings'), + '#description' => t('Devel module can cause problems if you have query log enabled. It will output a few thousand queries and crash your browser'), + ); + $form['devel']['simpletest_devel'] = array( + '#type' => 'checkbox', + '#title' => t('Use devel query log on test result pages'), + '#default_value' => variable_get('simpletest_devel', FALSE), + ); + + return system_settings_form($form); + +} diff --git a/modules/simpletest/simpletest.php b/modules/simpletest/simpletest.php new file mode 100644 index 000000000..c5f647910 --- /dev/null +++ b/modules/simpletest/simpletest.php @@ -0,0 +1,369 @@ +<?php +// $Id$ + +require_once dirname(__FILE__) .'/test_case.php'; +require_once dirname(__FILE__) .'/unit_tester.php'; +require_once dirname(__FILE__) .'/expectation.php'; +require_once dirname(__FILE__) .'/invoker.php'; +require_once dirname(__FILE__) .'/scorer.php'; +require_once dirname(__FILE__) .'/reporter.php'; +require_once dirname(__FILE__) .'/default_reporter.php'; +require_once dirname(__FILE__) .'/dumper.php'; +require_once dirname(__FILE__) .'/errors.php'; +require_once dirname(__FILE__) .'/exceptions.php'; +require_once dirname(__FILE__) .'/xml.php'; + +/** + * Registry and test context. Includes a few + * global options that I'm slowly getting rid of. + */ +class SimpleTest { + /** + * Sets the name of a test case to ignore, usually + * because the class is an abstract case that should + * not be run. Once PHP4 is dropped this will disappear + * as a public method and "abstract" will rule. + * @param string $class Add a class to ignore. + * @static + * @access public + */ + function ignore($class) { + $registry = &SimpleTest::_getRegistry(); + $registry['IgnoreList'][strtolower($class)] = true; + } + + /** + * Scans the now complete ignore list, and adds + * all parent classes to the list. If a class + * is not a runnable test case, then it's parents + * wouldn't be either. This is syntactic sugar + * to cut down on ommissions of ignore()'s or + * missing abstract declarations. This cannot + * be done whilst loading classes wiithout forcing + * a particular order on the class declarations and + * the ignore() calls. It's just nice to have the ignore() + * calls at the top of the file before the actual declarations. + * @param array $classes Class names of interest. + * @static + * @access public + */ + function ignoreParentsIfIgnored($classes) { + $registry = &SimpleTest::_getRegistry(); + foreach ($classes as $class) { + if (SimpleTest::isIgnored($class)) { + $reflection = new ReflectionClass($class); + if ($parent = $reflection->getParentClass()) { + SimpleTest::ignore($parent->getName()); + } + } + } + } + + /** + * Puts the object to the global pool of 'preferred' objects + * which can be retrieved with SimpleTest :: preferred() method. + * Instances of the same class are overwritten. + * @param object $object Preferred object + * @static + * @access public + * @see preferred() + */ + function prefer(&$object) { + $registry = &SimpleTest::_getRegistry(); + $registry['Preferred'][] = &$object; + } + + /** + * Retrieves 'preferred' objects from global pool. Class filter + * can be applied in order to retrieve the object of the specific + * class + * @param array|string $classes Allowed classes or interfaces. + * @static + * @access public + * @return array|object|null + * @see prefer() + */ + function &preferred($classes) { + if (! is_array($classes)) { + $classes = array($classes); + } + $registry = &SimpleTest::_getRegistry(); + for ($i = count($registry['Preferred']) - 1; $i >= 0; $i--) { + foreach ($classes as $class) { + if (is_a($registry['Preferred'][$i], $class)) { + return $registry['Preferred'][$i]; + } + } + } + return null; + } + + /** + * Test to see if a test case is in the ignore + * list. Quite obviously the ignore list should + * be a separate object and will be one day. + * This method is internal to SimpleTest. Don't + * use it. + * @param string $class Class name to test. + * @return boolean True if should not be run. + * @access public + * @static + */ + function isIgnored($class) { + $registry = &SimpleTest::_getRegistry(); + return isset($registry['IgnoreList'][strtolower($class)]); + } + + /** + * Sets proxy to use on all requests for when + * testing from behind a firewall. Set host + * to false to disable. This will take effect + * if there are no other proxy settings. + * @param string $proxy Proxy host as URL. + * @param string $username Proxy username for authentication. + * @param string $password Proxy password for authentication. + * @access public + */ + function useProxy($proxy, $username = false, $password = false) { + $registry = &SimpleTest::_getRegistry(); + $registry['DefaultProxy'] = $proxy; + $registry['DefaultProxyUsername'] = $username; + $registry['DefaultProxyPassword'] = $password; + } + + /** + * Accessor for default proxy host. + * @return string Proxy URL. + * @access public + */ + function getDefaultProxy() { + $registry = &SimpleTest::_getRegistry(); + return $registry['DefaultProxy']; + } + + /** + * Accessor for default proxy username. + * @return string Proxy username for authentication. + * @access public + */ + function getDefaultProxyUsername() { + $registry = &SimpleTest::_getRegistry(); + return $registry['DefaultProxyUsername']; + } + + /** + * Accessor for default proxy password. + * @return string Proxy password for authentication. + * @access public + */ + function getDefaultProxyPassword() { + $registry = &SimpleTest::_getRegistry(); + return $registry['DefaultProxyPassword']; + } + + /** + * Accessor for global registry of options. + * @return hash All stored values. + * @access private + * @static + */ + function &_getRegistry() { + static $registry = false; + if (! $registry) { + $registry = SimpleTest::_getDefaults(); + } + return $registry; + } + + /** + * Accessor for the context of the current + * test run. + * @return SimpleTestContext Current test run. + * @access public + * @static + */ + function &getContext() { + static $context = false; + if (! $context) { + $context = new SimpleTestContext(); + } + return $context; + } + + /** + * Constant default values. + * @return hash All registry defaults. + * @access private + * @static + */ + function _getDefaults() { + return array( + 'StubBaseClass' => 'SimpleStub', + 'MockBaseClass' => 'SimpleMock', + 'IgnoreList' => array(), + 'DefaultProxy' => false, + 'DefaultProxyUsername' => false, + 'DefaultProxyPassword' => false, + 'Preferred' => array(new HtmlReporter(), new TextReporter(), new XmlReporter())); + } +} + +/** + * Container for all components for a specific + * test run. Makes things like error queues + * available to PHP event handlers, and also + * gets around some nasty reference issues in + * the mocks. + * @package SimpleTest + */ +class SimpleTestContext { + var $_test; + var $_reporter; + var $_resources; + + /** + * Clears down the current context. + * @access public + */ + function clear() { + $this->_resources = array(); + } + + /** + * Sets the current test case instance. This + * global instance can be used by the mock objects + * to send message to the test cases. + * @param SimpleTestCase $test Test case to register. + * @access public + */ + function setTest(&$test) { + $this->clear(); + $this->_test = &$test; + } + + /** + * Accessor for currently running test case. + * @return SimpleTestCase Current test. + * @access public + */ + function &getTest() { + return $this->_test; + } + + /** + * Sets the current reporter. This + * global instance can be used by the mock objects + * to send messages. + * @param SimpleReporter $reporter Reporter to register. + * @access public + */ + function setReporter(&$reporter) { + $this->clear(); + $this->_reporter = &$reporter; + } + + /** + * Accessor for current reporter. + * @return SimpleReporter Current reporter. + * @access public + */ + function &getReporter() { + return $this->_reporter; + } + + /** + * Accessor for the Singleton resource. + * @return object Global resource. + * @access public + * @static + */ + function &get($resource) { + if (! isset($this->_resources[$resource])) { + $this->_resources[$resource] = &new $resource(); + } + return $this->_resources[$resource]; + } +} + +/** + * Interrogates the stack trace to recover the + * failure point. + */ +class SimpleStackTrace { + var $_prefixes; + + /** + * Stashes the list of target prefixes. + * @param array $prefixes List of method prefixes + * to search for. + */ + function SimpleStackTrace($prefixes) { + $this->_prefixes = $prefixes; + } + + /** + * Extracts the last method name that was not within + * Simpletest itself. Captures a stack trace if none given. + * @param array $stack List of stack frames. + * @return string Snippet of test report with line + * number and file. + * @access public + */ + function traceMethod($stack = false) { + $stack = $stack ? $stack : $this->_captureTrace(); + foreach ($stack as $frame) { + if ($this->_frameLiesWithinSimpleTestFolder($frame)) { + continue; + } + if ($this->_frameMatchesPrefix($frame)) { + return ' at [' . $frame['file'] . ' line ' . $frame['line'] . ']'; + } + } + return ''; + } + + /** + * Test to see if error is generated by SimpleTest itself. + * @param array $frame PHP stack frame. + * @return boolean True if a SimpleTest file. + * @access private + */ + function _frameLiesWithinSimpleTestFolder($frame) { + if (isset($frame['file'])) { + $path = dirname(__FILE__); + if (strpos($frame['file'], $path) === 0) { + if (dirname($frame['file']) == $path) { + return true; + } + } + } + return false; + } + + /** + * Tries to determine if the method call is an assert, etc. + * @param array $frame PHP stack frame. + * @return boolean True if matches a target. + * @access private + */ + function _frameMatchesPrefix($frame) { + foreach ($this->_prefixes as $prefix) { + if (strncmp($frame['function'], $prefix, strlen($prefix)) == 0) { + return true; + } + } + return false; + } + + /** + * Grabs a current stack trace. + * @return array Fulle trace. + * @access private + */ + function _captureTrace() { + if (function_exists('debug_backtrace')) { + return array_reverse(debug_backtrace()); + } + return array(); + } +} |