summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2008-04-20 18:34:43 +0000
committerDries Buytaert <dries@buytaert.net>2008-04-20 18:34:43 +0000
commitffc0e93c4eb0555a08f0b58bed0735416e6ba41f (patch)
tree7f741d0b40124633c5b337dc7210711bfcd4b876
parentaf474609e3e80db9ba1d16b9ad2eae89775f51c8 (diff)
downloadbrdo-ffc0e93c4eb0555a08f0b58bed0735416e6ba41f.tar.gz
brdo-ffc0e93c4eb0555a08f0b58bed0735416e6ba41f.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 ...
-rw-r--r--modules/simpletest/default_reporter.php75
-rw-r--r--modules/simpletest/drupal_reporter.php257
-rw-r--r--modules/simpletest/drupal_test_suite.php121
-rw-r--r--modules/simpletest/drupal_unit_test_case.php97
-rw-r--r--modules/simpletest/drupal_web_test_case.php1051
-rw-r--r--modules/simpletest/dumper.php80
-rw-r--r--modules/simpletest/errors.php240
-rw-r--r--modules/simpletest/exceptions.php171
-rw-r--r--modules/simpletest/expectation.php144
-rw-r--r--modules/simpletest/invoker.php119
-rw-r--r--modules/simpletest/reporter.php283
-rw-r--r--modules/simpletest/scorer.php413
-rw-r--r--modules/simpletest/test_case.php615
-rw-r--r--modules/simpletest/unit_tester.php178
-rw-r--r--modules/simpletest/xml.php641
15 files changed, 4485 insertions, 0 deletions
diff --git a/modules/simpletest/default_reporter.php b/modules/simpletest/default_reporter.php
new file mode 100644
index 000000000..8a760353d
--- /dev/null
+++ b/modules/simpletest/default_reporter.php
@@ -0,0 +1,75 @@
+<?php
+// $Id$
+
+/**
+ * Parser for command line arguments. Extracts
+ * the a specific test to run and engages XML
+ * reporting when necessary.
+ */
+class SimpleCommandLineParser {
+ protected $to_property = array('c' => 'case', 't' => 'test');
+ protected $case = '';
+ protected $test = '';
+ protected $xml = false;
+
+ function SimpleCommandLineParser($arguments) {
+ if (!is_array($arguments)) {
+ return;
+ }
+ foreach ($arguments as $i => $argument) {
+ if (preg_match('/^--?(test|case|t|c)=(.+)$/', $argument, $matches)) {
+ $this->{$this->_parseProperty($matches[1])} = $matches[2];
+ }
+ elseif (preg_match('/^--?(test|case|t|c)$/', $argument, $matches)) {
+ if (isset($arguments[$i + 1])) {
+ $this->{$this->_parseProperty($matches[1])} = $arguments[$i + 1];
+ }
+ }
+ elseif (preg_match('/^--?(xml|x)$/', $argument)) {
+ $this->xml = true;
+ }
+ }
+ }
+ function _parseProperty($property) {
+ if (isset($this->to_property[$property])) {
+ return $this->to_property[$property];
+ }
+ else {
+ return $property;
+ }
+ }
+ function getTest() {
+ return $this->test;
+ }
+
+ function getTestCase() {
+ return $this->case;
+ }
+
+ function isXml() {
+ return $this->xml;
+ }
+}
+
+/**
+ * The default reporter used by SimpleTest's autorun
+ * feature. The actual reporters used are dependency
+ * injected and can be overridden.
+ */
+class DefaultReporter extends SimpleReporterDecorator {
+ /**
+ * Assembles the appopriate reporter for the environment.
+ */
+ function DefaultReporter() {
+ if (SimpleReporter::inCli()) {
+ global $argv;
+ $parser = new SimpleCommandLineParser($argv);
+ $interfaces = $parser->isXml() ? array('XmlReporter') : array('TextReporter');
+ $reporter = &new SelectiveReporter(SimpleTest::preferred($interfaces), $parser->getTestCase(), $parser->getTest());
+ }
+ else {
+ $reporter = &new SelectiveReporter(SimpleTest::preferred('HtmlReporter'), @$_GET['c'], @$_GET['t']);
+ }
+ $this->SimpleReporterDecorator($reporter);
+ }
+}
diff --git a/modules/simpletest/drupal_reporter.php b/modules/simpletest/drupal_reporter.php
new file mode 100644
index 000000000..8885dfbe4
--- /dev/null
+++ b/modules/simpletest/drupal_reporter.php
@@ -0,0 +1,257 @@
+<?php
+// $Id$
+
+/**
+ * Minimal drupal displayer. Accumulates output to $_output.
+ * Based on HtmlReporter by Marcus Baker
+ */
+class DrupalReporter extends SimpleReporter {
+ var $_output_error = '';
+ var $_character_set;
+ var $_fails_stack = array(0);
+ var $_exceptions_stack = array(0);
+ var $_passes_stack = array(0);
+ var $_progress_stack = array(0);
+ var $test_info_stack = array();
+ var $_output_stack_id = -1;
+ var $form;
+ var $form_depth = array();
+ var $current_field_set = array();
+ var $content_count = 0;
+ var $weight = -10;
+ var $test_stack = array();
+
+ function DrupalReporter($character_set = 'ISO-8859-1') {
+ $this->SimpleReporter();
+ drupal_add_css(drupal_get_path('module', 'simpletest') .'/simpletest.css');
+ $this->_character_set = $character_set;
+ }
+
+ /**
+ * Paints the top of the web page setting the
+ * title to the name of the starting test.
+ * @param string $test_name Name class of test.
+ * @access public
+ **/
+ function paintHeader($test_name) {
+
+ }
+
+ /**
+ * Paints the end of the test with a summary of
+ * the passes and failures.
+ * @param string $test_name Name class of test.
+ * @access public
+ */
+ function paintFooter($test_name) {
+ $ok = ($this->getFailCount() + $this->getExceptionCount() == 0);
+ $class = $ok ? 'simpletest-pass' : 'simpletest-fail';
+ $this->writeContent('<strong>' . $this->getPassCount() . '</strong> passes, <strong>' . $this->getFailCount() . '</strong> fails and <strong>' . $this->getExceptionCount() . '<strong> exceptions.');
+ }
+
+ /**
+ * Paints the test passes
+ * @param string $message Failure message displayed in
+ * the context of the other tests.
+ * @access public
+ **/
+ function paintPass($message, $group) {
+ parent::paintPass($message);
+ if ($group == 'Other') {
+ $group = t($group);
+ }
+ $this->test_stack[] = array(
+ 'data' => array($message, "<strong>[$group]</strong>", t('Pass'), theme('image', 'misc/watchdog-ok.png')),
+ 'class' => 'simpletest-pass',
+ );
+ }
+
+ /**
+ * Paints the test failure with a breadcrumbs
+ * trail of the nesting test suites below the
+ * top level test.
+ * @param string $message Failure message displayed in
+ * the context of the other tests.
+ * @access public
+ */
+ function paintFail($message, $group) {
+ parent::paintFail($message);
+ if ($group == 'Other') {
+ $group = t($group);
+ }
+ $this->test_stack[] = array(
+ 'data' => array($message, "<strong>[$group]</strong>", t('Fail'), theme('image', 'misc/watchdog-error.png')),
+ 'class' => 'simpletest-fail',
+ );
+ }
+
+
+ /**
+ * Paints a PHP error or exception.
+ * @param string $message Message is ignored.
+ * @access public
+ **/
+ function paintError($message) {
+ parent::paintError($message);
+ $this->test_stack[] = array(
+ 'data' => array($message, '<strong>[PHP]</strong>', t('Exception'), theme('image', 'misc/watchdog-warning.png')),
+ 'class' => 'simpletest-exception',
+ );
+ }
+
+ /**
+ * Paints the start of a group test. Will also paint
+ * the page header and footer if this is the
+ * first test. Will stash the size if the first
+ * start.
+ * @param string $test_name Name of test that is starting.
+ * @param integer $size Number of test cases starting.
+ * @access public
+ */
+ function paintGroupStart($test_name, $size, $extra = '') {
+ $this->_progress_stack[] = $this->_progress;
+ $this->_progress = 0;
+ $this->_exceptions_stack[] = $this->_exceptions;
+ $this->_exceptions = 0;
+ $this->_fails_stack[] = $this->_fails;
+ $this->_fails = 0;
+ $this->_passes_stack[] = $this->_passes;
+ $this->_passes = 0;
+ $this->form_depth[] = $test_name;
+ $this->writeToLastField($this->form, array(
+ '#type' => 'fieldset',
+ '#title' => $test_name,
+ '#weight' => $this->weight++,
+ ), $this->form_depth);
+
+ if (! isset($this->_size)) {
+ $this->_size = $size;
+ }
+
+ if (($c = count($this->test_info_stack)) > 0) {
+ $info = $this->test_info_stack[$c - 1];
+ $this->writeContent('<strong>' . $info['name'] . '</strong>: ' . $info['description'], $this->getParentWeight() );
+ }
+
+ $this->_test_stack[] = $test_name;
+ }
+
+ function paintCaseStart($test_name) {
+ $this->_progress++;
+ $this->paintGroupStart($test_name, 1);
+ }
+
+
+ /**
+ * Paints the end of a group test. Will paint the page
+ * footer if the stack of tests has unwound.
+ * @param string $test_name Name of test that is ending.
+ * @param integer $progress Number of test cases ending.
+ * @access public
+ */
+ function paintGroupEnd($test_name) {
+ array_pop($this->_test_stack);
+ $ok = ($this->getFailCount() + $this->getExceptionCount() == 0);
+ $class = $ok ? "simpletest-pass" : "simpletest-fail";
+ $parent_weight = $this->getParentWeight() - 0.5;
+ /* Exception for the top groups, no subgrouping for singles */
+ if (($this->_output_stack_id == 2) && ($this->_output_stack[$this->_output_stack_id]['size'] == 1)) {
+ $this->writeContent(format_plural($this->getTestCaseProgress(), '1 test case complete: ', '@count test cases complete: '), -10);
+ $parent_weight = $this->getParentWeight() - 0.5;
+ $this->writeContent('<strong>' . $this->getPassCount() . '</strong> passes, <strong>' . $this->getFailCount() . '</strong> fails and <strong>' . $this->getExceptionCount() . '</strong> exceptions.', $parent_weight, $class);
+ array_pop($this->form_depth);
+ }
+ else {
+ $collapsed = $ok ? TRUE : FALSE;
+ if ($this->getTestCaseProgress()) {
+ $this->writeContent(format_plural($this->getTestCaseProgress(), '1 test case complete: ', '@count test cases complete: '), -10);
+ $use_grouping = FALSE;
+ }
+ else {
+ $use_grouping = TRUE;
+ }
+ $write = array('#collapsible' => $use_grouping, '#collapsed' => $collapsed);
+ $this->writeToLastField($this->form, $write, $this->form_depth);
+ $this->writeContent('<strong>' . $this->getPassCount() . '</strong> passes, <strong>' . $this->getFailCount() . '</strong> fails and <strong>' . $this->getExceptionCount() . '</strong> exceptions.', $parent_weight, $class);
+ if (count($this->test_stack) != 0) {
+ $this->writeContent(theme('table', array(), $this->test_stack));
+ $this->test_stack = array();
+ }
+ array_pop($this->form_depth);
+ }
+
+ $this->_progress += array_pop($this->_progress_stack);
+ $this->_exceptions += array_pop($this->_exceptions_stack);
+ $this->_fails += array_pop($this->_fails_stack);
+ $this->_passes += array_pop($this->_passes_stack);
+ }
+
+ function paintCaseEnd($test_name) {
+ $this->paintGroupEnd($test_name);
+ }
+
+ /**
+ * Could be extended to show more headers or whatever?
+ **/
+ function getOutput() {
+ return drupal_get_form('unit_tests', $this);
+ }
+
+ /**
+ * Recursive function that writes attr to the deepest array
+ */
+ function writeToLastField(&$form, $attr, $keys) {
+ while(count($keys) != 0) {
+ $value = array_shift($keys);
+ if (isset($form[$value])) {
+ if (count($keys) == 0) {
+ $form[$value] += $attr;
+ }
+ else {
+ $this->writeToLastField($form[$value], $attr, $keys);
+ }
+ $keys = array();
+ }
+ else {
+ $form[$value] = $attr;
+ }
+ }
+ }
+
+ /**
+ * writes $msg into the deepest fieldset
+ * @param $msg content to write
+ */
+ function writeContent($msg, $weight = NULL, $class = 'simpletest') {
+ if (!$weight) {
+ $weight = $this->weight++;
+ }
+ $write['content'.$this->content_count++] = array(
+ '#value' => '<div class=' . $class .'>' . $msg . '</div>',
+ '#weight' => $weight,
+ );
+ $this->writeToLastField($this->form, $write, $this->form_depth);
+ }
+
+ /**
+ * Retrieves weight of the currently deepest fieldset
+ */
+ function getParentWeight($form = NULL, $keys = NULL ) {
+ if (!isset($form)) {
+ $form = $this->form;
+ }
+ if (!isset($keys)) {
+ $keys = $this->form_depth;
+ }
+ if(count($keys) != 0) {
+ $value = array_shift($keys);
+ return $this->getParentWeight($form[$value], $keys);
+ }
+ return $form['#weight'];
+ }
+}
+
+function unit_tests($args, $reporter) {
+ return $reporter->form['Drupal Unit Tests'];
+}
+?> \ No newline at end of file
diff --git a/modules/simpletest/drupal_test_suite.php b/modules/simpletest/drupal_test_suite.php
new file mode 100644
index 000000000..bbc33e220
--- /dev/null
+++ b/modules/simpletest/drupal_test_suite.php
@@ -0,0 +1,121 @@
+<?php
+// $Id$
+
+/**
+ * Implementes getTestInstances to allow access to the test objects from outside
+ */
+class DrupalTestSuite extends TestSuite {
+ var $_cleanupModules = array();
+
+ function DrupalTestSuite($label) {
+ $this->TestSuite($label);
+ }
+
+ /**
+ * @return array of instantiated tests that this GroupTests holds
+ */
+ function getTestInstances() {
+ for ($i = 0, $count = count($this->_test_cases); $i < $count; $i++) {
+ if (is_string($this->_test_cases[$i])) {
+ $class = $this->_test_cases[$i];
+ $this->_test_cases[$i] = &new $class();
+ }
+ }
+ return $this->_test_cases;
+ }
+}
+
+class DrupalTests extends DrupalTestSuite {
+ /**
+ * Constructor
+ * @param array $class_list list containing the classes of tests to be processed
+ * default: NULL - run all tests
+ */
+ function DrupalTests($class_list = NULL) {
+ static $classes;
+ $this->DrupalTestSuite('Drupal Unit Tests');
+
+ /* Tricky part to avoid double inclusion */
+ if (!$classes) {
+
+ $files = array();
+ foreach (array_keys(module_rebuild_cache()) as $module) {
+ $module_path = drupal_get_path('module', $module);
+ $test = $module_path . "/$module.test";
+ if (file_exists($test)) {
+ $files[] = $test;
+ }
+ }
+
+ $existing_classes = get_declared_classes();
+ foreach ($files as $file) {
+ include_once($file);
+ }
+ $classes = array_diff(get_declared_classes(), $existing_classes);
+ }
+ if (!is_null($class_list)) {
+ $classes = $class_list;
+ }
+ if (count($classes) == 0) {
+ drupal_set_message('No test cases found.', 'error');
+ return;
+ }
+ $groups = array();
+ foreach ($classes as $class) {
+ if (!is_subclass_of($class, 'DrupalWebTestCase') && !is_subclass_of($class, 'DrupalUnitTestCase')) {
+ continue;
+ }
+ $this->_addClassToGroups($groups, $class);
+ }
+ foreach ($groups as $group_name => $group) {
+ $group_test = &new DrupalTestSuite($group_name);
+ foreach ($group as $key => $v) {
+ $group_test->addTestCase($group[$key]);
+ }
+ $this->addTestCase($group_test);
+ }
+ }
+
+ /**
+ * Adds a class to a groups array specified by the getInfo of the group
+ * @param array $groups Group of categorized tests
+ * @param string $class Name of a class
+ */
+ function _addClassToGroups(&$groups, $class) {
+ $test = &new $class();
+ if (method_exists($test, 'getInfo')) {
+ $info = $test->getInfo();
+ $groups[$info['group']][] = $test;
+ }
+ }
+
+ /**
+ * Invokes run() on all of the held test cases, instantiating
+ * them if necessary.
+ * The Drupal version uses paintHeader instead of paintGroupStart
+ * to avoid collapsing of the very top level.
+ *
+ * @param SimpleReporter $reporter Current test reporter.
+ * @access public
+ */
+ function run(&$reporter) {
+ cache_clear_all();
+ @set_time_limit(0);
+ ignore_user_abort(TRUE);
+
+ // Disable devel output, check simpletest settings page
+ if (!variable_get('simpletest_devel', FALSE)) {
+ $GLOBALS['devel_shutdown'] = FALSE;
+ }
+
+ $result = parent::run($reporter);
+
+ // Restores modules
+ foreach ($this->_cleanupModules as $name => $status) {
+ db_query("UPDATE {system} SET status = %d WHERE name = '%s' AND type = 'module'", $status, $name);
+ }
+ $this->_cleanupModules = array();
+
+ return $result;
+ }
+}
diff --git a/modules/simpletest/drupal_unit_test_case.php b/modules/simpletest/drupal_unit_test_case.php
new file mode 100644
index 000000000..7a7807095
--- /dev/null
+++ b/modules/simpletest/drupal_unit_test_case.php
@@ -0,0 +1,97 @@
+<?php
+// $Id$
+
+/**
+ * Test case Drupal unit tests.
+ */
+class DrupalUnitTestCase extends UnitTestCase {
+ protected $created_temp_environment = FALSE;
+ protected $db_prefix_original;
+ protected $original_file_directory;
+
+ /**
+ * Retrieve the test information from getInfo().
+ *
+ * @param string $label Name of the test to be used by the SimpleTest library.
+ */
+ function __construct($label = NULL) {
+ if (!$label) {
+ if (method_exists($this, 'getInfo')) {
+ $info = $this->getInfo();
+ $label = $info['name'];
+ }
+ }
+ parent::__construct($label);
+ }
+
+ /**
+ * Generates a random database prefix and runs the install scripts on the prefixed database.
+ * After installation many caches are flushed and the internal browser is setup so that the page
+ * requests will run on the new prefix. A temporary files directory is created with the same name
+ * as the database prefix.
+ *
+ * @param ... List modules to enable.
+ */
+ public function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * Create a temporary environment for tests to take place in so that changes
+ * will be reverted and other tests won't be affected.
+ *
+ * Generates a random database prefix and runs the install scripts on the prefixed database.
+ * After installation many caches are flushed and the internal browser is setup so that the page
+ * requests will run on the new prefix. A temporary files directory is created with the same name
+ * as the database prefix.
+ */
+ protected function createTempEnvironment() {
+ global $db_prefix, $simpletest_ua_key;
+ $this->created_temp_environment = TRUE;
+ if ($simpletest_ua_key) {
+ $this->db_prefix_original = $db_prefix;
+ $clean_url_original = variable_get('clean_url', 0);
+ $db_prefix = 'simpletest'. mt_rand(1000, 1000000);
+ include_once './includes/install.inc';
+ drupal_install_system();
+ $modules = array_unique(array_merge(func_get_args(), drupal_verify_profile('default', 'en')));
+ drupal_install_modules($modules);
+ $this->_modules = drupal_map_assoc($modules);
+ $this->_modules['system'] = 'system';
+ $task = 'profile';
+ default_profile_tasks($task, '');
+ menu_rebuild();
+ actions_synchronize();
+ _drupal_flush_css_js();
+ variable_set('install_profile', 'default');
+ variable_set('install_task', 'profile-finished');
+ variable_set('clean_url', $clean_url_original);
+
+ // Use temporary files directory with the same prefix as database.
+ $this->original_file_directory = file_directory_path();
+ variable_set('file_directory_path', file_directory_path() .'/'. $db_prefix);
+ file_check_directory(file_directory_path(), TRUE); // Create the files directory.
+ }
+ }
+
+ /**
+ * Delete created files and temporary files directory, delete the tables created by setUp(),
+ * and reset the database prefix.
+ */
+ public function tearDown() {
+ global $db_prefix;
+ if ($this->created_temp_environment && preg_match('/simpletest\d+/', $db_prefix)) {
+ // Delete temporary files directory and reset files directory path.
+ simpletest_clean_temporary_directory(file_directory_path());
+ variable_set('file_directory_path', $this->original_file_directory);
+
+ $schema = drupal_get_schema(NULL, TRUE);
+ $ret = array();
+ foreach ($schema as $name => $table) {
+ db_drop_table($ret, $name);
+ }
+ $db_prefix = $this->db_prefix_original;
+ }
+ parent::tearDown();
+ }
+}
diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php
new file mode 100644
index 000000000..304a2d214
--- /dev/null
+++ b/modules/simpletest/drupal_web_test_case.php
@@ -0,0 +1,1051 @@
+<?php
+// $Id$
+
+/**
+ * Test case for typical Drupal tests.
+ */
+class DrupalWebTestCase extends UnitTestCase {
+ protected $_logged_in = FALSE;
+ protected $_content;
+ protected $plain_text;
+ protected $ch;
+ protected $_modules = array();
+ // We do not reuse the cookies in further runs, so we do not need a file
+ // but we still need cookie handling, so we set the jar to NULL
+ protected $cookie_file = NULL;
+ // Overwrite this any time to supply cURL options as necessary,
+ // DrupalTestCase itself never sets this but always obeys whats set.
+ protected $curl_options = array();
+ protected $original_file_directory;
+
+ /**
+ * Retrieve the test information from getInfo().
+ *
+ * @param string $label Name of the test to be used by the SimpleTest library.
+ */
+ function __construct($label = NULL) {
+ if (!$label) {
+ if (method_exists($this, 'getInfo')) {
+ $info = $this->getInfo();
+ $label = $info['name'];
+ }
+ }
+ parent::__construct($label);
+ }
+
+ /**
+ * Creates a node based on default settings.
+ *
+ * @param settings
+ * An assocative array of settings to change from the defaults, keys are
+ * node properties, for example 'body' => 'Hello, world!'.
+ * @return object Created node object.
+ */
+ function drupalCreateNode($settings = array()) {
+ // Populate defaults array
+ $defaults = array(
+ 'body' => $this->randomName(32),
+ 'title' => $this->randomName(8),
+ 'comment' => 2,
+ 'changed' => time(),
+ 'format' => FILTER_FORMAT_DEFAULT,
+ 'moderate' => 0,
+ 'promote' => 0,
+ 'revision' => 1,
+ 'log' => '',
+ 'status' => 1,
+ 'sticky' => 0,
+ 'type' => 'page',
+ 'revisions' => NULL,
+ 'taxonomy' => NULL,
+ );
+ $defaults['teaser'] = $defaults['body'];
+ // If we already have a node, we use the original node's created time, and this
+ if (isset($defaults['created'])) {
+ $defaults['date'] = format_date($defaults['created'], 'custom', 'Y-m-d H:i:s O');
+ }
+ if (empty($settings['uid'])) {
+ global $user;
+ $defaults['uid'] = $user->uid;
+ }
+ $node = ($settings + $defaults);
+ $node = (object)$node;
+
+ node_save($node);
+
+ // small hack to link revisions to our test user
+ db_query('UPDATE {node_revisions} SET uid = %d WHERE vid = %d', $node->uid, $node->vid);
+ return $node;
+ }
+
+ /**
+ * Creates a custom content type based on default settings.
+ *
+ * @param settings
+ * An array of settings to change from the defaults.
+ * Example: 'type' => 'foo'.
+ * @return object Created content type.
+ */
+ function drupalCreateContentType($settings = array()) {
+ // find a non-existent random type name.
+ do {
+ $name = strtolower($this->randomName(3, 'type_'));
+ } while (node_get_types('type', $name));
+
+ // Populate defaults array
+ $defaults = array(
+ 'type' => $name,
+ 'name' => $name,
+ 'description' => '',
+ 'help' => '',
+ 'min_word_count' => 0,
+ 'title_label' => 'Title',
+ 'body_label' => 'Body',
+ 'has_title' => 1,
+ 'has_body' => 1,
+ );
+ // imposed values for a custom type
+ $forced = array(
+ 'orig_type' => '',
+ 'old_type' => '',
+ 'module' => 'node',
+ 'custom' => 1,
+ 'modified' => 1,
+ 'locked' => 0,
+ );
+ $type = $forced + $settings + $defaults;
+ $type = (object)$type;
+
+ node_type_save($type);
+ node_types_rebuild();
+
+ return $type;
+ }
+
+ /**
+ * Get a list files that can be used in tests.
+ *
+ * @param string $type File type, possible values: 'binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'.
+ * @param integer $size File size in bytes to match. Please check the tests/files folder.
+ * @return array List of files that match filter.
+ */
+ function drupalGetTestFiles($type, $size = NULL) {
+ $files = array();
+
+ // Make sure type is valid.
+ if (in_array($type, array('binary', 'html', 'image', 'javascript', 'php', 'sql', 'text'))) {
+ // Use original file directory instead of one created during setUp().
+ $path = $this->original_file_directory .'/simpletest';
+ $files = file_scan_directory($path, $type .'\-.*');
+
+ // If size is set then remove any files that are not of that size.
+ if ($size !== NULL) {
+ foreach ($files as $file) {
+ $stats = stat($file->filename);
+ if ($stats['size'] != $size) {
+ unset($files[$file->filename]);
+ }
+ }
+ }
+ }
+ usort($files, array($this, 'drupalCompareFiles'));
+ return $files;
+ }
+
+ /**
+ * Compare two files based on size.
+ */
+ function drupalCompareFiles($file1, $file2) {
+ if (stat($file1->filename) > stat($file2->filename)) {
+ return 1;
+ }
+ return -1;
+ }
+
+ /**
+ * Generates a random string.
+ *
+ * @param integer $number Number of characters in length to append to the prefix.
+ * @param string $prefix Prefix to use.
+ * @return string Randomly generated string.
+ */
+ function randomName($number = 4, $prefix = 'simpletest_') {
+ $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
+ for ($x = 0; $x < $number; $x++) {
+ $prefix .= $chars{mt_rand(0, strlen($chars) - 1)};
+ if ($x == 0) {
+ $chars .= '0123456789';
+ }
+ }
+ return $prefix;
+ }
+
+ /**
+ * Enables a drupal module in the test database. Any module that is not
+ * part of the required core modules needs to be enabled in order to use
+ * it in a test.
+ *
+ * @param string $name Name of the module to enable.
+ * @return boolean Success.
+ */
+ function drupalModuleEnable($name) {
+ if (module_exists($name)) {
+ $this->pass(t('@name module already enabled', array('@name' => $name)), t('Module'));
+ return TRUE;
+ }
+ $this->_modules[$name] = $name;
+ $form_state['values'] = array('status' => $this->_modules, 'op' => t('Save configuration'));
+ drupal_execute('system_modules', $form_state);
+
+ //rebuilding all caches
+ drupal_rebuild_theme_registry();
+ node_types_rebuild();
+ menu_rebuild();
+ cache_clear_all('schema', 'cache');
+ module_rebuild_cache();
+ }
+
+ /**
+ * Disables a drupal module in the test database.
+ *
+ * @param string $name Name of the module.
+ * @return boolean Success.
+ * @see drupalModuleEnable()
+ */
+ function drupalModuleDisable($name) {
+ if (!module_exists($name)) {
+ $this->pass(t('@name module already disabled', array('@name' => $name)), t('Module'));
+ return TRUE;
+ }
+ unset($this->_modules[$key]);
+ $form_state['values'] = array('status' => $this->_modules, 'op' => t('Save configuration'));
+ drupal_execute('system_modules', $form_state);
+
+ //rebuilding all caches
+ drupal_rebuild_theme_registry();
+ node_types_rebuild();
+ menu_rebuild();
+ cache_clear_all('schema', 'cache');
+ module_rebuild_cache();
+ }
+
+ /**
+ * Create a user with a given set of permissions. The permissions correspond to the
+ * names given on the privileges page.
+ *
+ * @param array $permissions Array of permission names to assign to user.
+ * @return A fully loaded user object with pass_raw property, or FALSE if account
+ * creation fails.
+ */
+ function drupalCreateUser($permissions = NULL) {
+ // Create a role with the given permission set.
+ $rid = $this->_drupalCreateRole($permissions);
+ if (!$rid) {
+ return FALSE;
+ }
+
+ // Create a user assigned to that role.
+ $edit = array();
+ $edit['name'] = $this->randomName();
+ $edit['mail'] = $edit['name'] .'@example.com';
+ $edit['roles'] = array($rid => $rid);
+ $edit['pass'] = user_password();
+ $edit['status'] = 1;
+
+ $account = user_save('', $edit);
+
+ $this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login'));
+ if (empty($account->uid)) {
+ return FALSE;
+ }
+
+ // Add the raw password so that we can log in as this user.
+ $account->pass_raw = $edit['pass'];
+ return $account;
+ }
+
+ /**
+ * Internal helper function; Create a role with specified permissions.
+ *
+ * @param array $permissions Array of permission names to assign to role.
+ * @return integer Role ID of newly created role, or FALSE if role creation failed.
+ */
+ private function _drupalCreateRole($permissions = NULL) {
+ // Generate string version of permissions list.
+ if ($permissions === NULL) {
+ $permission_string = 'access comments, access content, post comments, post comments without approval';
+ } else {
+ $permission_string = implode(', ', $permissions);
+ }
+
+ // Create new role.
+ $role_name = $this->randomName();
+ db_query("INSERT INTO {role} (name) VALUES ('%s')", $role_name);
+ $role = db_fetch_object(db_query("SELECT * FROM {role} WHERE name = '%s'", $role_name));
+ $this->assertTrue($role, t('Created role of name: @role_name, id: @rid', array('@role_name' => $role_name, '@rid' => (isset($role->rid) ? $role->rid : t('-n/a-')))), t('Role'));
+ if ($role && !empty($role->rid)) {
+ // Assign permissions to role and mark it for clean-up.
+ db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')", $role->rid, $permission_string);
+ $this->assertTrue(db_affected_rows(), t('Created permissions: @perms', array('@perms' => $permission_string)), t('Role'));
+ return $role->rid;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Logs in a user with the internal browser. If already logged in then logs the current
+ * user out before logging in the specified user. If no user is specified then a new
+ * user will be created and logged in.
+ *
+ * @param object $user User object representing the user to login.
+ * @return object User that was logged in. Useful if no user was passed in order
+ * to retreive the created user.
+ */
+ function drupalLogin($user = NULL) {
+ if ($this->_logged_in) {
+ $this->drupalLogout();
+ }
+
+ if (!isset($user)) {
+ $user = $this->_drupalCreateRole();
+ }
+
+ $edit = array(
+ 'name' => $user->name,
+ 'pass' => $user->pass_raw
+ );
+ $this->drupalPost('user', $edit, t('Log in'));
+
+ $pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login'));
+ $pass = $pass && $this->assertNoText(t('The username %name has been blocked.', array('%name' => $user->name)), t('No blocked message at login page'), t('User login'));
+ $pass = $pass && $this->assertNoText(t('The name %name is a reserved username.', array('%name' => $user->name)), t('No reserved message at login page'), t('User login'));
+
+ $this->_logged_in = $pass;
+
+ return $user;
+ }
+
+ /*
+ * Logs a user out of the internal browser, then check the login page to confirm logout.
+ */
+ function drupalLogout() {
+ // Make a request to the logout page.
+ $this->drupalGet('logout');
+
+ // Load the user page, the idea being if you were properly logged out you should be seeing a login screen.
+ $this->drupalGet('user');
+ $pass = $this->assertField('name', t('Username field found.'), t('Logout'));
+ $pass = $pass && $this->assertField('pass', t('Password field found.'), t('Logout'));
+
+ $this->_logged_in = !$pass;
+ }
+
+ /**
+ * Generates a random database prefix and runs the install scripts on the prefixed database.
+ * After installation many caches are flushed and the internal browser is setup so that the page
+ * requests will run on the new prefix. A temporary files directory is created with the same name
+ * as the database prefix.
+ *
+ * @param ... List modules to enable.
+ */
+ function setUp() {
+ global $db_prefix;
+ $this->db_prefix_original = $db_prefix;
+ $clean_url_original = variable_get('clean_url', 0);
+ $db_prefix = 'simpletest'. mt_rand(1000, 1000000);
+ include_once './includes/install.inc';
+ drupal_install_system();
+ $modules = array_unique(array_merge(func_get_args(), drupal_verify_profile('default', 'en')));
+ drupal_install_modules($modules);
+ $this->_modules = drupal_map_assoc($modules);
+ $this->_modules['system'] = 'system';
+ $task = 'profile';
+ default_profile_tasks($task, '');
+ menu_rebuild();
+ actions_synchronize();
+ _drupal_flush_css_js();
+ variable_set('install_profile', 'default');
+ variable_set('install_task', 'profile-finished');
+ variable_set('clean_url', $clean_url_original);
+
+ // Use temporary files directory with the same prefix as database.
+ $this->original_file_directory = file_directory_path();
+ variable_set('file_directory_path', file_directory_path() .'/'. $db_prefix);
+ file_check_directory(file_directory_path(), TRUE); // Create the files directory.
+ parent::setUp();
+ }
+
+ /**
+ * Delete created files and temporary files directory, delete the tables created by setUp(),
+ * and reset the database prefix.
+ */
+ function tearDown() {
+ global $db_prefix;
+ if (preg_match('/simpletest\d+/', $db_prefix)) {
+ // Delete temporary files directory and reset files directory path.
+// simpletest_clean_temporary_directory(file_directory_path());
+ variable_set('file_directory_path', $this->original_file_directory);
+
+ $schema = drupal_get_schema(NULL, TRUE);
+ $ret = array();
+ foreach ($schema as $name => $table) {
+ db_drop_table($ret, $name);
+ }
+ $db_prefix = $this->db_prefix_original;
+ $this->_logged_in = FALSE;
+ $this->curlClose();
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * Set necessary reporter info.
+ */
+ function run(&$reporter) {
+ $arr = array('class' => get_class($this));
+ if (method_exists($this, 'getInfo')) {
+ $arr = array_merge($arr, $this->getInfo());
+ }
+ $reporter->test_info_stack[] = $arr;
+ parent::run($reporter);
+ array_pop($reporter->test_info_stack);
+ }
+
+ /**
+ * Initializes the cURL connection and gets a session cookie.
+ *
+ * This function will add authentaticon headers as specified in
+ * simpletest_httpauth_username and simpletest_httpauth_pass variables.
+ * Also, see the description of $curl_options among the properties.
+ */
+ protected function curlConnect() {
+ global $base_url, $db_prefix;
+ if (!isset($this->ch)) {
+ $this->ch = curl_init();
+ $curl_options = $this->curl_options + array(
+ CURLOPT_COOKIEJAR => $this->cookie_file,
+ CURLOPT_URL => $base_url,
+ CURLOPT_FOLLOWLOCATION => TRUE,
+ CURLOPT_RETURNTRANSFER => TRUE,
+ );
+ if (preg_match('/simpletest\d+/', $db_prefix)) {
+ $curl_options[CURLOPT_USERAGENT] = $db_prefix;
+ }
+ if (!isset($curl_options[CURLOPT_USERPWD]) && ($auth = variable_get('simpletest_httpauth_username', ''))) {
+ if ($pass = variable_get('simpletest_httpauth_pass', '')) {
+ $auth .= ':'. $pass;
+ }
+ $curl_options[CURLOPT_USERPWD] = $auth;
+ }
+ return $this->curlExec($curl_options);
+ }
+ }
+
+ /**
+ * Peforms a cURL exec with the specified options after calling curlConnect().
+ *
+ * @param array $curl_options Custom cURL options.
+ * @return string Content returned from the exec.
+ */
+ protected function curlExec($curl_options) {
+ $this->curlConnect();
+ $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
+ curl_setopt_array($this->ch, $this->curl_options + $curl_options);
+ $this->_content = curl_exec($this->ch);
+ $this->plain_text = FALSE;
+ $this->elements = FALSE;
+ $this->assertTrue($this->_content, t('!method to !url, response is !length bytes.', array('!method' => isset($curl_options[CURLOPT_POSTFIELDS]) ? 'POST' : 'GET', '!url' => $url, '!length' => strlen($this->_content))), t('Browser'));
+ return $this->_content;
+ }
+
+ /**
+ * Close the cURL handler and unset the handler.
+ */
+ protected function curlClose() {
+ if (isset($this->ch)) {
+ curl_close($this->ch);
+ unset($this->ch);
+ }
+ }
+
+ /**
+ * Parse content returned from curlExec using DOM and simplexml.
+ *
+ * @return SimpleXMLElement A SimpleXMLElement or FALSE on failure.
+ */
+ protected function parse() {
+ if (!$this->elements) {
+ // DOM can load HTML soup. But, HTML soup can throw warnings, supress
+ // them.
+ @$htmlDom = DOMDocument::loadHTML($this->_content);
+ if ($htmlDom) {
+ $this->assertTrue(TRUE, t('Valid HTML found on "@path"', array('@path' => $this->getUrl())), t('Browser'));
+ // It's much easier to work with simplexml than DOM, luckily enough
+ // we can just simply import our DOM tree.
+ $this->elements = simplexml_import_dom($htmlDom);
+ }
+ }
+ return $this->elements;
+ }
+
+ /**
+ * Retrieves a Drupal path or an absolute path.
+ *
+ * @param $path string Drupal path or url to load into internal browser
+ * @param array $options Options to be forwarded to url().
+ * @return The retrieved HTML string, also available as $this->drupalGetContent()
+ */
+ function drupalGet($path, $options = array()) {
+ $options['absolute'] = TRUE;
+ return $this->curlExec(array(CURLOPT_URL => url($path, $options)));
+ }
+
+ /**
+ * Do a post request on a drupal page.
+ * It will be done as usual post request with SimpleBrowser
+ * By $reporting you specify if this request does assertions or not
+ * Warning: empty ("") returns will cause fails with $reporting
+ *
+ * @param string $path
+ * Location of the post form. Either a Drupal path or an absolute path or
+ * NULL to post to the current page.
+ * @param array $edit
+ * Field data in an assocative array. Changes the current input fields
+ * (where possible) to the values indicated. A checkbox can be set to
+ * TRUE to be checked and FALSE to be unchecked.
+ * @param string $submit
+ * Untranslated value, id or name of the submit button.
+ * @param $tamper
+ * If this is set to TRUE then you can post anything, otherwise hidden and
+ * nonexistent fields are not posted.
+ */
+ function drupalPost($path, $edit, $submit, $tamper = FALSE) {
+ $submit_matches = FALSE;
+ if (isset($path)) {
+ $html = $this->drupalGet($path);
+ }
+ if ($this->parse()) {
+ $edit_save = $edit;
+ // Let's iterate over all the forms.
+ $forms = $this->elements->xpath('//form');
+ foreach ($forms as $form) {
+ if ($tamper) {
+ // @TODO: this will be Drupal specific. One needs to add the build_id
+ // and the token to $edit then $post that.
+ }
+ else {
+ // We try to set the fields of this form as specified in $edit.
+ $edit = $edit_save;
+ $post = array();
+ $upload = array();
+ $submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
+ $action = isset($form['action']) ? $this->getAbsoluteUrl($form['action']) : $this->getUrl();
+ }
+ // We post only if we managed to handle every field in edit and the
+ // submit button matches;
+ if (!$edit && $submit_matches) {
+ // This part is not pretty. There is very little I can do.
+ if ($upload) {
+ foreach ($post as &$value) {
+ if (strlen($value) > 0 && $value[0] == '@') {
+ $this->fail(t("Can't upload and post a value starting with @"));
+ return FALSE;
+ }
+ }
+ foreach ($upload as $key => $file) {
+ $post[$key] = '@'. realpath($file);
+ }
+ }
+ else {
+ $post_array = $post;
+ $post = array();
+ foreach ($post_array as $key => $value) {
+ // Whethet this needs to be urlencode or rawurlencode, is not
+ // quite clear, but this seems to be the better choice.
+ $post[] = urlencode($key) .'='. urlencode($value);
+ }
+ $post = implode('&', $post);
+ }
+ return $this->curlExec(array(CURLOPT_URL => $action, CURLOPT_POSTFIELDS => $post));
+ }
+ }
+ // We have not found a form which contained all fields of $edit.
+ $this->fail(t('Found the requested form'));
+ $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit)));
+ foreach ($edit as $name => $value) {
+ $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value)));
+ }
+ }
+ }
+
+ /**
+ * Handle form input related to drupalPost(). Ensure that the specified fields
+ * exist and attempt to create POST data in the correct manor for the particular
+ * field type.
+ *
+ * @param array $post Reference to array of post values.
+ * @param array $edit Reference to array of edit values to be checked against the form.
+ * @param string $submit Form submit button value.
+ * @param array $form Array of form elements.
+ * @return boolean Submit value matches a valid submit input in the form.
+ */
+ protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
+ // Retrieve the form elements.
+ $elements = $form->xpath('.//input|.//textarea|.//select');
+ $submit_matches = FALSE;
+ foreach ($elements as $element) {
+ // SimpleXML objects need string casting all the time.
+ $name = (string)$element['name'];
+ // This can either be the type of <input> or the name of the tag itself
+ // for <select> or <textarea>.
+ $type = isset($element['type']) ? (string)$element['type'] : $element->getName();
+ $value = isset($element['value']) ? (string)$element['value'] : '';
+ $done = FALSE;
+ if (isset($edit[$name])) {
+ switch ($type) {
+ case 'text':
+ case 'textarea':
+ case 'password':
+ $post[$name] = $edit[$name];
+ unset($edit[$name]);
+ break;
+ case 'radio':
+ if ($edit[$name] == $value) {
+ $post[$name] = $edit[$name];
+ unset($edit[$name]);
+ }
+ break;
+ case 'checkbox':
+ // To prevent checkbox from being checked.pass in a FALSE,
+ // otherwise the checkbox will be set to its value regardless
+ // of $edit.
+ if ($edit[$name] === FALSE) {
+ unset($edit[$name]);
+ continue 2;
+ }
+ else {
+ unset($edit[$name]);
+ $post[$name] = $value;
+ }
+ break;
+ case 'select':
+ $new_value = $edit[$name];
+ $index = 0;
+ $key = preg_replace('/\[\]$/', '', $name);
+ $options = $this->getAllOptions($element);
+ foreach ($options as $option) {
+ if (is_array($new_value)) {
+ $option_value= (string)$option['value'];
+ if (in_array($option_value, $new_value)) {
+ $post[$key .'['. $index++ .']'] = $option_value;
+ $done = TRUE;
+ unset($edit[$name]);
+ }
+ }
+ elseif ($new_value == $option['value']) {
+ $post[$name] = $new_value;
+ unset($edit[$name]);
+ $done = TRUE;
+ }
+ }
+ break;
+ case 'file':
+ $upload[$name] = $edit[$name];
+ unset($edit[$name]);
+ break;
+ }
+ }
+ if (!isset($post[$name]) && !$done) {
+ switch ($type) {
+ case 'textarea':
+ $post[$name] = (string)$element;
+ break;
+ case 'select':
+ $single = empty($element['multiple']);
+ $first = TRUE;
+ $index = 0;
+ $key = preg_replace('/\[\]$/', '', $name);
+ $options = $this->getAllOptions($element);
+ foreach ($options as $option) {
+ // For single select, we load the first option, if there is a
+ // selected option that will overwrite it later.
+ if ($option['selected'] || ($first && $single)) {
+ $first = FALSE;
+ if ($single) {
+ $post[$name] = (string)$option['value'];
+ }
+ else {
+ $post[$key .'['. $index++ .']'] = (string)$option['value'];
+ }
+ }
+ }
+ break;
+ case 'file':
+ break;
+ case 'submit':
+ case 'image':
+ if ($submit == $value) {
+ $post[$name] = $value;
+ $submit_matches = TRUE;
+ }
+ break;
+ case 'radio':
+ case 'checkbox':
+ if (!isset($element['checked'])) {
+ break;
+ }
+ // Deliberate no break.
+ default:
+ $post[$name] = $value;
+ }
+ }
+ }
+ return $submit_matches;
+ }
+
+ /**
+ * Get all option elements, including nested options, in a select.
+ *
+ * @param SimpleXMLElement $element
+ * @return array Option elements in select.
+ */
+ private function getAllOptions(SimpleXMLElement $element) {
+ $options = array();
+ // Add all options items.
+ foreach ($element->option as $option) {
+ $options[] = $option;
+ }
+
+ // Search option group children.
+ if (isset($element->optgroup)) {
+ $options = array_merge($options, $this->getAllOptions($element->optgroup));
+ }
+ return $options;
+ }
+
+ /**
+ * Follows a link by name.
+ *
+ * Will click the first link found with this link text by default, or a
+ * later one if an index is given. Match is case insensitive with
+ * normalized space. The label is translated label. There is an assert
+ * for successful click.
+ * WARNING: Assertion fails on empty ("") output from the clicked link.
+ *
+ * @param string $label Text between the anchor tags.
+ * @param integer $index Link position counting from zero.
+ * @param boolean $reporting Assertions or not.
+ * @return boolean/string Page on success.
+ */
+ function clickLink($label, $index = 0) {
+ $url_before = $this->getUrl();
+ $ret = FALSE;
+ if ($this->parse()) {
+ $urls = $this->elements->xpath('//a[text()="'. $label .'"]');
+ if (isset($urls[$index])) {
+ $url_target = $this->getAbsoluteUrl($urls[$index]['href']);
+ $curl_options = array(CURLOPT_URL => $url_target);
+ $ret = $this->curlExec($curl_options);
+ }
+ $this->assertTrue($ret, t('Clicked link !label (!url_target) from !url_before', array('!label' => $label, '!url_target' => $url_target, '!url_before' => $url_before)), t('Browser'));
+ }
+ return $ret;
+ }
+
+ /**
+ * Takes a path and returns an absolute path.
+ *
+ * @param @path
+ * The path, can be a Drupal path or a site-relative path. It might have a
+ * query, too. Can even be an absolute path which is just passed through.
+ * @return
+ * An absolute path.
+ */
+ function getAbsoluteUrl($path) {
+ $options = array('absolute' => TRUE);
+ $parts = parse_url($path);
+ // This is more crude than the menu_is_external but enough here.
+ if (empty($parts['host'])) {
+ $path = $parts['path'];
+ $base_path = base_path();
+ $n = strlen($base_path);
+ if (substr($path, 0, $n) == $base_path) {
+ $path = substr($path, $n);
+ }
+ if (isset($parts['query'])) {
+ $options['query'] = $parts['query'];
+ }
+ $path = url($path, $options);
+ }
+ return $path;
+ }
+
+ /**
+ * Get the current url from the cURL handler.
+ *
+ * @return string current url.
+ */
+ function getUrl() {
+ return curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL);
+ }
+
+ /**
+ * Gets the current raw HTML of requested page.
+ */
+ function drupalGetContent() {
+ return $this->_content;
+ }
+
+ /**
+ * Pass if the raw text IS found on the loaded page, fail otherwise. Raw text
+ * refers to the raw HTML that the page generated.
+ *
+ * @param string $raw Raw string to look for.
+ * @param string $message Message to display.
+ * @return boolean TRUE on pass.
+ */
+ function assertRaw($raw, $message = "%s", $group = 'Other') {
+ return $this->assertFalse(strpos($this->_content, $raw) === FALSE, $message, $group);
+ }
+
+ /**
+ * Pass if the raw text is NOT found on the loaded page, fail otherwise. Raw text
+ * refers to the raw HTML that the page generated.
+ *
+ * @param string $raw Raw string to look for.
+ * @param string $message Message to display.
+ * @return boolean TRUE on pass.
+ */
+ function assertNoRaw($raw, $message = "%s", $group = 'Other') {
+ return $this->assertTrue(strpos($this->_content, $raw) === FALSE, $message, $group);
+ }
+
+ /**
+ * Pass if the text IS found on the text version of the page. The text version
+ * is the equivilent of what a user would see when viewing through a web browser.
+ * In other words the HTML has been filtered out of the contents.
+ *
+ * @param string $raw Text string to look for.
+ * @param string $message Message to display.
+ * @return boolean TRUE on pass.
+ */
+ function assertText($text, $message = '', $group = 'Other') {
+ return $this->assertTextHelper($text, $message, $group = 'Other', FALSE);
+ }
+
+ /**
+ * Pass if the text is NOT found on the text version of the page. The text version
+ * is the equivilent of what a user would see when viewing through a web browser.
+ * In other words the HTML has been filtered out of the contents.
+ *
+ * @param string $raw Text string to look for.
+ * @param string $message Message to display.
+ * @return boolean TRUE on pass.
+ */
+ function assertNoText($text, $message = '', $group = 'Other') {
+ return $this->assertTextHelper($text, $message, $group, TRUE);
+ }
+
+ /**
+ * Filter out the HTML of the page and assert that the plain text us found. Called by
+ * the plain text assertions.
+ *
+ * @param string $text Text to look for.
+ * @param string $message Message to display.
+ * @param boolean $not_exists The assert to make in relation to the text's existance.
+ * @return boolean Assertion result.
+ */
+ protected function assertTextHelper($text, $message, $group, $not_exists) {
+ if ($this->plain_text === FALSE) {
+ $this->plain_text = filter_xss($this->_content, array());
+ }
+ if (!$message) {
+ $message = '"'. $text .'"'. ($not_exists ? ' not found.' : ' found.');
+ }
+ return $this->assertTrue($not_exists == (strpos($this->plain_text, $text) === FALSE), $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the Perl regex pattern is found in the raw content.
+ *
+ * @param string $pattern Perl regex to look for including the regex delimiters.
+ * @param string $message Message to display.
+ * @return boolean True if pass.
+ */
+ function assertPattern($pattern, $message = '%s', $group = 'Other') {
+ return $this->assertTrue(preg_match($pattern, $this->drupalGetContent()), $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the perl regex pattern is not present in raw content.
+ *
+ * @param string $pattern Perl regex to look for including the regex delimiters.
+ * @param string $message Message to display.
+ * @return boolean True if pass.
+ */
+ function assertNoPattern($pattern, $message = '%s', $group = 'Other') {
+ return $this->assertFalse(preg_match($pattern, $this->drupalGetContent()), $message, $group);
+ }
+
+ /**
+ * Pass if the page title is the given string.
+ *
+ * @param $title Text string to look for.
+ * @param $message Message to display.
+ * @return boolean TRUE on pass.
+ */
+ function assertTitle($title, $message, $group = 'Other') {
+ return $this->assertTrue($this->parse() && $this->elements->xpath('//title[text()="'. $title .'"]'), $message, $group);
+ }
+
+ /**
+ * Assert that a field exists in the current page by the given XPath.
+ *
+ * @param string $xpath XPath used to find the field.
+ * @param string $value Value of the field to assert.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertFieldByXPath($xpath, $value, $message, $group = 'Other') {
+ $fields = array();
+ if ($this->parse()) {
+ $fields = $this->elements->xpath($xpath);
+ }
+
+ // If value specified then check array for match.
+ $found = TRUE;
+ if ($value) {
+ $found = FALSE;
+ foreach ($fields as $field) {
+ if ($field['value'] == $value) {
+ $found = TRUE;
+ }
+ }
+ }
+ return $this->assertTrue($fields && $found, $message, $group);
+ }
+
+ /**
+ * Assert that a field does not exists in the current page by the given XPath.
+ *
+ * @param string $xpath XPath used to find the field.
+ * @param string $value Value of the field to assert.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertNoFieldByXPath($xpath, $value, $message, $group = 'Other') {
+ $fields = array();
+ if ($this->parse()) {
+ $fields = $this->elements->xpath($xpath);
+ }
+
+ // If value specified then check array for match.
+ $found = TRUE;
+ if ($value) {
+ $found = FALSE;
+ foreach ($fields as $field) {
+ if ($field['value'] == $value) {
+ $found = TRUE;
+ }
+ }
+ }
+ return $this->assertFalse($fields && $found, $message, $group);
+ }
+
+ /**
+ * Assert that a field exists in the current page with the given name and value.
+ *
+ * @param string $name Name of field to assert.
+ * @param string $value Value of the field to assert.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertFieldByName($name, $value = '', $message = '') {
+ return $this->assertFieldByXPath($this->_constructFieldXpath('name', $name), $value, $message ? $message : t('Found field by name @name', array('@name' => $name)), t('Browser'));
+ }
+
+ /**
+ * Assert that a field does not exists in the current page with the given name and value.
+ *
+ * @param string $name Name of field to assert.
+ * @param string $value Value of the field to assert.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertNoFieldByName($name, $value = '', $message = '') {
+ return $this->assertNoFieldByXPath($this->_constructFieldXpath('name', $name), $value, $message ? $message : t('Did not find field by name @name', array('@name' => $name)), t('Browser'));
+ }
+
+ /**
+ * Assert that a field exists in the current page with the given id and value.
+ *
+ * @param string $id Id of field to assert.
+ * @param string $value Value of the field to assert.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertFieldById($id, $value = '', $message = '') {
+ return $this->assertFieldByXPath($this->_constructFieldXpath('id', $id), $value, $message ? $message : t('Found field by id @id', array('@id' => $id)), t('Browser'));
+ }
+
+ /**
+ * Assert that a field does not exists in the current page with the given id and value.
+ *
+ * @param string $id Id of field to assert.
+ * @param string $value Value of the field to assert.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertNoFieldById($id, $value = '', $message = '') {
+ return $this->assertNoFieldByXPath($this->_constructFieldXpath('id', $id), $value, $message ? $message : t('Did not find field by id @id', array('@id' => $id)), t('Browser'));
+ }
+
+ /**
+ * Assert that a field exists in the current page with the given name or id.
+ *
+ * @param string $field Name or id of the field.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertField($field, $message = '', $group = 'Other') {
+ return $this->assertFieldByXPath($this->_constructFieldXpath('name', $field) .'|'. $this->_constructFieldXpath('id', $field), '', $message, $group);
+ }
+
+ /**
+ * Assert that a field does not exists in the current page with the given name or id.
+ *
+ * @param string $field Name or id of the field.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertNoField($field, $message = '', $group = 'Other') {
+ return $this->assertNoFieldByXPath($this->_constructFieldXpath('name', $field) .'|'. $this->_constructFieldXpath('id', $field), '', $message, $group);
+ }
+
+ /**
+ * Construct an XPath for the given set of attributes and value.
+ *
+ * @param array $attribute Field attributes.
+ * @param string $value Value of field.
+ * @return string XPath for specified values.
+ */
+ function _constructFieldXpath($attribute, $value) {
+ return '//textarea[@'. $attribute .'="'. $value .'"]|//input[@'. $attribute .'="'. $value .'"]|//select[@'. $attribute .'="'. $value .'"]';
+ }
+
+ /**
+ * Assert the page responds with the specified response code.
+ *
+ * @param integer $code Reponse code. For example 200 is a successful page request. For
+ * a list of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
+ * @param string $message Message to display.
+ * @return boolean Assertion result.
+ */
+ function assertResponse($code, $message = '') {
+ $curl_code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
+ $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code;
+ return $this->assertTrue($match, $message ? $message : t('HTTP response expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)), t('Browser'));
+ }
+}
diff --git a/modules/simpletest/dumper.php b/modules/simpletest/dumper.php
new file mode 100644
index 000000000..e2995cb21
--- /dev/null
+++ b/modules/simpletest/dumper.php
@@ -0,0 +1,80 @@
+<?php
+// $Id$
+
+/**
+ * Displays variables as text and does diffs.
+ */
+class SimpleDumper {
+ /**
+ * Renders a variable in a shorter form than print_r().
+ *
+ * @param mixed $value Variable to render as a string.
+ *
+ * @return string Human readable string form.
+ * @access public
+ */
+ function describeValue($value) {
+ $type = $this->getType($value);
+ switch ($type) {
+ case "Null":
+ return "NULL";
+
+ case "Bool":
+ return "Boolean: ". ($value ? "true" : "false");
+
+ case "Array":
+ return "Array: ". count($value) ." items";
+
+ case "Object":
+ return "Object: of ". get_class($value);
+
+ case "String":
+ return "String: ". $this->clipString($value, 200);
+
+ default:
+ return "$type: $value";
+ }
+ }
+
+ /**
+ * Gets the string representation of a type.
+ * @param mixed $value Variable to check against.
+ * @return string Type.
+ * @access public
+ */
+ function getType($value) {
+ if (!isset($value)) {
+ return "Null";
+ }
+ $functions = array('bool', 'string', 'integer', 'float', 'array', 'resource', 'object');
+ foreach ($functions as $function) {
+ $function_name = 'is_'. $function;
+ if ($function_name($value)) {
+ return ucfirst($function);
+ }
+ }
+ return "Unknown";
+ }
+
+ /**
+ * Clips a string to a maximum length.
+ * @param string $value String to truncate.
+ * @param integer $size Minimum string size to show.
+ * @param integer $position Centre of string section.
+ * @return string Shortened version.
+ * @access public
+ */
+ function clipString($value, $size, $position = 0) {
+ $length = strlen($value);
+ if ($length <= $size) {
+ return $value;
+ }
+ $position = min($position, $length);
+ $start = ($size / 2 > $position ? 0 : $position - $size / 2);
+ if ($start + $size > $length) {
+ $start = $length - $size;
+ }
+ $value = substr($value, $start, $size);
+ return ($start > 0 ? "..." : "") . $value . ($start + $size < $length ? "..." : "");
+ }
+}
diff --git a/modules/simpletest/errors.php b/modules/simpletest/errors.php
new file mode 100644
index 000000000..f186ce6ab
--- /dev/null
+++ b/modules/simpletest/errors.php
@@ -0,0 +1,240 @@
+<?php
+// $Id$
+
+/**
+ * Extension that traps errors into an error queue.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleErrorTrappingInvoker extends SimpleInvokerDecorator {
+ /**
+ * Stores the invoker to wrap.
+ * @param SimpleInvoker $invoker Test method runner.
+ */
+ function SimpleErrorTrappingInvoker(&$invoker) {
+ $this->SimpleInvokerDecorator($invoker);
+ }
+
+ /**
+ * Invokes a test method and dispatches any
+ * untrapped errors. Called back from
+ * the visiting runner.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function invoke($method) {
+ $queue = &$this->_createErrorQueue();
+ set_error_handler('SimpleTestErrorHandler');
+ parent::invoke($method);
+ restore_error_handler();
+ $queue->tally();
+ }
+
+ /**
+ * Wires up the error queue for a single test.
+ * @return SimpleErrorQueue Queue connected to the test.
+ * @access private
+ */
+ function & _createErrorQueue() {
+ $context = &SimpleTest::getContext();
+ $test = &$this->getTestCase();
+ $queue = &$context->get('SimpleErrorQueue');
+ $queue->setTestCase($test);
+ return $queue;
+ }
+}
+
+/**
+ * Error queue used to record trapped
+ * errors.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleErrorQueue {
+ var $_queue;
+ var $_expectation_queue;
+ var $_test;
+ var $_using_expect_style = false;
+
+ /**
+ * Starts with an empty queue.
+ */
+ function SimpleErrorQueue() {
+ $this->clear();
+ }
+
+ /**
+ * Discards the contents of the error queue.
+ * @access public
+ */
+ function clear() {
+ $this->_queue = array();
+ $this->_expectation_queue = array();
+ }
+
+ /**
+ * Sets the currently running test case.
+ * @param SimpleTestCase $test Test case to send messages to.
+ * @access public
+ */
+ function setTestCase(&$test) {
+ $this->_test = &$test;
+ }
+
+ /**
+ * Sets up an expectation of an error. If this is
+ * not fulfilled at the end of the test, a failure
+ * will occour. If the error does happen, then this
+ * will cancel it out and send a pass message.
+ * @param SimpleExpectation $expected Expected error match.
+ * @param string $message Message to display.
+ * @access public
+ */
+ function expectError($expected, $message) {
+ $this->_using_expect_style = true;
+ array_push($this->_expectation_queue, array($expected, $message));
+ }
+
+ /**
+ * Adds an error to the front of the queue.
+ * @param integer $severity PHP error code.
+ * @param string $content Text of error.
+ * @param string $filename File error occoured in.
+ * @param integer $line Line number of error.
+ * @access public
+ */
+ function add($severity, $content, $filename, $line) {
+ $content = str_replace('%', '%%', $content);
+ if ($this->_using_expect_style) {
+ $this->_testLatestError($severity, $content, $filename, $line);
+ }
+ else {
+ array_push($this->_queue, array($severity, $content, $filename, $line));
+ }
+ }
+
+ /**
+ * Any errors still in the queue are sent to the test
+ * case. Any unfulfilled expectations trigger failures.
+ * @access public
+ */
+ function tally() {
+ while (list($severity, $message, $file, $line) = $this->extract()) {
+ $severity = $this->getSeverityAsString($severity);
+ $this->_test->error($severity, $message, $file, $line);
+ }
+ while (list($expected, $message) = $this->_extractExpectation()) {
+ $this->_test->assert($expected, false, "%s -> Expected error not caught");
+ }
+ }
+
+ /**
+ * Tests the error against the most recent expected
+ * error.
+ * @param integer $severity PHP error code.
+ * @param string $content Text of error.
+ * @param string $filename File error occoured in.
+ * @param integer $line Line number of error.
+ * @access private
+ */
+ function _testLatestError($severity, $content, $filename, $line) {
+ if ($expectation = $this->_extractExpectation()) {
+ list($expected, $message) = $expectation;
+ $this->_test->assert($expected, $content, sprintf(
+ $message,
+ "%s -> PHP error [$content] severity [".
+ $this->getSeverityAsString($severity) .
+ "] in [$filename] line [$line]"));
+ }
+ else {
+ $this->_test->error($severity, $content, $filename, $line);
+ }
+ }
+
+ /**
+ * Pulls the earliest error from the queue.
+ * @return mixed False if none, or a list of error
+ * information. Elements are: severity
+ * as the PHP error code, the error message,
+ * the file with the error, the line number
+ * and a list of PHP super global arrays.
+ * @access public
+ */
+ function extract() {
+ if (count($this->_queue)) {
+ return array_shift($this->_queue);
+ }
+ return false;
+ }
+
+ /**
+ * Pulls the earliest expectation from the queue.
+ * @return SimpleExpectation False if none.
+ * @access private
+ */
+ function _extractExpectation() {
+ if (count($this->_expectation_queue)) {
+ return array_shift($this->_expectation_queue);
+ }
+ return false;
+ }
+
+ /**
+ * Converts an error code into it's string
+ * representation.
+ * @param $severity PHP integer error code.
+ * @return String version of error code.
+ * @access public
+ * @static
+ */
+ function getSeverityAsString($severity) {
+ static $map = array(
+ E_STRICT => 'E_STRICT',
+ E_ERROR => 'E_ERROR',
+ E_WARNING => 'E_WARNING',
+ E_PARSE => 'E_PARSE',
+ E_NOTICE => 'E_NOTICE',
+ E_CORE_ERROR => 'E_CORE_ERROR',
+ E_CORE_WARNING => 'E_CORE_WARNING',
+ E_COMPILE_ERROR => 'E_COMPILE_ERROR',
+ E_COMPILE_WARNING => 'E_COMPILE_WARNING',
+ E_USER_ERROR => 'E_USER_ERROR',
+ E_USER_WARNING => 'E_USER_WARNING',
+ E_USER_NOTICE => 'E_USER_NOTICE'
+ );
+ if (version_compare(phpversion(), '5.2.0', '>=')) {
+ $map[E_RECOVERABLE_ERROR] = 'E_RECOVERABLE_ERROR';
+ }
+ return $map[$severity];
+ }
+}
+
+/**
+ * Error handler that simply stashes any errors into the global
+ * error queue. Simulates the existing behaviour with respect to
+ * logging errors, but this feature may be removed in future.
+ * @param $severity PHP error code.
+ * @param $message Text of error.
+ * @param $filename File error occoured in.
+ * @param $line Line number of error.
+ * @param $super_globals Hash of PHP super global arrays.
+ * @static
+ * @access public
+ */
+function SimpleTestErrorHandler($severity, $message, $filename = null, $line = null, $super_globals = null, $mask = null) {
+ $severity = $severity & error_reporting();
+ if ($severity) {
+ restore_error_handler();
+ if (ini_get('log_errors')) {
+ $label = SimpleErrorQueue::getSeverityAsString($severity);
+ error_log("$label: $message in $filename on line $line");
+ }
+ $context = &SimpleTest::getContext();
+ $queue = &$context->get('SimpleErrorQueue');
+ $queue->add($severity, $message, $filename, $line);
+ set_error_handler('SimpleTestErrorHandler');
+ }
+ return true;
+}
+
+
diff --git a/modules/simpletest/exceptions.php b/modules/simpletest/exceptions.php
new file mode 100644
index 000000000..c79aaf318
--- /dev/null
+++ b/modules/simpletest/exceptions.php
@@ -0,0 +1,171 @@
+<?php
+// $Id$
+
+class SimpleExceptionTrappingInvoker extends SimpleInvokerDecorator {
+
+ /**
+ * Stores the invoker to be wrapped.
+ * @param SimpleInvoker $invoker Test method runner.
+ */
+ function SimpleExceptionTrappingInvoker($invoker) {
+ $this->SimpleInvokerDecorator($invoker);
+ }
+
+ /**
+ * Invokes a test method whilst trapping expected
+ * exceptions. Any left over unthrown exceptions
+ * are then reported as failures.
+ * @param string $method Test method to call.
+ */
+ function invoke($method) {
+ $trap = SimpleTest::getContext()->get('SimpleExceptionTrap');
+ $trap->clear();
+ try {
+ parent::invoke($method);
+ }
+ catch (Exception $exception) {
+ if (!$trap->isExpected($this->getTestCase(), $exception)) {
+ $this->getTestCase()->exception($exception);
+ }
+ $trap->clear();
+ $this->_invoker->getTestCase()->tearDown();
+ }
+ if ($message = $trap->getOutstanding()) {
+ $this->getTestCase()->fail($message);
+ }
+ }
+}
+
+/**
+ * Tests exceptions either by type or the exact
+ * exception. This could be improved to accept
+ * a pattern expectation to test the error
+ * message, but that will have to come later.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class ExceptionExpectation extends SimpleExpectation {
+ private$expected;
+
+ /**
+ * Sets up the conditions to test against.
+ * If the expected value is a string, then
+ * it will act as a test of the class name.
+ * An exception as the comparison will
+ * trigger an identical match. Writing this
+ * down now makes it look doubly dumb. I hope
+ * come up with a better scheme later.
+ * @param mixed $expected A class name or an actual
+ * exception to compare with.
+ * @param string $message Message to display.
+ */
+ function __construct($expected, $message = '%s') {
+ $this->expected = $expected;
+ parent::__construct($message);
+ }
+
+ /**
+ * Carry out the test.
+ * @param Exception $compare Value to check.
+ * @return boolean True if matched.
+ */
+ function test($compare) {
+ if (is_string($this->expected)) {
+ return ($compare instanceof $this->expected);
+ }
+ if (get_class($compare) != get_class($this->expected)) {
+ return false;
+ }
+ return $compare->getMessage() == $this->expected->getMessage();
+ }
+
+ /**
+ * Create the message to display describing the test.
+ * @param Exception $compare Exception to match.
+ * @return string Final message.
+ */
+ function testMessage($compare) {
+ if (is_string($this->expected)) {
+ return "Exception [". $this->describeException($compare) ."] should be type [". $this->expected ."]";
+ }
+ return "Exception [". $this->describeException($compare) ."] should match [". $this->describeException($this->expected) ."]";
+ }
+
+ /**
+ * Summary of an Exception object.
+ * @param Exception $compare Exception to describe.
+ * @return string Text description.
+ */
+ protected function describeException($exception) {
+ return get_class($exception) .": ". $exception->getMessage();
+ }
+}
+
+/**
+ * Stores expected exceptions for when they
+ * get thrown. Saves the irritating try...catch
+ * block.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleExceptionTrap {
+ private$expected;
+ private$message;
+
+ /**
+ * Clears down the queue ready for action.
+ */
+ function __construct() {
+ $this->clear();
+ }
+
+ /**
+ * Sets up an expectation of an exception.
+ * This has the effect of intercepting an
+ * exception that matches.
+ * @param SimpleExpectation $expected Expected exception to match.
+ * @param string $message Message to display.
+ * @access public
+ */
+ function expectException($expected = false, $message = '%s') {
+ if ($expected === false) {
+ $expected = new AnythingExpectation();
+ }
+ if (!SimpleExpectation::isExpectation($expected)) {
+ $expected = new ExceptionExpectation($expected);
+ }
+ $this->expected = $expected;
+ $this->message = $message;
+ }
+
+ /**
+ * Compares the expected exception with any
+ * in the queue. Issues a pass or fail and
+ * returns the state of the test.
+ * @param SimpleTestCase $test Test case to send messages to.
+ * @param Exception $exception Exception to compare.
+ * @return boolean False on no match.
+ */
+ function isExpected($test, $exception) {
+ if ($this->expected) {
+ return $test->assert($this->expected, $exception, $this->message);
+ }
+ return false;
+ }
+
+ /**
+ * Tests for any left over exception.
+ * @return string/false The failure message or false if none.
+ */
+ function getOutstanding() {
+ return sprintf($this->message, 'Failed to trap exception');
+ }
+
+ /**
+ * Discards the contents of the error queue.
+ */
+ function clear() {
+ $this->expected = false;
+ $this->message = false;
+ }
+}
diff --git a/modules/simpletest/expectation.php b/modules/simpletest/expectation.php
new file mode 100644
index 000000000..1dd85c9ac
--- /dev/null
+++ b/modules/simpletest/expectation.php
@@ -0,0 +1,144 @@
+<?php
+// $Id$
+
+/**
+ * Assertion that can display failure information.
+ * Also includes various helper methods.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ * @abstract
+ */
+class SimpleExpectation {
+ var $_dumper = false;
+ var $_message;
+
+ /**
+ * Creates a dumper for displaying values and sets
+ * the test message.
+ * @param string $message Customised message on failure.
+ */
+ function SimpleExpectation($message = '%s') {
+ $this->_message = $message;
+ }
+
+ /**
+ * Tests the expectation. True if correct.
+ * @param mixed $compare Comparison value.
+ * @return boolean True if correct.
+ * @access public
+ * @abstract
+ */
+ function test($compare) {}
+
+ /**
+ * Returns a human readable test message.
+ * @param mixed $compare Comparison value.
+ * @return string Description of success
+ * or failure.
+ * @access public
+ * @abstract
+ */
+ function testMessage($compare) {}
+
+ /**
+ * Overlays the generated message onto the stored user
+ * message. An additional message can be interjected.
+ * @param mixed $compare Comparison value.
+ * @param SimpleDumper $dumper For formatting the results.
+ * @return string Description of success
+ * or failure.
+ * @access public
+ */
+ function overlayMessage($compare, $dumper) {
+ $this->_dumper = $dumper;
+ return sprintf($this->_message, $this->testMessage($compare));
+ }
+
+ /**
+ * Accessor for the dumper.
+ * @return SimpleDumper Current value dumper.
+ * @access protected
+ */
+ function &_getDumper() {
+ if (!$this->_dumper) {
+ $dumper = &new SimpleDumper();
+ return $dumper;
+ }
+ return $this->_dumper;
+ }
+
+ /**
+ * Test to see if a value is an expectation object.
+ * A useful utility method.
+ * @param mixed $expectation Hopefully an Epectation
+ * class.
+ * @return boolean True if descended from
+ * this class.
+ * @access public
+ * @static
+ */
+ function isExpectation($expectation) {
+ return is_object($expectation) && is_a($expectation, 'SimpleExpectation');
+ }
+}
+
+/**
+ * A wildcard expectation always matches.
+ * @package SimpleTest
+ * @subpackage MockObjects
+ */
+class AnythingExpectation extends SimpleExpectation {
+
+ /**
+ * Tests the expectation. Always true.
+ * @param mixed $compare Ignored.
+ * @return boolean True.
+ * @access public
+ */
+ function test($compare) {
+ return true;
+ }
+
+ /**
+ * Returns a human readable test message.
+ * @param mixed $compare Comparison value.
+ * @return string Description of success
+ * or failure.
+ * @access public
+ */
+ function testMessage($compare) {
+ $dumper = &$this->_getDumper();
+ return 'Anything always matches ['. $dumper->describeValue($compare) .']';
+ }
+}
+
+/**
+ * An expectation that passes on boolean true.
+ * @package SimpleTest
+ * @subpackage MockObjects
+ */
+class TrueExpectation extends SimpleExpectation {
+
+ /**
+ * Tests the expectation.
+ * @param mixed $compare Should be true.
+ * @return boolean True on match.
+ * @access public
+ */
+ function test($compare) {
+ return (boolean)$compare;
+ }
+
+ /**
+ * Returns a human readable test message.
+ * @param mixed $compare Comparison value.
+ * @return string Description of success
+ * or failure.
+ * @access public
+ */
+ function testMessage($compare) {
+ $dumper = &$this->_getDumper();
+ return 'Expected true, got ['. $dumper->describeValue($compare) .']';
+ }
+}
+
diff --git a/modules/simpletest/invoker.php b/modules/simpletest/invoker.php
new file mode 100644
index 000000000..7944c1b4f
--- /dev/null
+++ b/modules/simpletest/invoker.php
@@ -0,0 +1,119 @@
+<?php
+// $Id$
+
+/**
+ * This is called by the class runner to run a
+ * single test method. Will also run the setUp()
+ * and tearDown() methods.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleInvoker {
+ var $_test_case;
+
+ /**
+ * Stashes the test case for later.
+ * @param SimpleTestCase $test_case Test case to run.
+ */
+ function SimpleInvoker(&$test_case) {
+ $this->_test_case = &$test_case;
+ }
+
+ /**
+ * Accessor for test case being run.
+ * @return SimpleTestCase Test case.
+ * @access public
+ */
+ function &getTestCase() {
+ return $this->_test_case;
+ }
+
+ /**
+ * Runs test level set up. Used for changing
+ * the mechanics of base test cases.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function before($method) {
+ $this->_test_case->before($method);
+ }
+
+ /**
+ * Invokes a test method and buffered with setUp()
+ * and tearDown() calls.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function invoke($method) {
+ $this->_test_case->setUp();
+ $this->_test_case->$method();
+ $this->_test_case->tearDown();
+ }
+
+ /**
+ * Runs test level clean up. Used for changing
+ * the mechanics of base test cases.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function after($method) {
+ $this->_test_case->after($method);
+ }
+}
+
+/**
+ * Do nothing decorator. Just passes the invocation
+ * straight through.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleInvokerDecorator {
+ var $_invoker;
+
+ /**
+ * Stores the invoker to wrap.
+ * @param SimpleInvoker $invoker Test method runner.
+ */
+ function SimpleInvokerDecorator(&$invoker) {
+ $this->_invoker = &$invoker;
+ }
+
+ /**
+ * Accessor for test case being run.
+ * @return SimpleTestCase Test case.
+ * @access public
+ */
+ function &getTestCase() {
+ return $this->_invoker->getTestCase();
+ }
+
+ /**
+ * Runs test level set up. Used for changing
+ * the mechanics of base test cases.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function before($method) {
+ $this->_invoker->before($method);
+ }
+
+ /**
+ * Invokes a test method and buffered with setUp()
+ * and tearDown() calls.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function invoke($method) {
+ $this->_invoker->invoke($method);
+ }
+
+ /**
+ * Runs test level clean up. Used for changing
+ * the mechanics of base test cases.
+ * @param string $method Test method to call.
+ * @access public
+ */
+ function after($method) {
+ $this->_invoker->after($method);
+ }
+}
diff --git a/modules/simpletest/reporter.php b/modules/simpletest/reporter.php
new file mode 100644
index 000000000..c6bca2fd7
--- /dev/null
+++ b/modules/simpletest/reporter.php
@@ -0,0 +1,283 @@
+<?php
+// $Id$
+
+/**
+ * Sample minimal test displayer. Generates only
+ * failure messages and a pass count.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class HtmlReporter extends SimpleReporter {
+ var $_character_set;
+
+ /**
+ * Does nothing yet. The first output will
+ * be sent on the first test start. For use
+ * by a web browser.
+ * @access public
+ */
+ function HtmlReporter($character_set = 'ISO-8859-1') {
+ $this->SimpleReporter();
+ $this->_character_set = $character_set;
+ }
+
+ /**
+ * Paints the top of the web page setting the
+ * title to the name of the starting test.
+ * @param string $test_name Name class of test.
+ * @access public
+ */
+ function paintHeader($test_name) {
+ $this->sendNoCacheHeaders();
+ print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">";
+ print "<html>\n<head>\n<title>$test_name</title>\n";
+ print "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=". $this->_character_set ."\">\n";
+ print "<style type=\"text/css\">\n";
+ print $this->_getCss() ."\n";
+ print "</style>\n";
+ print "</head>\n<body>\n";
+ print "<h1>$test_name</h1>\n";
+ flush();
+ }
+
+ /**
+ * Send the headers necessary to ensure the page is
+ * reloaded on every request. Otherwise you could be
+ * scratching your head over out of date test data.
+ * @access public
+ * @static
+ */
+ function sendNoCacheHeaders() {
+ if (!headers_sent()) {
+ header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+ header("Last-Modified: ". gmdate("D, d M Y H:i:s") ." GMT");
+ header("Cache-Control: no-store, no-cache, must-revalidate");
+ header("Cache-Control: post-check=0, pre-check=0", false);
+ header("Pragma: no-cache");
+ }
+ }
+
+ /**
+ * Paints the CSS. Add additional styles here.
+ * @return string CSS code as text.
+ * @access protected
+ */
+ function _getCss() {
+ return ".fail { background-color: inherit; color: red; }".".pass { background-color: inherit; color: green; }"." pre { background-color: lightgray; color: inherit; }";
+ }
+
+ /**
+ * Paints the end of the test with a summary of
+ * the passes and failures.
+ * @param string $test_name Name class of test.
+ * @access public
+ */
+ function paintFooter($test_name) {
+ $colour = ($this->getFailCount() + $this->getExceptionCount() > 0 ? "red" : "green");
+ print "<div style=\"";
+ print "padding: 8px; margin-top: 1em; background-color: $colour; color: white;";
+ print "\">";
+ print $this->getTestCaseProgress() ."/". $this->getTestCaseCount();
+ print " test cases complete:\n";
+ print "<strong>". $this->getPassCount() ."</strong> passes, ";
+ print "<strong>". $this->getFailCount() ."</strong> fails and ";
+ print "<strong>". $this->getExceptionCount() ."</strong> exceptions.";
+ print "</div>\n";
+ print "</body>\n</html>\n";
+ }
+
+ /**
+ * Paints the test failure with a breadcrumbs
+ * trail of the nesting test suites below the
+ * top level test.
+ * @param string $message Failure message displayed in
+ * the context of the other tests.
+ * @access public
+ */
+ function paintFail($message) {
+ parent::paintFail($message);
+ print "<span class=\"fail\">Fail</span>: ";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print implode(" -&gt; ", $breadcrumb);
+ print " -&gt; ". $this->_htmlEntities($message) ."<br />\n";
+ }
+
+ /**
+ * Paints a PHP error.
+ * @param string $message Message is ignored.
+ * @access public
+ */
+ function paintError($message) {
+ parent::paintError($message);
+ print "<span class=\"fail\">Exception</span>: ";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print implode(" -&gt; ", $breadcrumb);
+ print " -&gt; <strong>". $this->_htmlEntities($message) ."</strong><br />\n";
+ }
+
+ /**
+ * Paints a PHP exception.
+ * @param Exception $exception Exception to display.
+ * @access public
+ */
+ function paintException($exception) {
+ parent::paintException($exception);
+ print "<span class=\"fail\">Exception</span>: ";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print implode(" -&gt; ", $breadcrumb);
+ $message = 'Unexpected exception of type ['. get_class($exception) .'] with message ['. $exception->getMessage() .'] in ['. $exception->getFile() .' line '. $exception->getLine() .']';
+ print " -&gt; <strong>". $this->_htmlEntities($message) ."</strong><br />\n";
+ }
+
+ /**
+ * Prints the message for skipping tests.
+ * @param string $message Text of skip condition.
+ * @access public
+ */
+ function paintSkip($message) {
+ parent::paintSkip($message);
+ print "<span class=\"pass\">Skipped</span>: ";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print implode(" -&gt; ", $breadcrumb);
+ print " -&gt; ". $this->_htmlEntities($message) ."<br />\n";
+ }
+
+ /**
+ * Paints formatted text such as dumped variables.
+ * @param string $message Text to show.
+ * @access public
+ */
+ function paintFormattedMessage($message) {
+ print '<pre>'. $this->_htmlEntities($message) .'</pre>';
+ }
+
+ /**
+ * Character set adjusted entity conversion.
+ * @param string $message Plain text or Unicode message.
+ * @return string Browser readable message.
+ * @access protected
+ */
+ function _htmlEntities($message) {
+ return htmlentities($message, ENT_COMPAT, $this->_character_set);
+ }
+}
+
+/**
+ * Sample minimal test displayer. Generates only
+ * failure messages and a pass count. For command
+ * line use. I've tried to make it look like JUnit,
+ * but I wanted to output the errors as they arrived
+ * which meant dropping the dots.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class TextReporter extends SimpleReporter {
+
+ /**
+ * Does nothing yet. The first output will
+ * be sent on the first test start.
+ * @access public
+ */
+ function TextReporter() {
+ $this->SimpleReporter();
+ }
+
+ /**
+ * Paints the title only.
+ * @param string $test_name Name class of test.
+ * @access public
+ */
+ function paintHeader($test_name) {
+ if (!SimpleReporter::inCli()) {
+ header('Content-type: text/plain');
+ }
+ print "$test_name\n";
+ flush();
+ }
+
+ /**
+ * Paints the end of the test with a summary of
+ * the passes and failures.
+ * @param string $test_name Name class of test.
+ * @access public
+ */
+ function paintFooter($test_name) {
+ if ($this->getFailCount() + $this->getExceptionCount() == 0) {
+ print "OK\n";
+ }
+ else {
+ print "FAILURES!!!\n";
+ }
+ print "Test cases run: ". $this->getTestCaseProgress() ."/". $this->getTestCaseCount() .", Passes: ". $this->getPassCount() .", Failures: ". $this->getFailCount() .", Exceptions: ". $this->getExceptionCount() ."\n";
+ }
+
+ /**
+ * Paints the test failure as a stack trace.
+ * @param string $message Failure message displayed in
+ * the context of the other tests.
+ * @access public
+ */
+ function paintFail($message) {
+ parent::paintFail($message);
+ print $this->getFailCount() .") $message\n";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print "\tin ". implode("\n\tin ", array_reverse($breadcrumb));
+ print "\n";
+ }
+
+ /**
+ * Paints a PHP error or exception.
+ * @param string $message Message to be shown.
+ * @access public
+ * @abstract
+ */
+ function paintError($message) {
+ parent::paintError($message);
+ print "Exception ". $this->getExceptionCount() ."!\n$message\n";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print "\tin ". implode("\n\tin ", array_reverse($breadcrumb));
+ print "\n";
+ }
+
+ /**
+ * Paints a PHP error or exception.
+ * @param Exception $exception Exception to describe.
+ * @access public
+ * @abstract
+ */
+ function paintException($exception) {
+ parent::paintException($exception);
+ $message = 'Unexpected exception of type ['. get_class($exception) .'] with message ['. $exception->getMessage() .'] in ['. $exception->getFile() .' line '. $exception->getLine() .']';
+ print "Exception ". $this->getExceptionCount() ."!\n$message\n";
+ $breadcrumb = $this->getTestList();
+ array_shift($breadcrumb);
+ print "\tin ". implode("\n\tin ", array_reverse($breadcrumb));
+ print "\n";
+ }
+
+ /**
+ * Prints the message for skipping tests.
+ * @param string $message Text of skip condition.
+ * @access public
+ */
+ function paintSkip($message) {
+ parent::paintSkip($message);
+ print "Skip: $message\n";
+ }
+
+ /**
+ * Paints formatted text such as dumped variables.
+ * @param string $message Text to show.
+ * @access public
+ */
+ function paintFormattedMessage($message) {
+ print "$message\n";
+ flush();
+ }
+}
diff --git a/modules/simpletest/scorer.php b/modules/simpletest/scorer.php
new file mode 100644
index 000000000..2215d1dff
--- /dev/null
+++ b/modules/simpletest/scorer.php
@@ -0,0 +1,413 @@
+<?php
+// $Id$
+
+/**
+ * Can recieve test events and display them. Display
+ * is achieved by making display methods available
+ * and visiting the incoming event.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ * @abstract
+ */
+class SimpleScorer {
+ var$_passes;
+ var$_fails;
+ var$_exceptions;
+ var$_is_dry_run;
+
+ /**
+ * Starts the test run with no results.
+ * @access public
+ */
+ function SimpleScorer() {
+ $this->_passes = 0;
+ $this->_fails = 0;
+ $this->_exceptions = 0;
+ $this->_is_dry_run = false;
+ }
+
+ /**
+ * Signals that the next evaluation will be a dry
+ * run. That is, the structure events will be
+ * recorded, but no tests will be run.
+ * @param boolean $is_dry Dry run if true.
+ * @access public
+ */
+ function makeDry($is_dry = true) {
+ $this->_is_dry_run = $is_dry;
+ }
+
+ /**
+ * The reporter has a veto on what should be run.
+ * @param string $test_case_name name of test case.
+ * @param string $method Name of test method.
+ * @access public
+ */
+ function shouldInvoke($test_case_name, $method) {
+ return !$this->_is_dry_run;
+ }
+
+ /**
+ * Can wrap the invoker in preperation for running
+ * a test.
+ * @param SimpleInvoker $invoker Individual test runner.
+ * @return SimpleInvoker Wrapped test runner.
+ * @access public
+ */
+ function & createInvoker(&$invoker) {
+ return $invoker;
+ }
+
+ /**
+ * Accessor for current status. Will be false
+ * if there have been any failures or exceptions.
+ * Used for command line tools.
+ * @return boolean True if no failures.
+ * @access public
+ */
+ function getStatus() {
+ if ($this->_exceptions + $this->_fails > 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Paints the start of a group test.
+ * @param string $test_name Name of test or other label.
+ * @param integer $size Number of test cases starting.
+ * @access public
+ */
+ function paintGroupStart($test_name, $size) {}
+
+ /**
+ * Paints the end of a group test.
+ * @param string $test_name Name of test or other label.
+ * @access public
+ */
+ function paintGroupEnd($test_name) {}
+
+ /**
+ * Paints the start of a test case.
+ * @param string $test_name Name of test or other label.
+ * @access public
+ */
+ function paintCaseStart($test_name) {}
+
+ /**
+ * Paints the end of a test case.
+ * @param string $test_name Name of test or other label.
+ * @access public
+ */
+ function paintCaseEnd($test_name) {}
+
+ /**
+ * Paints the start of a test method.
+ * @param string $test_name Name of test or other label.
+ * @access public
+ */
+ function paintMethodStart($test_name) {}
+
+ /**
+ * Paints the end of a test method.
+ * @param string $test_name Name of test or other label.
+ * @access public
+ */
+ function paintMethodEnd($test_name) {}
+
+ /**
+ * Increments the pass count.
+ * @param string $message Message is ignored.
+ * @access public
+ */
+ function paintPass($message) {
+ $this->_passes++;
+ }
+
+ /**
+ * Increments the fail count.
+ * @param string $message Message is ignored.
+ * @access public
+ */
+ function paintFail($message) {
+ $this->_fails++;
+ }
+
+ /**
+ * Deals with PHP 4 throwing an error.
+ * @param string $message Text of error formatted by
+ * the test case.
+ * @access public
+ */
+ function paintError($message) {
+ $this->_exceptions++;
+ }
+
+ /**
+ * Deals with PHP 5 throwing an exception.
+ * @param Exception $exception The actual exception thrown.
+ * @access public
+ */
+ function paintException($exception) {
+ $this->_exceptions++;
+ }
+
+ /**
+ * Prints the message for skipping tests.
+ * @param string $message Text of skip condition.
+ * @access public
+ */
+ function paintSkip($message) {}
+
+ /**
+ * Accessor for the number of passes so far.
+ * @return integer Number of passes.
+ * @access public
+ */
+ function getPassCount() {
+ return $this->_passes;
+ }
+
+ /**
+ * Accessor for the number of fails so far.
+ * @return integer Number of fails.
+ * @access public
+ */
+ function getFailCount() {
+ return $this->_fails;
+ }
+
+ /**
+ * Accessor for the number of untrapped errors
+ * so far.
+ * @return integer Number of exceptions.
+ * @access public
+ */
+ function getExceptionCount() {
+ return $this->_exceptions;
+ }
+
+ /**
+ * Paints a simple supplementary message.
+ * @param string $message Text to display.
+ * @access public
+ */
+ function paintMessage($message) {}
+
+ /**
+ * Paints a formatted ASCII message such as a
+ * variable dump.
+ * @param string $message Text to display.
+ * @access public
+ */
+ function paintFormattedMessage($message) {}
+
+ /**
+ * By default just ignores user generated events.
+ * @param string $type Event type as text.
+ * @param mixed $payload Message or object.
+ * @access public
+ */
+ function paintSignal($type, $payload) {}
+}
+
+/**
+ * Recipient of generated test messages that can display
+ * page footers and headers. Also keeps track of the
+ * test nesting. This is the main base class on which
+ * to build the finished test (page based) displays.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleReporter extends SimpleScorer {
+ var$_test_stack;
+ var$_size;
+ var$_progress;
+
+ /**
+ * Starts the display with no results in.
+ * @access public
+ */
+ function SimpleReporter() {
+ $this->SimpleScorer();
+ $this->_test_stack = array();
+ $this->_size = null;
+ $this->_progress = 0;
+ }
+
+ /**
+ * Gets the formatter for variables and other small
+ * generic data items.
+ * @return SimpleDumper Formatter.
+ * @access public
+ */
+ function getDumper() {
+ return new SimpleDumper();
+ }
+
+ /**
+ * Paints the start of a group test. Will also paint
+ * the page header and footer if this is the
+ * first test. Will stash the size if the first
+ * start.
+ * @param string $test_name Name of test that is starting.
+ * @param integer $size Number of test cases starting.
+ * @access public
+ */
+ function paintGroupStart($test_name, $size) {
+ if (!isset($this->_size)) {
+ $this->_size = $size;
+ }
+ if (count($this->_test_stack) == 0) {
+ $this->paintHeader($test_name);
+ }
+ $this->_test_stack[] = $test_name;
+ }
+
+ /**
+ * Paints the end of a group test. Will paint the page
+ * footer if the stack of tests has unwound.
+ * @param string $test_name Name of test that is ending.
+ * @param integer $progress Number of test cases ending.
+ * @access public
+ */
+ function paintGroupEnd($test_name) {
+ array_pop($this->_test_stack);
+ if (count($this->_test_stack) == 0) {
+ $this->paintFooter($test_name);
+ }
+ }
+
+ /**
+ * Paints the start of a test case. Will also paint
+ * the page header and footer if this is the
+ * first test. Will stash the size if the first
+ * start.
+ * @param string $test_name Name of test that is starting.
+ * @access public
+ */
+ function paintCaseStart($test_name) {
+ if (!isset($this->_size)) {
+ $this->_size = 1;
+ }
+ if (count($this->_test_stack) == 0) {
+ $this->paintHeader($test_name);
+ }
+ $this->_test_stack[] = $test_name;
+ }
+
+ /**
+ * Paints the end of a test case. Will paint the page
+ * footer if the stack of tests has unwound.
+ * @param string $test_name Name of test that is ending.
+ * @access public
+ */
+ function paintCaseEnd($test_name) {
+ $this->_progress++;
+ array_pop($this->_test_stack);
+ if (count($this->_test_stack) == 0) {
+ $this->paintFooter($test_name);
+ }
+ }
+
+ /**
+ * Paints the start of a test method.
+ * @param string $test_name Name of test that is starting.
+ * @access public
+ */
+ function paintMethodStart($test_name) {
+ $this->_test_stack[] = $test_name;
+ }
+
+ /**
+ * Paints the end of a test method. Will paint the page
+ * footer if the stack of tests has unwound.
+ * @param string $test_name Name of test that is ending.
+ * @access public
+ */
+ function paintMethodEnd($test_name) {
+ array_pop($this->_test_stack);
+ }
+
+ /**
+ * Paints the test document header.
+ * @param string $test_name First test top level
+ * to start.
+ * @access public
+ * @abstract
+ */
+ function paintHeader($test_name) {}
+
+ /**
+ * Paints the test document footer.
+ * @param string $test_name The top level test.
+ * @access public
+ * @abstract
+ */
+ function paintFooter($test_name) {}
+
+ /**
+ * Accessor for internal test stack. For
+ * subclasses that need to see the whole test
+ * history for display purposes.
+ * @return array List of methods in nesting order.
+ * @access public
+ */
+ function getTestList() {
+ return $this->_test_stack;
+ }
+
+ /**
+ * Accessor for total test size in number
+ * of test cases. Null until the first
+ * test is started.
+ * @return integer Total number of cases at start.
+ * @access public
+ */
+ function getTestCaseCount() {
+ return $this->_size;
+ }
+
+ /**
+ * Accessor for the number of test cases
+ * completed so far.
+ * @return integer Number of ended cases.
+ * @access public
+ */
+ function getTestCaseProgress() {
+ return $this->_progress;
+ }
+
+ /**
+ * Static check for running in the comand line.
+ * @return boolean True if CLI.
+ * @access public
+ * @static
+ */
+ function inCli() {
+ return php_sapi_name() == 'cli';
+ }
+}
+
+/**
+ * For modifying the behaviour of the visual reporters.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleReporterDecorator {
+ var $_reporter;
+
+ /**
+ * Mediates between the reporter and the test case.
+ * @param SimpleScorer $reporter Reporter to receive events.
+ */
+ function __construct(&$reporter) {
+ $this->_reporter = &$reporter;
+ }
+
+ function __call($method, $arguments) {
+ if (method_exists($this->_reporter, $method)) {
+ return call_user_func_array($this->_reporter->$method, $arguments);
+ }
+ }
+}
diff --git a/modules/simpletest/test_case.php b/modules/simpletest/test_case.php
new file mode 100644
index 000000000..b7b724061
--- /dev/null
+++ b/modules/simpletest/test_case.php
@@ -0,0 +1,615 @@
+<?php
+// $Id$
+
+/**
+ * Basic test case. This is the smallest unit of a test
+ * suite. It searches for
+ * all methods that start with the the string "test" and
+ * runs them. Working test cases extend this class.
+ */
+class SimpleTestCase {
+ var $_label = false;
+ var $_reporter;
+ var $_observers;
+ var $_should_skip = false;
+
+ /**
+ * Sets up the test with no display.
+ * @param string $label If no test name is given then
+ * the class name is used.
+ * @access public
+ */
+ function SimpleTestCase($label = false) {
+ if ($label) {
+ $this->_label = $label;
+ }
+ }
+
+ /**
+ * Accessor for the test name for subclasses.
+ * @return string Name of the test.
+ * @access public
+ */
+ function getLabel() {
+ return $this->_label ? $this->_label : get_class($this);
+ }
+
+ /**
+ * This is a placeholder for skipping tests. In this
+ * method you place skipIf() and skipUnless() calls to
+ * set the skipping state.
+ * @access public
+ */
+ function skip() {}
+
+ /**
+ * Will issue a message to the reporter and tell the test
+ * case to skip if the incoming flag is true.
+ * @param string $should_skip Condition causing the tests to be skipped.
+ * @param string $message Text of skip condition.
+ * @access public
+ */
+ function skipIf($should_skip, $message = '%s') {
+ if ($should_skip && !$this->_should_skip) {
+ $this->_should_skip = true;
+ $message = sprintf($message, 'Skipping ['. get_class($this) .']');
+ $this->_reporter->paintSkip($message . $this->getAssertionLine());
+ }
+ }
+
+ /**
+ * Will issue a message to the reporter and tell the test
+ * case to skip if the incoming flag is false.
+ * @param string $shouldnt_skip Condition causing the tests to be run.
+ * @param string $message Text of skip condition.
+ * @access public
+ */
+ function skipUnless($shouldnt_skip, $message = false) {
+ $this->skipIf(!$shouldnt_skip, $message);
+ }
+
+ /**
+ * Used to invoke the single tests.
+ * @return SimpleInvoker Individual test runner.
+ * @access public
+ */
+ function & createInvoker() {
+ $invoker = &new SimpleErrorTrappingInvoker(new SimpleInvoker($this));
+ if (version_compare(phpversion(), '5') >= 0) {
+ $invoker = &new SimpleExceptionTrappingInvoker($invoker);
+ }
+ return $invoker;
+ }
+
+ /**
+ * Uses reflection to run every method within itself
+ * starting with the string "test" unless a method
+ * is specified.
+ * @param SimpleReporter $reporter Current test reporter.
+ * @return boolean True if all tests passed.
+ * @access public
+ */
+ function run(&$reporter) {
+ $context = &SimpleTest::getContext();
+ $context->setTest($this);
+ $context->setReporter($reporter);
+ $this->_reporter = &$reporter;
+ $started = false;
+ foreach ($this->getTests() as $method) {
+ if ($reporter->shouldInvoke($this->getLabel(), $method)) {
+ $this->skip();
+ if ($this->_should_skip) {
+ break;
+ }
+ if (!$started) {
+ $reporter->paintCaseStart($this->getLabel());
+ $started = true;
+ }
+ $invoker = &$this->_reporter->createInvoker($this->createInvoker());
+ $invoker->before($method);
+ $invoker->invoke($method);
+ $invoker->after($method);
+ }
+ }
+ if ($started) {
+ $reporter->paintCaseEnd($this->getLabel());
+ }
+ unset($this->_reporter);
+ return $reporter->getStatus();
+ }
+
+ /**
+ * Gets a list of test names. Normally that will
+ * be all internal methods that start with the
+ * name "test". This method should be overridden
+ * if you want a different rule.
+ * @return array List of test names.
+ * @access public
+ */
+ function getTests() {
+ $methods = array();
+ foreach (get_class_methods(get_class($this)) as $method) {
+ if ($this->_isTest($method)) {
+ $methods[] = $method;
+ }
+ }
+ return $methods;
+ }
+
+ /**
+ * Tests to see if the method is a test that should
+ * be run. Currently any method that starts with 'test'
+ * is a candidate unless it is the constructor.
+ * @param string $method Method name to try.
+ * @return boolean True if test method.
+ * @access protected
+ */
+ function _isTest($method) {
+ if (strtolower(substr($method, 0, 4)) == 'test') {
+ return !is_a($this, strtolower($method));
+ }
+ return false;
+ }
+
+ /**
+ * Announces the start of the test.
+ * @param string $method Test method just started.
+ * @access public
+ */
+ function before($method) {
+ $this->_reporter->paintMethodStart($method);
+ $this->_observers = array();
+ }
+
+ /**
+ * Sets up unit test wide variables at the start
+ * of each test method. To be overridden in
+ * actual user test cases.
+ * @access public
+ */
+ function setUp() {}
+
+ /**
+ * Clears the data set in the setUp() method call.
+ * To be overridden by the user in actual user test cases.
+ * @access public
+ */
+ function tearDown() {}
+
+ /**
+ * Announces the end of the test. Includes private clean up.
+ * @param string $method Test method just finished.
+ * @access public
+ */
+ function after($method) {
+ for ($i = 0; $i < count($this->_observers); $i++) {
+ $this->_observers[$i]->atTestEnd($method, $this);
+ }
+ $this->_reporter->paintMethodEnd($method);
+ }
+
+ /**
+ * Sets up an observer for the test end.
+ * @param object $observer Must have atTestEnd()
+ * method.
+ * @access public
+ */
+ function tell(&$observer) {
+ $this->_observers[] = &$observer;
+ }
+
+ /**
+ * @deprecated
+ */
+ function pass($message = 'Pass', $group = 'Other') {
+ if (!isset($this->_reporter)) {
+ trigger_error('Can only make assertions within test methods');
+ }
+ $this->_reporter->paintPass($message . $this->getAssertionLine(), $group);
+ return TRUE;
+ }
+
+ /**
+ * Sends a fail event with a message.
+ * @param string $message Message to send.
+ * @access public
+ */
+ function fail($message = 'Fail', $group = 'Other') {
+ if (!isset($this->_reporter)) {
+ trigger_error('Can only make assertions within test methods');
+ }
+ $this->_reporter->paintFail($message . $this->getAssertionLine(), $group);
+ return FALSE;
+ }
+
+ /**
+ * Formats a PHP error and dispatches it to the
+ * reporter.
+ * @param integer $severity PHP error code.
+ * @param string $message Text of error.
+ * @param string $file File error occoured in.
+ * @param integer $line Line number of error.
+ * @access public
+ */
+ function error($severity, $message, $file, $line) {
+ if (!isset($this->_reporter)) {
+ trigger_error('Can only make assertions within test methods');
+ }
+ $this->_reporter->paintError("Unexpected PHP error [$message] severity [$severity] in [$file line $line]");
+ }
+
+ /**
+ * Formats an exception and dispatches it to the
+ * reporter.
+ * @param Exception $exception Object thrown.
+ * @access public
+ */
+ function exception($exception) {
+ $this->_reporter->paintException($exception);
+ }
+
+ /**
+ * @deprecated
+ */
+ function signal($type, &$payload) {
+ if (!isset($this->_reporter)) {
+ trigger_error('Can only make assertions within test methods');
+ }
+ $this->_reporter->paintSignal($type, $payload);
+ }
+
+ /**
+ * Runs an expectation directly, for extending the
+ * tests with new expectation classes.
+ * @param SimpleExpectation $expectation Expectation subclass.
+ * @param mixed $compare Value to compare.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assert(&$expectation, $compare, $message = '%s', $group = 'Other') {
+ if ($expectation->test($compare)) {
+ return $this->pass(sprintf($message, $expectation->overlayMessage($compare, $this->_reporter->getDumper())), $group);
+ }
+ else {
+ return $this->fail(sprintf($message, $expectation->overlayMessage($compare, $this->_reporter->getDumper())), $group);
+ }
+ }
+
+ /**
+ * @deprecated
+ */
+ function assertExpectation(&$expectation, $compare, $message = '%s', $group = 'Other') {
+ return $this->assert($expectation, $compare, $message, $group);
+ }
+
+ /**
+ * Uses a stack trace to find the line of an assertion.
+ * @return string Line number of first assert*
+ * method embedded in format string.
+ * @access public
+ */
+ function getAssertionLine() {
+ $trace = new SimpleStackTrace(array('assert', 'expect', 'pass', 'fail', 'skip'));
+ return $trace->traceMethod();
+ }
+
+ /**
+ * @deprecated
+ */
+ function sendMessage($message) {
+ $this->_reporter->PaintMessage($message);
+ }
+
+ /**
+ * Accessor for the number of subtests.
+ * @return integer Number of test cases.
+ * @access public
+ * @static
+ */
+ function getSize() {
+ return 1;
+ }
+}
+
+/**
+ * Helps to extract test cases automatically from a file.
+ */
+class SimpleFileLoader {
+
+ /**
+ * Builds a test suite from a library of test cases.
+ * The new suite is composed into this one.
+ * @param string $test_file File name of library with
+ * test case classes.
+ * @return TestSuite The new test suite.
+ * @access public
+ */
+ function &load($test_file) {
+ $existing_classes = get_declared_classes();
+ include_once ($test_file);
+ $classes = $this->selectRunnableTests(
+ array_diff(get_declared_classes(), $existing_classes));
+ $suite = &$this->createSuiteFromClasses($test_file, $classes);
+ return $suite;
+ }
+
+ /**
+ * Calculates the incoming test cases. Skips abstract
+ * and ignored classes.
+ * @param array $candidates Candidate classes.
+ * @return array New classes which are test
+ * cases that shouldn't be ignored.
+ * @access public
+ */
+ function selectRunnableTests($candidates) {
+ $classes = array();
+ foreach ($candidates as $class) {
+ if (TestSuite::getBaseTestCase($class)) {
+ $reflection = new ReflectionClass($class);
+ if ($reflection->isAbstract()) {
+ SimpleTest::ignore($class);
+ }
+ $classes[] = $class;
+ }
+ }
+ return $classes;
+ }
+
+ /**
+ * Builds a test suite from a class list.
+ * @param string $title Title of new group.
+ * @param array $classes Test classes.
+ * @return TestSuite Group loaded with the new
+ * test cases.
+ * @access public
+ */
+ function &createSuiteFromClasses($title, $classes) {
+ if (count($classes) == 0) {
+ $suite = &new BadTestSuite($title, "No runnable test cases in [$title]");
+ return $suite;
+ }
+ SimpleTest::ignoreParentsIfIgnored($classes);
+ $suite = &new TestSuite($title);
+ foreach ($classes as $class) {
+ if (!SimpleTest::isIgnored($class)) {
+ $suite->addTestClass($class);
+ }
+ }
+ return $suite;
+ }
+}
+
+/**
+ * This is a composite test class for combining
+ * test cases and other RunnableTest classes into
+ * a group test.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class TestSuite {
+ var $_label;
+ var $_test_cases;
+
+ /**
+ * Sets the name of the test suite.
+ * @param string $label Name sent at the start and end
+ * of the test.
+ * @access public
+ */
+ function TestSuite($label = false) {
+ $this->_label = $label;
+ $this->_test_cases = array();
+ }
+
+ /**
+ * Accessor for the test name for subclasses. If the suite
+ * wraps a single test case the label defaults to the name of that test.
+ * @return string Name of the test.
+ * @access public
+ */
+ function getLabel() {
+ if (!$this->_label) {
+ return ($this->getSize() == 1) ? get_class($this->_test_cases[0]) : get_class($this);
+ }
+ else {
+ return $this->_label;
+ }
+ }
+
+ /**
+ * @deprecated
+ */
+ function addTestCase(&$test_case) {
+ $this->_test_cases[] = &$test_case;
+ }
+
+ /**
+ * @deprecated
+ */
+ function addTestClass($class) {
+ if (TestSuite::getBaseTestCase($class) == 'testsuite') {
+ $this->_test_cases[] = &new $class();
+ }
+ else {
+ $this->_test_cases[] = $class;
+ }
+ }
+
+ /**
+ * Adds a test into the suite by instance or class. The class will
+ * be instantiated if it's a test suite.
+ * @param SimpleTestCase $test_case Suite or individual test
+ * case implementing the
+ * runnable test interface.
+ * @access public
+ */
+ function add(&$test_case) {
+ if (!is_string($test_case)) {
+ $this->_test_cases[] = &$test_case;
+ }
+ elseif (TestSuite::getBaseTestCase($class) == 'testsuite') {
+ $this->_test_cases[] = &new $class();
+ }
+ else {
+ $this->_test_cases[] = $class;
+ }
+ }
+
+ /**
+ * @deprecated
+ */
+ function addTestFile($test_file) {
+ $this->addFile($test_file);
+ }
+
+ /**
+ * Builds a test suite from a library of test cases.
+ * The new suite is composed into this one.
+ * @param string $test_file File name of library with
+ * test case classes.
+ * @access public
+ */
+ function addFile($test_file) {
+ $extractor = new SimpleFileLoader();
+ $this->add($extractor->load($test_file));
+ }
+
+ /**
+ * Delegates to a visiting collector to add test
+ * files.
+ * @param string $path Path to scan from.
+ * @param SimpleCollector $collector Directory scanner.
+ * @access public
+ */
+ function collect($path, &$collector) {
+ $collector->collect($this, $path);
+ }
+
+ /**
+ * Invokes run() on all of the held test cases, instantiating
+ * them if necessary.
+ * @param SimpleReporter $reporter Current test reporter.
+ * @access public
+ */
+ function run(&$reporter) {
+ $reporter->paintGroupStart($this->getLabel(), $this->getSize());
+ for ($i = 0, $count = count($this->_test_cases); $i < $count; $i++) {
+ if (is_string($this->_test_cases[$i])) {
+ $class = $this->_test_cases[$i];
+ $test = &new $class();
+ $test->run($reporter);
+ unset($test);
+ }
+ else {
+ $this->_test_cases[$i]->run($reporter);
+ }
+ }
+ $reporter->paintGroupEnd($this->getLabel());
+ return $reporter->getStatus();
+ }
+
+ /**
+ * Number of contained test cases.
+ * @return integer Total count of cases in the group.
+ * @access public
+ */
+ function getSize() {
+ $count = 0;
+ foreach ($this->_test_cases as $case) {
+ if (is_string($case)) {
+ $count++;
+ }
+ else {
+ $count += $case->getSize();
+ }
+ }
+ return $count;
+ }
+
+ /**
+ * Test to see if a class is derived from the
+ * SimpleTestCase class.
+ * @param string $class Class name.
+ * @access public
+ * @static
+ */
+ function getBaseTestCase($class) {
+ while ($class = get_parent_class($class)) {
+ $class = strtolower($class);
+ if ($class == 'simpletestcase' || $class == 'testsuite') {
+ return $class;
+ }
+ }
+ return false;
+ }
+}
+
+/**
+ * @package SimpleTest
+ * @subpackage UnitTester
+ * @deprecated
+ */
+class GroupTest extends TestSuite {}
+
+/**
+ * This is a failing group test for when a test suite hasn't
+ * loaded properly.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class BadTestSuite {
+ var $_label;
+ var $_error;
+
+ /**
+ * Sets the name of the test suite and error message.
+ * @param string $label Name sent at the start and end
+ * of the test.
+ * @access public
+ */
+ function BadTestSuite($label, $error) {
+ $this->_label = $label;
+ $this->_error = $error;
+ }
+
+ /**
+ * Accessor for the test name for subclasses.
+ * @return string Name of the test.
+ * @access public
+ */
+ function getLabel() {
+ return $this->_label;
+ }
+
+ /**
+ * Sends a single error to the reporter.
+ * @param SimpleReporter $reporter Current test reporter.
+ * @access public
+ */
+ function run(&$reporter) {
+ $reporter->paintGroupStart($this->getLabel(), $this->getSize());
+ $reporter->paintFail('Bad TestSuite ['. $this->getLabel() .
+ '] with error ['. $this->_error .']');
+ $reporter->paintGroupEnd($this->getLabel());
+ return $reporter->getStatus();
+ }
+
+ /**
+ * Number of contained test cases. Always zero.
+ * @return integer Total count of cases in the group.
+ * @access public
+ */
+ function getSize() {
+ return 0;
+ }
+}
+
+/**
+ * @package SimpleTest
+ * @subpackage UnitTester
+ * @deprecated
+ */
+class BadGroupTest extends BadTestSuite {}
+
+
diff --git a/modules/simpletest/unit_tester.php b/modules/simpletest/unit_tester.php
new file mode 100644
index 000000000..cf4ea16ba
--- /dev/null
+++ b/modules/simpletest/unit_tester.php
@@ -0,0 +1,178 @@
+<?php
+// $Id$
+
+/**
+ * Standard unit test class for day to day testing
+ * of PHP code XP style. Adds some useful standard
+ * assertions.
+ */
+class UnitTestCase extends SimpleTestCase {
+
+ /**
+ * Creates an empty test case. Should be subclassed
+ * with test methods for a functional test case.
+ * @param string $label Name of test case. Will use
+ * the class name if none specified.
+ * @access public
+ */
+ function UnitTestCase($label = false) {
+ if (!$label) {
+ $label = get_class($this);
+ }
+ $this->SimpleTestCase($label);
+ }
+
+ /**
+ * Called from within the test methods to register
+ * passes and failures.
+ * @param boolean $result Pass on true.
+ * @param string $message Message to display describing
+ * the test state.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertTrue($result, $message = FALSE, $group = 'Other') {
+ return $this->assert(new TrueExpectation(), $result, $message, $group);
+ }
+
+ /**
+ * Will be true on false and vice versa. False
+ * is the PHP definition of false, so that null,
+ * empty strings, zero and an empty array all count
+ * as false.
+ * @param boolean $result Pass on false.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertFalse($result, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, 'Expected false, got ['. $dumper->describeValue($result) .']');
+ return $this->assertTrue(!$result, $message, $group);
+ }
+
+ /**
+ * Will be true if the value is null.
+ * @param null $value Supposedly null value.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertNull($value, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, '['. $dumper->describeValue($value) .'] should be null');
+ return $this->assertTrue(!isset($value), $message, $group);
+ }
+
+ /**
+ * Will be true if the value is set.
+ * @param mixed $value Supposedly set value.
+ * @param string $message Message to display.
+ * @return boolean True on pass.
+ * @access public
+ */
+ function assertNotNull($value, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, '['. $dumper->describeValue($value) .'] should not be null');
+ return $this->assertTrue(isset($value), $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the two parameters have
+ * the same value only. Otherwise a fail.
+ * @param mixed $first Value to compare.
+ * @param mixed $second Value to compare.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertEqual($first, $second, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, 'Expected '. $dumper->describeValue($first) .', got ['. $dumper->describeValue($second) .']');
+ $this->assertTrue($first == $second, $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the two parameters have
+ * a different value. Otherwise a fail.
+ * @param mixed $first Value to compare.
+ * @param mixed $second Value to compare.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertNotEqual($first, $second, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, 'Expected '. $dumper->describeValue($first) .', not equal to '. $dumper->describeValue($second));
+ $this->assertTrue($first != $second, $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the two parameters have
+ * the same value and same type. Otherwise a fail.
+ * @param mixed $first Value to compare.
+ * @param mixed $second Value to compare.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertIdentical($first, $second, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, 'Expected '. $dumper->describeValue($first) .', got ['. $dumper->describeValue($second) .']');
+ $this->assertTrue($first === $second, $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the two parameters have
+ * the different value or different type.
+ * @param mixed $first Value to compare.
+ * @param mixed $second Value to compare.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertNotIdentical($first, $second, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $message = sprintf($message, 'Expected '. $dumper->describeValue($first) .', not identical to '. $dumper->describeValue($second));
+ $this->assertTrue($first !== $second, $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the Perl regex pattern
+ * is found in the subject. Fail otherwise.
+ * @param string $pattern Perl regex to look for including
+ * the regex delimiters.
+ * @param string $subject String to search in.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertPattern($pattern, $subject, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $replace = 'Pattern '. $pattern .' detected in ['. $dumper->describeValue($subject) .']';
+ $found = preg_match($pattern, $subject, $matches);
+ if ($found) {
+ $position = strpos($subject, $matches[0]);
+ $replace .= ' in region ['. $dumper->clipString($subject, 100, $position) .']';
+ }
+ $message = sprintf($message, $replace);
+ $this->assertTrue($found, $message, $group);
+ }
+
+ /**
+ * Will trigger a pass if the perl regex pattern
+ * is not present in subject. Fail if found.
+ * @param string $pattern Perl regex to look for including
+ * the regex delimiters.
+ * @param string $subject String to search in.
+ * @param string $message Message to display.
+ * @return boolean True on pass
+ * @access public
+ */
+ function assertNoPattern($pattern, $subject, $message = '%s', $group = 'Other') {
+ $dumper = &new SimpleDumper();
+ $found = preg_match($pattern, $subject);
+ $message = sprintf($message, 'Pattern '. $pattern .' not detected in ['. $dumper->describeValue($subject) .']');
+ $this->assertFalse($found, $message, $group = 'Other');
+ }
+} \ No newline at end of file
diff --git a/modules/simpletest/xml.php b/modules/simpletest/xml.php
new file mode 100644
index 000000000..f3f105618
--- /dev/null
+++ b/modules/simpletest/xml.php
@@ -0,0 +1,641 @@
+<?php
+// $Id$
+
+// ---------------------------------------
+// This file will be removed
+// ---------------------------------------
+
+
+/**
+ * Creates the XML needed for remote communication
+ * by SimpleTest.
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class XmlReporter extends SimpleReporter {
+ var $_indent;
+ var $_namespace;
+
+ /**
+ * Sets up indentation and namespace.
+ * @param string $namespace Namespace to add to each tag.
+ * @param string $indent Indenting to add on each nesting.
+ * @access public
+ */
+ function XmlReporter($namespace = false, $indent = ' ') {
+ $this->SimpleReporter();
+ $this->_namespace = ($namespace ? $namespace . ':' : '');
+ $this->_indent = $indent;
+ }
+
+ /**
+ * Calculates the pretty printing indent level
+ * from the current level of nesting.
+ * @param integer $offset Extra indenting level.
+ * @return string Leading space.
+ * @access protected
+ */
+ function _getIndent($offset = 0) {
+ return str_repeat(
+ $this->_indent,
+ count($this->getTestList()) + $offset);
+ }
+
+ /**
+ * Converts character string to parsed XML
+ * entities string.
+ * @param string text Unparsed character data.
+ * @return string Parsed character data.
+ * @access public
+ */
+ function toParsedXml($text) {
+ return str_replace(
+ array('&', '<', '>', '"', '\''),
+ array('&amp;', '&lt;', '&gt;', '&quot;', '&apos;'),
+ $text);
+ }
+
+ /**
+ * Paints the start of a group test.
+ * @param string $test_name Name of test that is starting.
+ * @param integer $size Number of test cases starting.
+ * @access public
+ */
+ function paintGroupStart($test_name, $size) {
+ parent::paintGroupStart($test_name, $size);
+ print $this->_getIndent();
+ print "<" . $this->_namespace . "group size=\"$size\">\n";
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "name>" .
+ $this->toParsedXml($test_name) .
+ "</" . $this->_namespace . "name>\n";
+ }
+
+ /**
+ * Paints the end of a group test.
+ * @param string $test_name Name of test that is ending.
+ * @access public
+ */
+ function paintGroupEnd($test_name) {
+ print $this->_getIndent();
+ print "</" . $this->_namespace . "group>\n";
+ parent::paintGroupEnd($test_name);
+ }
+
+ /**
+ * Paints the start of a test case.
+ * @param string $test_name Name of test that is starting.
+ * @access public
+ */
+ function paintCaseStart($test_name) {
+ parent::paintCaseStart($test_name);
+ print $this->_getIndent();
+ print "<" . $this->_namespace . "case>\n";
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "name>" .
+ $this->toParsedXml($test_name) .
+ "</" . $this->_namespace . "name>\n";
+ }
+
+ /**
+ * Paints the end of a test case.
+ * @param string $test_name Name of test that is ending.
+ * @access public
+ */
+ function paintCaseEnd($test_name) {
+ print $this->_getIndent();
+ print "</" . $this->_namespace . "case>\n";
+ parent::paintCaseEnd($test_name);
+ }
+
+ /**
+ * Paints the start of a test method.
+ * @param string $test_name Name of test that is starting.
+ * @access public
+ */
+ function paintMethodStart($test_name) {
+ parent::paintMethodStart($test_name);
+ print $this->_getIndent();
+ print "<" . $this->_namespace . "test>\n";
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "name>" .
+ $this->toParsedXml($test_name) .
+ "</" . $this->_namespace . "name>\n";
+ }
+
+ /**
+ * Paints the end of a test method.
+ * @param string $test_name Name of test that is ending.
+ * @param integer $progress Number of test cases ending.
+ * @access public
+ */
+ function paintMethodEnd($test_name) {
+ print $this->_getIndent();
+ print "</" . $this->_namespace . "test>\n";
+ parent::paintMethodEnd($test_name);
+ }
+
+ /**
+ * Paints pass as XML.
+ * @param string $message Message to encode.
+ * @access public
+ */
+ function paintPass($message) {
+ parent::paintPass($message);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "pass>";
+ print $this->toParsedXml($message);
+ print "</" . $this->_namespace . "pass>\n";
+ }
+
+ /**
+ * Paints failure as XML.
+ * @param string $message Message to encode.
+ * @access public
+ */
+ function paintFail($message) {
+ parent::paintFail($message);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "fail>";
+ print $this->toParsedXml($message);
+ print "</" . $this->_namespace . "fail>\n";
+ }
+
+ /**
+ * Paints error as XML.
+ * @param string $message Message to encode.
+ * @access public
+ */
+ function paintError($message) {
+ parent::paintError($message);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "exception>";
+ print $this->toParsedXml($message);
+ print "</" . $this->_namespace . "exception>\n";
+ }
+
+ /**
+ * Paints exception as XML.
+ * @param Exception $exception Exception to encode.
+ * @access public
+ */
+ function paintException($exception) {
+ parent::paintException($exception);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "exception>";
+ $message = 'Unexpected exception of type [' . get_class($exception) .
+ '] with message ['. $exception->getMessage() .
+ '] in ['. $exception->getFile() .
+ ' line ' . $exception->getLine() . ']';
+ print $this->toParsedXml($message);
+ print "</" . $this->_namespace . "exception>\n";
+ }
+
+ /**
+ * Paints the skipping message and tag.
+ * @param string $message Text to display in skip tag.
+ * @access public
+ */
+ function paintSkip($message) {
+ parent::paintSkip($message);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "skip>";
+ print $this->toParsedXml($message);
+ print "</" . $this->_namespace . "skip>\n";
+ }
+
+ /**
+ * Paints a simple supplementary message.
+ * @param string $message Text to display.
+ * @access public
+ */
+ function paintMessage($message) {
+ parent::paintMessage($message);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "message>";
+ print $this->toParsedXml($message);
+ print "</" . $this->_namespace . "message>\n";
+ }
+
+ /**
+ * Paints a formatted ASCII message such as a
+ * variable dump.
+ * @param string $message Text to display.
+ * @access public
+ */
+ function paintFormattedMessage($message) {
+ parent::paintFormattedMessage($message);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "formatted>";
+ print "<![CDATA[$message]]>";
+ print "</" . $this->_namespace . "formatted>\n";
+ }
+
+ /**
+ * Serialises the event object.
+ * @param string $type Event type as text.
+ * @param mixed $payload Message or object.
+ * @access public
+ */
+ function paintSignal($type, &$payload) {
+ parent::paintSignal($type, $payload);
+ print $this->_getIndent(1);
+ print "<" . $this->_namespace . "signal type=\"$type\">";
+ print "<![CDATA[" . serialize($payload) . "]]>";
+ print "</" . $this->_namespace . "signal>\n";
+ }
+
+ /**
+ * Paints the test document header.
+ * @param string $test_name First test top level
+ * to start.
+ * @access public
+ * @abstract
+ */
+ function paintHeader($test_name) {
+ if (! SimpleReporter::inCli()) {
+ header('Content-type: text/xml');
+ }
+ print "<?xml version=\"1.0\"";
+ if ($this->_namespace) {
+ print " xmlns:" . $this->_namespace .
+ "=\"www.lastcraft.com/SimpleTest/Beta3/Report\"";
+ }
+ print "?>\n";
+ print "<" . $this->_namespace . "run>\n";
+ }
+
+ /**
+ * Paints the test document footer.
+ * @param string $test_name The top level test.
+ * @access public
+ * @abstract
+ */
+ function paintFooter($test_name) {
+ print "</" . $this->_namespace . "run>\n";
+ }
+}
+
+/**
+ * Accumulator for incoming tag. Holds the
+ * incoming test structure information for
+ * later dispatch to the reporter.
+ @package SimpleTest
+ @subpackage UnitTester
+ */
+class NestingXmlTag {
+ var $_name;
+ var $_attributes;
+
+ /**
+ * Sets the basic test information except
+ * the name.
+ * @param hash $attributes Name value pairs.
+ * @access public
+ */
+ function NestingXmlTag($attributes) {
+ $this->_name = false;
+ $this->_attributes = $attributes;
+ }
+
+ /**
+ * Sets the test case/method name.
+ * @param string $name Name of test.
+ * @access public
+ */
+ function setName($name) {
+ $this->_name = $name;
+ }
+
+ /**
+ * Accessor for name.
+ * @return string Name of test.
+ * @access public
+ */
+ function getName() {
+ return $this->_name;
+ }
+
+ /**
+ * Accessor for attributes.
+ * @return hash All attributes.
+ * @access protected
+ */
+ function _getAttributes() {
+ return $this->_attributes;
+ }
+}
+
+/**
+ * Accumulator for incoming method tag. Holds the
+ * incoming test structure information for
+ * later dispatch to the reporter.
+ @package SimpleTest
+ @subpackage UnitTester
+ */
+class NestingMethodTag extends NestingXmlTag {
+
+ /**
+ * Sets the basic test information except
+ * the name.
+ * @param hash $attributes Name value pairs.
+ * @access public
+ */
+ function NestingMethodTag($attributes) {
+ $this->NestingXmlTag($attributes);
+ }
+
+ /**
+ * Signals the appropriate start event on the
+ * listener.
+ * @param SimpleReporter $listener Target for events.
+ * @access public
+ */
+ function paintStart(&$listener) {
+ $listener->paintMethodStart($this->getName());
+ }
+
+ /**
+ * Signals the appropriate end event on the
+ * listener.
+ * @param SimpleReporter $listener Target for events.
+ * @access public
+ */
+ function paintEnd(&$listener) {
+ $listener->paintMethodEnd($this->getName());
+ }
+}
+
+/**
+ * Accumulator for incoming case tag. Holds the
+ * incoming test structure information for
+ * later dispatch to the reporter.
+ @package SimpleTest
+ @subpackage UnitTester
+ */
+class NestingCaseTag extends NestingXmlTag {
+
+ /**
+ * Sets the basic test information except
+ * the name.
+ * @param hash $attributes Name value pairs.
+ * @access public
+ */
+ function NestingCaseTag($attributes) {
+ $this->NestingXmlTag($attributes);
+ }
+
+ /**
+ * Signals the appropriate start event on the
+ * listener.
+ * @param SimpleReporter $listener Target for events.
+ * @access public
+ */
+ function paintStart(&$listener) {
+ $listener->paintCaseStart($this->getName());
+ }
+
+ /**
+ * Signals the appropriate end event on the
+ * listener.
+ * @param SimpleReporter $listener Target for events.
+ * @access public
+ */
+ function paintEnd(&$listener) {
+ $listener->paintCaseEnd($this->getName());
+ }
+}
+
+/**
+ * Accumulator for incoming group tag. Holds the
+ * incoming test structure information for
+ * later dispatch to the reporter.
+ @package SimpleTest
+ @subpackage UnitTester
+ */
+class NestingGroupTag extends NestingXmlTag {
+
+ /**
+ * Sets the basic test information except
+ * the name.
+ * @param hash $attributes Name value pairs.
+ * @access public
+ */
+ function NestingGroupTag($attributes) {
+ $this->NestingXmlTag($attributes);
+ }
+
+ /**
+ * Signals the appropriate start event on the
+ * listener.
+ * @param SimpleReporter $listener Target for events.
+ * @access public
+ */
+ function paintStart(&$listener) {
+ $listener->paintGroupStart($this->getName(), $this->getSize());
+ }
+
+ /**
+ * Signals the appropriate end event on the
+ * listener.
+ * @param SimpleReporter $listener Target for events.
+ * @access public
+ */
+ function paintEnd(&$listener) {
+ $listener->paintGroupEnd($this->getName());
+ }
+
+ /**
+ * The size in the attributes.
+ * @return integer Value of size attribute or zero.
+ * @access public
+ */
+ function getSize() {
+ $attributes = $this->_getAttributes();
+ if (isset($attributes['SIZE'])) {
+ return (integer)$attributes['SIZE'];
+ }
+ return 0;
+ }
+}
+
+/**
+ * Parser for importing the output of the XmlReporter.
+ * Dispatches that output to another reporter.
+ @package SimpleTest
+ @subpackage UnitTester
+ */
+class SimpleTestXmlParser {
+ var $_listener;
+ var $_expat;
+ var $_tag_stack;
+ var $_in_content_tag;
+ var $_content;
+ var $_attributes;
+
+ /**
+ * Loads a listener with the SimpleReporter
+ * interface.
+ * @param SimpleReporter $listener Listener of tag events.
+ * @access public
+ */
+ function SimpleTestXmlParser(&$listener) {
+ $this->_listener = &$listener;
+ $this->_expat = &$this->_createParser();
+ $this->_tag_stack = array();
+ $this->_in_content_tag = false;
+ $this->_content = '';
+ $this->_attributes = array();
+ }
+
+ /**
+ * Parses a block of XML sending the results to
+ * the listener.
+ * @param string $chunk Block of text to read.
+ * @return boolean True if valid XML.
+ * @access public
+ */
+ function parse($chunk) {
+ if (! xml_parse($this->_expat, $chunk)) {
+ trigger_error('XML parse error with ' .
+ xml_error_string(xml_get_error_code($this->_expat)));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Sets up expat as the XML parser.
+ * @return resource Expat handle.
+ * @access protected
+ */
+ function &_createParser() {
+ $expat = xml_parser_create();
+ xml_set_object($expat, $this);
+ xml_set_element_handler($expat, '_startElement', '_endElement');
+ xml_set_character_data_handler($expat, '_addContent');
+ xml_set_default_handler($expat, '_default');
+ return $expat;
+ }
+
+ /**
+ * Opens a new test nesting level.
+ * @return NestedXmlTag The group, case or method tag
+ * to start.
+ * @access private
+ */
+ function _pushNestingTag($nested) {
+ array_unshift($this->_tag_stack, $nested);
+ }
+
+ /**
+ * Accessor for current test structure tag.
+ * @return NestedXmlTag The group, case or method tag
+ * being parsed.
+ * @access private
+ */
+ function &_getCurrentNestingTag() {
+ return $this->_tag_stack[0];
+ }
+
+ /**
+ * Ends a nesting tag.
+ * @return NestedXmlTag The group, case or method tag
+ * just finished.
+ * @access private
+ */
+ function _popNestingTag() {
+ return array_shift($this->_tag_stack);
+ }
+
+ /**
+ * Test if tag is a leaf node with only text content.
+ * @param string $tag XML tag name.
+ * @return @boolean True if leaf, false if nesting.
+ * @private
+ */
+ function _isLeaf($tag) {
+ return in_array($tag, array(
+ 'NAME', 'PASS', 'FAIL', 'EXCEPTION', 'SKIP', 'MESSAGE', 'FORMATTED', 'SIGNAL'));
+ }
+
+ /**
+ * Handler for start of event element.
+ * @param resource $expat Parser handle.
+ * @param string $tag Element name.
+ * @param hash $attributes Name value pairs.
+ * Attributes without content
+ * are marked as true.
+ * @access protected
+ */
+ function _startElement($expat, $tag, $attributes) {
+ $this->_attributes = $attributes;
+ if ($tag == 'GROUP') {
+ $this->_pushNestingTag(new NestingGroupTag($attributes));
+ } elseif ($tag == 'CASE') {
+ $this->_pushNestingTag(new NestingCaseTag($attributes));
+ } elseif ($tag == 'TEST') {
+ $this->_pushNestingTag(new NestingMethodTag($attributes));
+ } elseif ($this->_isLeaf($tag)) {
+ $this->_in_content_tag = true;
+ $this->_content = '';
+ }
+ }
+
+ /**
+ * End of element event.
+ * @param resource $expat Parser handle.
+ * @param string $tag Element name.
+ * @access protected
+ */
+ function _endElement($expat, $tag) {
+ $this->_in_content_tag = false;
+ if (in_array($tag, array('GROUP', 'CASE', 'TEST'))) {
+ $nesting_tag = $this->_popNestingTag();
+ $nesting_tag->paintEnd($this->_listener);
+ } elseif ($tag == 'NAME') {
+ $nesting_tag = &$this->_getCurrentNestingTag();
+ $nesting_tag->setName($this->_content);
+ $nesting_tag->paintStart($this->_listener);
+ } elseif ($tag == 'PASS') {
+ $this->_listener->paintPass($this->_content);
+ } elseif ($tag == 'FAIL') {
+ $this->_listener->paintFail($this->_content);
+ } elseif ($tag == 'EXCEPTION') {
+ $this->_listener->paintError($this->_content);
+ } elseif ($tag == 'SKIP') {
+ $this->_listener->paintSkip($this->_content);
+ } elseif ($tag == 'SIGNAL') {
+ $this->_listener->paintSignal(
+ $this->_attributes['TYPE'],
+ unserialize($this->_content));
+ } elseif ($tag == 'MESSAGE') {
+ $this->_listener->paintMessage($this->_content);
+ } elseif ($tag == 'FORMATTED') {
+ $this->_listener->paintFormattedMessage($this->_content);
+ }
+ }
+
+ /**
+ * Content between start and end elements.
+ * @param resource $expat Parser handle.
+ * @param string $text Usually output messages.
+ * @access protected
+ */
+ function _addContent($expat, $text) {
+ if ($this->_in_content_tag) {
+ $this->_content .= $text;
+ }
+ return true;
+ }
+
+ /**
+ * XML and Doctype handler. Discards all such content.
+ * @param resource $expat Parser handle.
+ * @param string $default Text of default content.
+ * @access protected
+ */
+ function _default($expat, $default) {
+ }
+}
+