summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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) {
+ }
+}
+