]> git.mjollnir.org Git - moodle.git/commitdiff
Bug #5979 - Check in the OU's unit testing framework. Docs at http://docs.moodle...
authortjhunt <tjhunt>
Fri, 30 Jun 2006 14:59:05 +0000 (14:59 +0000)
committertjhunt <tjhunt>
Fri, 30 Jun 2006 14:59:05 +0000 (14:59 +0000)
60 files changed:
admin/report/simpletest/ex_reporter.php [new file with mode: 0644]
admin/report/simpletest/ex_simple_test.php [new file with mode: 0644]
admin/report/simpletest/index.php [new file with mode: 0644]
admin/report/simpletest/mod.php [new file with mode: 0644]
lang/en_utf8/unittest.php [new file with mode: 0644]
lib/datalib.php
lib/moodlelib.php
lib/simpletest/slowtestcode.php [new file with mode: 0644]
lib/simpletest/testcode.php [new file with mode: 0644]
lib/simpletest/testdatalib.php [new file with mode: 0644]
lib/simpletestlib.php [new file with mode: 0644]
lib/simpletestlib/BACKLOG [new file with mode: 0644]
lib/simpletestlib/HELP_MY_TESTS_DONT_WORK_ANYMORE [new file with mode: 0644]
lib/simpletestlib/LICENSE [new file with mode: 0644]
lib/simpletestlib/README [new file with mode: 0644]
lib/simpletestlib/TODO [new file with mode: 0644]
lib/simpletestlib/VERSION [new file with mode: 0644]
lib/simpletestlib/authentication.php [new file with mode: 0644]
lib/simpletestlib/browser.php [new file with mode: 0644]
lib/simpletestlib/collector.php [new file with mode: 0644]
lib/simpletestlib/compatibility.php [new file with mode: 0644]
lib/simpletestlib/cookies.php [new file with mode: 0644]
lib/simpletestlib/detached.php [new file with mode: 0644]
lib/simpletestlib/dumper.php [new file with mode: 0644]
lib/simpletestlib/eclipse.php [new file with mode: 0644]
lib/simpletestlib/encoding.php [new file with mode: 0644]
lib/simpletestlib/errors.php [new file with mode: 0644]
lib/simpletestlib/exceptions.php [new file with mode: 0644]
lib/simpletestlib/expectation.php [new file with mode: 0644]
lib/simpletestlib/extensions/pear_test_case.php [new file with mode: 0644]
lib/simpletestlib/extensions/phpunit_test_case.php [new file with mode: 0644]
lib/simpletestlib/form.php [new file with mode: 0644]
lib/simpletestlib/frames.php [new file with mode: 0644]
lib/simpletestlib/http.php [new file with mode: 0644]
lib/simpletestlib/invoker.php [new file with mode: 0644]
lib/simpletestlib/mock_objects.php [new file with mode: 0644]
lib/simpletestlib/page.php [new file with mode: 0644]
lib/simpletestlib/parser.php [new file with mode: 0644]
lib/simpletestlib/reflection_php4.php [new file with mode: 0644]
lib/simpletestlib/reflection_php5.php [new file with mode: 0644]
lib/simpletestlib/remote.php [new file with mode: 0644]
lib/simpletestlib/reporter.php [new file with mode: 0644]
lib/simpletestlib/scorer.php [new file with mode: 0644]
lib/simpletestlib/selector.php [new file with mode: 0644]
lib/simpletestlib/shell_tester.php [new file with mode: 0644]
lib/simpletestlib/simpletest.php [new file with mode: 0644]
lib/simpletestlib/socket.php [new file with mode: 0644]
lib/simpletestlib/tag.php [new file with mode: 0644]
lib/simpletestlib/test_case.php [new file with mode: 0644]
lib/simpletestlib/ui/colortext_reporter.php [new file with mode: 0644]
lib/simpletestlib/ui/css/webunit.css [new file with mode: 0644]
lib/simpletestlib/ui/img/wait.gif [new file with mode: 0644]
lib/simpletestlib/ui/js/webunit.js [new file with mode: 0644]
lib/simpletestlib/ui/js/x.js [new file with mode: 0644]
lib/simpletestlib/ui/webunit_reporter.php [new file with mode: 0644]
lib/simpletestlib/unit_tester.php [new file with mode: 0644]
lib/simpletestlib/url.php [new file with mode: 0644]
lib/simpletestlib/user_agent.php [new file with mode: 0644]
lib/simpletestlib/web_tester.php [new file with mode: 0644]
lib/simpletestlib/xml.php [new file with mode: 0644]

diff --git a/admin/report/simpletest/ex_reporter.php b/admin/report/simpletest/ex_reporter.php
new file mode 100644 (file)
index 0000000..22603d3
--- /dev/null
@@ -0,0 +1,211 @@
+<?php
+/**
+ * A SimpleTest report format for Moodle.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author N.D.Freear@open.ac.uk, T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @version $Id$
+ * @package SimpleTestEx
+ */
+
+/** */
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir . '/moodlelib.php');
+require_once($CFG->libdir . '/simpletestlib/reporter.php');
+
+/**
+ * Extended in-browser test displayer. HtmlReporter generates
+ * only failure messages and a pass count. ExHtmlReporter also
+ * generates pass messages and a time-stamp.
+ *
+ * @package SimpleTestEx
+ */
+class ExHtmlReporter extends HtmlReporter {
+
+    // Options set when the class is created.
+    var $showpasses;
+    
+    // Lang strings. Set in the constructor.
+    var $strrunonlyfolder;
+    var $strrunonlyfile;
+    
+    var $strseparator;
+    
+    /**
+     * Constructor.
+     * 
+     * @param bool $showpasses Whether this reporter should output anything for passes.
+     */
+    function ExHtmlReporter($showpasses) {
+        global $CFG;
+        
+        $this->HtmlReporter();
+        $this->showpasses = $showpasses;
+        
+        $this->strrunonlyfolder = $this->get_string('runonlyfolder');
+        $this->strrunonlyfile = $this->get_string('runonlyfile');
+        $this->strseparator = ' <img src="' . $CFG->pixpath . '/a/r_breadcrumb.gif" class="resize" alt="" /> ';
+    }
+
+    /**
+     * Called when a pass needs to be output.
+     */
+    function paintPass($message) {
+        //(Implicitly call grandparent, as parent not implemented.)
+        parent::paintPass($message);
+        if ($this->showpasses) {
+            $this->_paintPassFail('pass', $message);
+        }
+    }
+
+    /**
+     * Called when a fail needs to be output.
+     */
+    function paintFail($message) {
+        // Explicitly call grandparent, not parent::paintFail.
+        SimpleScorer::paintFail($message);
+        $this->_paintPassFail('fail', $message);
+    }
+
+    /**
+     * Called when an error (uncaught exception or PHP error) needs to be output.
+     */
+    function paintError($message) {
+        // Explicitly call grandparent, not parent::paintFail.
+        SimpleScorer::paintError($message);
+        $this->_paintPassFail('exception', $message);
+    }
+
+    /**
+     * Private method. Used by printPass/Fail/Error.
+     */
+    function _paintPassFail($passorfail, $message) {
+        global $FULLME, $CFG;
+        
+        print_simple_box_start('', '100%', '', 5, $passorfail . ' generalbox');
+        $url = $this->_htmlEntities($this->_stripParameterFromUrl($FULLME, 'path'));
+        echo '<b class="', $passorfail, '">', $this->get_string($passorfail), '</b>: ';
+        $breadcrumb = $this->getTestList();
+        array_shift($breadcrumb);
+        $file = array_shift($breadcrumb);
+        $pathbits = preg_split('/\/|\\\\/', substr($file, strlen($CFG->dirroot) + 1));
+        $file = array_pop($pathbits);
+        $folder = '';
+        foreach ($pathbits as $pathbit) {
+            $folder .= $pathbit . '/';
+            echo "<a href=\"{$url}path=$folder\" title=\"$this->strrunonlyfolder\">$pathbit</a>/";
+        }
+        echo "<a href=\"{$url}path=$folder$file\" title=\"$this->strrunonlyfile\">$file</a>";
+        echo $this->strseparator, implode($this->strseparator, $breadcrumb);
+        echo $this->strseparator, '<br />', $this->_htmlEntities($message), "\n\n";
+        print_simple_box_end();
+        flush();
+    }
+   
+    /**
+     * Called when a notice needs to be output.
+     */
+    function paintNotice($message) {
+        $this->paintMessage($this->_htmlEntities($message));
+    }
+
+    /**
+     * Paints a simple supplementary message.
+     * @param string $message Text to display.
+     */
+    function paintMessage($message) {
+        if ($this->showpasses) {
+            print_simple_box_start('', '100%');
+            echo '<span class="notice">', $this->get_string('notice'), '</span>: ';
+            $breadcrumb = $this->getTestList();
+            array_shift($breadcrumb);
+            echo implode($this->strseparator, $breadcrumb);
+            echo $this->strseparator, '<br />', $message, "\n";
+            print_simple_box_end();
+            flush();
+        }
+    }
+
+    /**
+     * Return extra CSS that is added to the results page.
+     */
+    function _getCss() {
+        // Don't call the parent, do our own thing instead.
+        return
+            ' span.notice { color: teal; }' .
+            ' b.pass { color: green; }' .
+            ' b.fail, b.exception { color: red; }' .
+            ' .exception, .exception pre { background: #fdd; }' .
+            ' .exception pre { padding: 8px; }' .
+            ' .unittestsummary { padding: 8px; margin-top: 1em; color: white; }' .
+            ' .unittestsummary.fail { background-color: red; } ' .
+            ' .unittestsummary.pass { background-color: green; } ';
+    }
+
+    /**
+     * Output anything that should appear above all the test output.
+     */
+    function paintHeader($test_name) {
+        // We do this the moodle way instead.
+    }
+
+    /**
+     * Output anything that should appear below all the test output, e.g. summary information.
+     */
+    function paintFooter($test_name) {
+        $summarydata = new stdClass;
+        $summarydata->run = $this->getTestCaseProgress();
+        $summarydata->total = $this->getTestCaseCount();
+        $summarydata->passes = $this->getPassCount();
+        $summarydata->fails = $this->getFailCount();
+        $summarydata->exceptions = $this->getExceptionCount();
+
+        if ($summarydata->fails == 0 && $summarydata->exceptions == 0) {
+            $status = "pass";
+        } else {
+            $status = "fail";
+        }
+        echo '<div class="unittestsummary ', $status, '">';
+        echo $this->get_string('summary', $summarydata);
+        echo '</div>';
+
+        echo '<div class="performanceinfo">', 
+                $this->get_string('runat', date('<b>d-m-Y H:i T</b>')),
+                $this->get_string('version', SimpleTestOptions::getVersion()),
+                '</div>';
+    }
+    
+    /**
+     * Strip a specified parameter from the query string of a URL, if present.
+     * Adds a separator to the end of the URL, so that a new parameter 
+     * can easily be appended. For example (assuming $param = 'frog'):
+     * 
+     * http://example.com/index.php               -> http://example.com/index.php?
+     * http://example.com/index.php?frog=1        -> http://example.com/index.php?
+     * http://example.com/index.php?toad=1        -> http://example.com/index.php?toad=1&
+     * http://example.com/index.php?frog=1&toad=1 -> http://example.com/index.php?toad=1&
+     * 
+     * @param string $url the URL to modify.
+     * @param string $param the parameter to strip from the URL, if present.
+     * 
+     * @return string The modified URL.
+     */
+    function _stripParameterFromUrl($url, $param) {
+        $url = preg_replace('/(\?|&)' . $param . '=[^&]*&?/', '$1', $url);
+        if (strpos($url, '?') === false) {
+            $url = $url . '?';
+        } else {
+            $url = $url . '&';
+        }
+        return $url;
+    }
+
+    /**
+     * Look up a lang string in the appropriate file.
+     */
+    function get_string($identifier, $a = NULL) {
+        return get_string($identifier, 'unittest', $a);
+    }
+}
+?>
\ No newline at end of file
diff --git a/admin/report/simpletest/ex_simple_test.php b/admin/report/simpletest/ex_simple_test.php
new file mode 100644 (file)
index 0000000..ac9259a
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+/**
+ * A SimpleTest GroupTest that automatically finds all the
+ * test files in a directory tree according to certain rules.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author N.D.Freear@open.ac.uk, T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @version $Id$
+ * @package SimpleTestEx
+ */
+
+/** */
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir . '/simpletestlib/test_case.php');
+
+/**
+ * This is a composite test class for finding test cases and
+ * other RunnableTest classes in a directory tree and combining
+ * them into a group test.
+ * @package SimpleTestEx
+ */
+class AutoGroupTest extends GroupTest {
+
+    var $thorough;
+    var $showsearch;
+
+    function AutoGroupTest($showsearch, $thorough, $test_name = null) {
+        $this->GroupTest($test_name);
+        $this->showsearch = $showsearch;
+        $this->thorough = $thorough;
+    }
+
+    function setLabel($test_name) {
+        //:HACK: there is no GroupTest::setLabel, so access parent::_label.
+        $this->_label = $test_name;
+    }
+
+    function addIgnoreFolder($ignorefolder) {
+        $this->ignorefolders[]=$ignorefolder;
+    }
+
+    function _recurseFolders($path) {
+        if ($this->showsearch) {
+            echo '<li>' . basename(realpath($path)) . '<ul>';
+        }
+
+        $files = scandir($path);
+        static $s_count = 0;
+
+        foreach ($files as $file) {
+            if ($file == '.' || $file == '..') {
+                continue;
+            }
+            $file_path = $path . '/' . $file;
+            if (is_dir($file_path)) {
+                if ($file != 'CVS' && !in_array($file_path, $this->ignorefolders)) {
+                    $this->_recurseFolders($file_path);
+                }
+            } elseif (preg_match('/simpletest(\/|\\\\)test.*\.php$/', $file_path) || 
+                    ($this->thorough && preg_match('/simpletest(\/|\\\\)slowtest.*\.php$/', $file_path))) {
+
+                $s_count++;
+                // OK, found: this shows as a 'Notice' for any 'simpletest/test*.php' file.
+                $this->addTestCase(new FindFileNotice($file_path, 'Found unit test file, '. $s_count));
+
+                // addTestFile: Unfortunately this doesn't return fail/success (bool).
+                $this->addTestFile($file_path, true);
+            }
+        }
+
+        if ($this->showsearch) {
+            echo '</ul></li>';
+        }
+        return $s_count;
+    }
+
+    function findTestFiles($dir) {
+        if ($this->showsearch) {
+            echo '<p>Searching folder: ' . realpath($dir) . '</p><ul>';
+        }
+        $path = $dir;
+        $count = $this->_recurseFolders($path);
+        if ($count <= 0) {
+            $this->addTestCase(new BadAutoGroupTest($path, 'Search complete. No unit test files found'));
+        } else {
+            $this->addTestCase(new AutoGroupTestNotice($path, 'Search complete. Total unit test files found: '. $count));
+        }
+        if ($this->showsearch) {
+                echo '</ul>';
+        }
+        return $count;
+    }
+
+    function addTestFile($file, $internalcall = false) {
+        if ($this->showsearch) {
+            if ($internalcall) {
+                echo '<li><b>' . basename($file) . '</b></li>';
+            } else {
+                echo '<p>Adding test file: ' . realpath($file) . '</p>';
+            }
+            // Make sure that syntax errors show up suring the search, otherwise you often
+            // get blank screens because evil people turn down error_reporting elsewhere.
+            error_reporting(E_ALL);
+        }
+        if(!is_file($file) ){
+            parent::addTestCase(new BadTest($file, 'Not a file or does not exist'));
+        }
+        parent::addTestFile($file);
+    }
+}
+
+
+/* ======================================================================= */
+// get_class_ex: Insert spaces to prettify the class-name.
+function get_class_ex($object) {
+    return preg_replace('/(.?)([A-Z])/', '${1} ${2}', get_class($object));
+}
+
+
+/**
+ * A failing test base-class for when a test suite has NOT loaded properly.
+ * See class, simple_test.php: BadGroupTest.
+ * @package SimpleTestEx
+ */
+class BadTest {
+
+    var $label;
+    var $error;
+
+    function BadTest($label, $error) {
+        $this->label = $label;
+        $this->error = $error;
+    }
+
+    function getLabel() {
+        return $this->label;
+    }
+
+    function run(&$reporter) {
+        $reporter->paintGroupStart(basename(__FILE__), $this->getSize());
+        $reporter->paintFail(get_class_ex($this) .' [' . $this->getLabel() .
+                '] with error [' . $this->error . ']');
+        $reporter->paintGroupEnd($this->getLabel());
+        return $reporter->getStatus();
+    }
+
+    /**
+     * @return int the number of test cases starting.
+     */
+    function getSize() {
+        return 0;
+  }
+}
+
+/**
+ * An informational notice base-class for when a test suite is being processed.
+ * See class, simple_test.php: BadGroupTest.
+ * @package SimpleTestEx
+ */
+class Notice {
+
+    var $label;
+    var $status;
+
+    function Notice($label, $error) {
+        $this->label = $label;
+        $this->status = $error;
+    }
+
+    function getLabel() {
+        return $this->label;
+    }
+
+    function run(&$reporter) {
+        $reporter->paintGroupStart(basename(__FILE__), $this->getSize());
+        $reporter->paintNotice(get_class_ex($this) .
+                ' ['. $this->getLabel() .'] with status [' . $this->status . ']');
+        $reporter->paintGroupEnd($this->getLabel());
+        return $reporter->getStatus();
+    }
+
+    function getSize() {
+        return 0;
+    }
+}
+
+/**
+ * A failing folder test for when the test-user specifies an invalid directory
+ * (run.php?folder=woops).
+ * @package SimpleTestEx
+ */
+class BadFolderTest extends BadTest { }
+
+/**
+ * A failing auto test for when no unit test files are found.
+ * @package SimpleTestEx
+ */
+class BadAutoGroupTest extends BadTest { }
+
+/**
+ * Auto group test notices - 1. Search complete. 2. A test file has been found.
+ * @package SimpleTestEx
+ */
+class AutoGroupTestNotice extends Notice { }
+
+class FindFileNotice extends Notice { }
+?>
\ No newline at end of file
diff --git a/admin/report/simpletest/index.php b/admin/report/simpletest/index.php
new file mode 100644 (file)
index 0000000..0fdde58
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Run the unit tests.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author N.D.Freear@open.ac.uk, T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @version $Id$
+ * @package SimpleTestEx
+ */
+
+/** */
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir . '/moodlelib.php');
+require_once('ex_simple_test.php');
+require_once('ex_reporter.php');
+
+/* The UNITTEST constant can be checked elsewhere if you need to know
+ * when your code is being run as part of a unit test. */
+define('UNITTEST', true);
+
+require_login();
+if (!isadmin()) {
+    print_error('Only admins can access this page');
+}
+
+// CGI arguments
+$path = optional_param('path', '', PARAM_PATH);
+$showpasses = optional_param('showpasses', false, PARAM_BOOL);
+$showsearch = optional_param('showsearch', false, PARAM_BOOL);
+$thorough = optional_param('thorough', false, PARAM_BOOL);
+
+// Create the group of tests.
+$test =& new AutoGroupTest($showsearch, $thorough);
+
+// OU specific. We use the _nonproject folder for stuff we want to 
+// keep in CVS, but which is not really relevant. It does no harm
+// to leave this here.
+$test->addIgnoreFolder($CFG->dirroot . '/_nonproject');
+
+// Make the reporter, which is what displays the results.
+$reporter = new ExHtmlReporter($showpasses);
+
+// Print the header.
+$strtitle = get_string('unittests', 'unittest');
+$stradmin = get_string('administration');
+print_header("$SITE->shortname: $strtitle", $SITE->fullname,
+        '<a href="../../index.php">' . get_string('administration') . '</a> -> ' .
+        '<a href="../../misc.php">' . get_string('miscellaneous') . '</a> -> ' .
+        '<a href="../../report.php">' . get_string('reports') . '</a> -> ' .
+        $strtitle, '', '<style type="text/css">' . $reporter->_getCss() . '</style>');
+if ($showsearch) {
+    print_heading('Searching for test cases');
+}
+flush();
+
+// Work out what to test.
+if (substr($path, 0, 1) == '/') {
+    $path = substr($path, 1);
+}
+$path = $CFG->dirroot . '/' . $path;
+if (substr($path, -1) == '/') {
+    $path = substr($path, 0, -1);
+}
+$displaypath = substr($path, strlen($CFG->dirroot) + 1);
+$ok = true;
+if (is_file($path)) {
+    $test->addTestFile($path);
+} else if (is_dir($path)){
+    $test->findTestFiles($path);
+} else {
+    print_simple_box(get_string('pathdoesnotexist', 'unittest', $path), '', '', '', '', 'errorbox');
+    $ok = false;
+}
+
+// If we have something to test, do it.
+if ($ok) {
+    if ($path == $CFG->dirroot) {
+        $title = get_string('moodleunittests', 'unittest', get_string('all', 'unittest'));
+    } else {
+        $title = get_string('moodleunittests', 'unittest', $displaypath);
+    }
+    print_heading($title);
+    $test->run($reporter);
+}
+
+// Print the form for adjusting options.
+print_simple_box_start('center', '70%');
+echo '<form method="GET" action="index.php">';
+print_heading(get_string('retest', 'unittest'));
+echo '<p>'; print_checkbox('showpasses', 1, $showpasses, get_string('showpasses', 'unittest')); echo '</p>';
+echo '<p>'; print_checkbox('showsearch', 1, $showsearch, get_string('showsearch', 'unittest')); echo '</p>';
+echo '<p>'; print_checkbox('thorough', 1, $thorough, get_string('thorough', 'unittest')); echo '</p>';
+echo '<p>';
+    echo '<label for="path">', get_string('onlytest', 'unittest'), '</label> ';
+    echo '<input type="text" id="path" name="path" value="', $displaypath, '" size="60" />';
+echo '</p>';
+echo '<input type="submit" value="' . get_string('runtests', 'unittest') . '" />';
+echo '</form>';
+print_simple_box_end();
+
+// Footer.
+print_footer();
+
+?>
\ No newline at end of file
diff --git a/admin/report/simpletest/mod.php b/admin/report/simpletest/mod.php
new file mode 100644 (file)
index 0000000..eed76a5
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+/**
+ * This file is used to include a link to the unit tests on the report page
+ * /admin/report.php.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @version $Id$
+ * @package SimpleTestEx
+ */
+
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.'); //  It must be included from a Moodle page
+}
+
+print_heading(get_string('unittests', 'unittest'));
+print_heading('<a href="'.$CFG->wwwroot.'/admin/report/simpletest/index.php">' . 
+        get_string('rununittests', 'unittest') . '</a>', '', 3);
+?>
\ No newline at end of file
diff --git a/lang/en_utf8/unittest.php b/lang/en_utf8/unittest.php
new file mode 100644 (file)
index 0000000..e87fbb6
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+$string['all'] = 'ALL';
+$string['exception'] = 'Exception';
+$string['fail'] = 'Fail';
+$string['ignorethisfile'] = 'Re-run the tests ignoring this test file.';
+$string['ignorefile'] = 'Ignore tests in the file';
+$string['moodleunittests'] = 'Moodle unit tests: $a';
+$string['notice'] = 'Notice';
+$string['onlytest'] = 'Only run tests in';
+$string['pass'] = 'Pass';
+$string['pathdoesnotexist'] = 'The path \'$a\' does not exist.';
+$string['retest'] = 'Re-run the tests';
+$string['retestonlythisfile'] = 'Re-run only this test file.';
+$string['runall'] = 'Run the tests from all the test files.';
+$string['runat'] = 'Run at $a. ';
+$string['runonlyfile'] = 'Run only the tests in this file';
+$string['runonlyfolder'] = 'Run only the tests in this folder';
+$string['runtests'] = 'Run tests';
+$string['rununittests'] = 'Run the unit tests';
+$string['showpasses'] = 'Show passes as well as fails.';
+$string['showsearch'] = 'Show the search for test files.';
+$string['stacktrace'] = 'Stack trace:';
+$string['summary'] = '{$a->run}/{$a->total} test cases complete: <strong>{$a->passes}</strong> passes, <strong>{$a->fails}</strong> fails and <strong>{$a->exceptions}</strong> exceptions.';
+$string['thorough'] = 'Run a thorough test (may be slow).';
+$string['unittests'] = 'Unit tests';
+$string['uncaughtexception'] = 'Uncaught exception [{$a->getMessage()}] in [{$a->getFile()}:{$a->getLine()}] TESTS ABORTED.';
+$string['version'] = 'Using <a href=\"http://sourceforge.net/projects/simpletest/\">SimpleTest</a> version $a. ';
+?>
\ No newline at end of file
index 319145846f21fc81981125f96d7c156cd0174ef1..93f3ee039cda225977b066bdfb3ec56610e53b3e 100644 (file)
@@ -950,7 +950,7 @@ function get_field($table, $return, $field1, $value1, $field2='', $value2='', $f
     $rs = get_recordset_sql('SELECT ' . $return . ' FROM ' . $CFG->prefix . $table . ' ' . $select);
 
     if ($rs && $rs->RecordCount() == 1) {
-        return $rs->fields[$return];
+        return $rs->fields[0];
     } else {
         return false;
     }
@@ -3145,4 +3145,4 @@ function category_parent_visible($parent = 0) {
 }
 
 // vim:autoindent:expandtab:shiftwidth=4:tabstop=4:tw=140:
-?>
+?>
\ No newline at end of file
index 846c6b536f328dabf8ac2f3739cea60051f3ae8c..739a3fd3fe9e55a6c31b866e7307735ee80d84b8 100644 (file)
@@ -6822,10 +6822,35 @@ eval('
 
         return $obj;
     }
+
+    // Supply the PHP5 function scandir() to older versions.
+    function scandir($directory) {
+        $files = array();
+        if ($dh = opendir($directory)) {
+            while (($file = readdir($dh)) !== false) {
+               $files[] = $file;
+            }
+            closedir($dh);
+        }
+        return $files;
+    }
+
+    // Supply the PHP5 function array_combine() to older versions.
+    function array_combine($keys, $values) {
+        if (!is_array($keys) || !is_array($values) || count($keys) != count($values)) {
+            return false;
+        }
+        reset($values);
+        $result = array();
+        foreach ($keys as $key) {
+            $result[$key] = current($values);
+            next($values);
+        }
+        return $result;
+    }
 ');
 }
 
-
 /**
  * This function will make a complete copy of anything it's given, 
  * regardless of whether it's an object or not.
diff --git a/lib/simpletest/slowtestcode.php b/lib/simpletest/slowtestcode.php
new file mode 100644 (file)
index 0000000..f1ae716
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Code quality unit tests that are so slow you don't want to run them every time.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package SimpleTestEx
+ */
+
+/** */
+require_once(dirname(__FILE__) . '/../../config.php');
+
+global $CFG;
+require_once($CFG->libdir . '/simpletestlib.php');
+
+class slow_code_test extends UnitTestCase {
+    var $php_code_extensions = array('php', 'html', 'php\.inc');
+    var $ignore_folders = array();
+    var $phppath;
+    
+    function prepend_dirroot($string) {
+        global $CFG;
+        return $CFG->dirroot . $string;
+    }
+    
+    function test_php_syntax() {
+        global $CFG;
+        
+        // See if we can run php from the command line:
+        $this->phppath = 'php';
+        if (!shell_exec($this->phppath . ' -v')) {
+            // If not, we can't do anything.
+            $this->fail('Cannot test PHP syntax because PHP is not on the path.');
+            return;
+        }
+        
+        $regexp = '/\.(' . implode('|', $this->php_code_extensions) . ')$/';
+        $ignore = array_map(array($this, 'prepend_dirroot'), $this->ignore_folders);
+        recurseFolders($CFG->dirroot, array($this, 'syntax_check_file'), $regexp, false, $ignore); 
+    }
+    
+    var $dotcount = 0;
+    function syntax_check_file($filepath) {
+        // If you don't print something for each test, then for some reason the
+        // server hangs after a thousand files or so. It is very intermittent.
+        // Printing a space does not seem to be good enough.
+        echo '.';
+        if (++$this->dotcount % 100 == 0) {
+            echo '<br>';   
+        }
+        flush();
+        $output = shell_exec($this->phppath . ' -d max_execution_time=5 -d short_open_tag= -l ' . escapeshellarg($filepath));
+        $this->assertTrue(strpos($output, 'No syntax errors detected') === 0, $output);
+// This generates so many fails that it is currently useless.
+//        $this->assertTrue(stripos(file_get_contents($filepath), "\t") === false,
+//                    "File $filepath contains a tab character.");
+    }
+}
+?>
\ No newline at end of file
diff --git a/lib/simpletest/testcode.php b/lib/simpletest/testcode.php
new file mode 100644 (file)
index 0000000..f12ed0e
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Code quality unit tests that are fast enough to run each time.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package SimpleTestEx
+ */
+
+/** */
+require_once(dirname(__FILE__) . '/../../config.php');
+
+global $CFG;
+require_once($CFG->libdir . '/simpletestlib.php');
+
+class code_test extends UnitTestCase {
+    var $allok = array();
+    
+    var $badstrings;
+    var $extensions_to_ignore = array('exe', 'gif', 'ico', 'jpg', 'png', 'ttf');
+    var $ignore_folders = array();
+    
+    function test_dnc() {
+        global $CFG;
+        $regexp = '/\.(' . implode('|', $this->extensions_to_ignore) . ')$/';
+        $this->badstrings = array();
+        $this->badstrings['DONOT' . 'COMMIT'] = 'DONOT' . 'COMMIT'; // If we put the literal string here, it fails the test!
+        $this->badstrings['trailing whitespace'] = "[\t ][\r\n]";
+        foreach ($this->badstrings as $description => $ignored) {
+            $this->allok[$description] = true;
+        }
+        recurseFolders($CFG->dirroot, array($this, 'search_file_for_dnc'), $regexp, true);
+        foreach ($this->badstrings as $description => $ignored) {
+            if ($this->allok[$description]) {
+                $this->pass("No files contain $description.");
+            }
+        }
+    }
+    
+    function search_file_for_dnc($filepath) {
+        $content = file_get_contents($filepath);
+        foreach ($this->badstrings as $description => $badstring) {
+            $pass = (stripos($content, $badstring) === false);
+            if (!$pass) {
+                $this->fail("File $filepath contains $description.");
+                $this->allok[$description] = false;
+            }
+        }
+    }
+}
+?>
\ No newline at end of file
diff --git a/lib/simpletest/testdatalib.php b/lib/simpletest/testdatalib.php
new file mode 100644 (file)
index 0000000..b92defd
--- /dev/null
@@ -0,0 +1,264 @@
+<?php
+/**
+ * Unit tests for (some of) ../datalib.php.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package moodlecore
+ */
+
+/** */
+require_once(dirname(__FILE__) . '/../../config.php');
+
+global $CFG;
+require_once($CFG->libdir . '/simpletestlib.php');
+require_once($CFG->libdir . '/simpletestlib/web_tester.php');
+require_once($CFG->libdir . '/datalib.php');
+
+class datalib_test extends prefix_changing_test_case {
+    var $table = 'table';
+    var $data = array(
+            array('id',   'text', 'number'),
+            array(  1,    'frog',     101),
+            array(  2,    'toad',     102),
+            array(  3, 'tadpole',     103),
+            array(  4, 'tadpole',     104),
+        );
+    var $objects = array();
+
+    function setUp() {
+        global $CFG, $db;
+        parent::setUp();
+        wipe_tables($CFG->prefix, $db);
+        load_test_table($CFG->prefix . $this->table, $this->data, $db);
+        $keys = reset($this->data);
+        foreach ($this->data as $datum) {
+            if ($datum != $keys) {
+               $this->objects[$datum[0]] = (object) array_combine($keys, $datum);
+            }
+        }
+    }
+
+    function tearDown() {
+        global $CFG, $db;
+        remove_test_table($CFG->prefix . $this->table, $db);
+        parent::tearDown();
+    }
+
+    function test_where_clause() {
+        $this->assertEqual(where_clause('f1', 'v1'), "WHERE f1 = 'v1'");
+        $this->assertEqual(where_clause('f1', 'v1', 'f2', 2), "WHERE f1 = 'v1' AND f2 = '2'");
+        $this->assertEqual(where_clause('f1', 'v1', 'f2', 1.75, 'f3', 'v3'), "WHERE f1 = 'v1' AND f2 = '1.75' AND f3 = 'v3'");
+    }
+
+    function test_get_record() {
+        // Get particular records.
+        $this->assert(new CheckSpecifiedFieldsExpectation($this->objects[1]), get_record($this->table, 'id', 1), 'id = 1');
+        $this->assert(new CheckSpecifiedFieldsExpectation($this->objects[3]), get_record($this->table, 'text', 'tadpole', 'number', 103), 'text = tadpole AND number = 103');
+
+        // Abiguous get attempt, should return one, and print a warning in debug mode.
+        global $CFG;
+        $old_debug = $CFG->debug;
+        $CFG->debug = 0;
+
+        ob_start();
+        $record = get_record($this->table, 'text', 'tadpole');
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assertEqual('', $result, '%s (No error ouside debug mode).');
+
+        $CFG->debug = E_ALL;
+        ob_start();
+        $record = get_record($this->table, 'text', 'tadpole');
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assert(new TextExpectation('Error:'), $result, 'Error in debug mode.');
+
+        $CFG->debug = $old_debug;
+
+        // Return only specified fields
+        $expected = new stdClass;
+        $expected->id = 3;
+        $expected->text = 'tadpole';
+        $result = get_record($this->table, 'id', '3', '', '', '', '', 'id,text');
+        $this->assert(new CheckSpecifiedFieldsExpectation($expected), $result);
+        $this->assertFalse(isset($result->number));
+        $expected = new stdClass;
+        $expected->text = 'tadpole';
+        $expected->number = 103;
+        $result = get_record($this->table, 'id', '3', '', '', '', '', 'text,number');
+        $this->assert(new CheckSpecifiedFieldsExpectation($expected), $result);
+        $this->assertFalse(isset($result->id));
+
+        // Attempting to get a non-existant records should return false.
+        $this->assertFalse(get_record($this->table, 'text', 'not there'), 'attempt to get non-existant record');
+    }
+
+    function test_get_record_sql() {
+        global $CFG;
+        // Get particular records.
+        $this->assert(new CheckSpecifiedFieldsExpectation($this->objects[1]), get_record_sql("SELECT * FROM {$CFG->prefix}" . $this->table . " WHERE id = '1'", 'id = 1'));
+
+        // Abiguous get attempt, should return one, and print a warning in debug mode, unless $expectmultiple is used.
+        $old_debug = $CFG->debug;
+        $CFG->debug = 0;
+
+        ob_start();
+        $record = get_record_sql("SELECT * FROM {$CFG->prefix}" . $this->table . " WHERE text = 'tadpole'");
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assertEqual('', $result, '%s (No error ouside debug mode).');
+
+        $CFG->debug = E_ALL;
+        ob_start();
+        $record = get_record_sql("SELECT * FROM {$CFG->prefix}" . $this->table . " WHERE text = 'tadpole'");
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assert(new TextExpectation('Error:'), $result, 'Error in debug mode.');
+
+        ob_start();
+        $record = get_record_sql("SELECT * FROM {$CFG->prefix}" . $this->table . " WHERE text = 'tadpole'", true);
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assertEqual('', $result, '%s (No error ouside debug mode).');
+
+        $CFG->debug = $old_debug;
+
+        // Attempting to get a non-existant records should return false.
+        $this->assertFalse(get_record_sql("SELECT * FROM {$CFG->prefix}" . $this->table . " WHERE text = 'not there'"), 'attempt to get non-existant record');
+    }
+
+    function test_get_record_select() {
+        // Get particular records.
+        $this->assert(new CheckSpecifiedFieldsExpectation($this->objects[2]), get_record_select($this->table, 'id > 1 AND id < 3'), 'id > 1 AND id < 3');
+
+        // Abiguous get attempt, should return one, and print a warning in debug mode.
+        global $CFG;
+        $old_debug = $CFG->debug;
+        $CFG->debug = 0;
+
+        ob_start();
+        $record = get_record_select($this->table, "text = 'tadpole'");
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assertEqual('', $result, '%s (No error ouside debug mode).');
+
+        $CFG->debug = E_ALL;
+        ob_start();
+        $record = get_record_select($this->table, "text = 'tadpole'");
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assert(new TextExpectation('Error:'), $result, 'Error in debug mode.');
+
+        $CFG->debug = $old_debug;
+
+        // Return only specified fields
+        $expected = new stdClass;
+        $expected->id = 1;
+        $expected->text = 'frog';
+        $result = get_record_select($this->table, "text = 'frog'", 'id,text');
+        $this->assert(new CheckSpecifiedFieldsExpectation($expected), $result);
+        $this->assertFalse(isset($result->number));
+
+        // Attempting to get a non-existant records should return false.
+        $this->assertFalse(get_record_select($this->table, 'id > 666'), 'attempt to get non-existant record');
+    }
+
+    function test_get_field() {
+        $this->assertEqual(get_field($this->table, 'number', 'id', 1), 101);
+        $this->assertEqual(get_field($this->table, 'text', 'number', 102), 'toad');
+        $this->assertEqual(get_field($this->table, 'number', 'text', 'tadpole', 'id', 4), 104);
+        $this->assertEqual(get_field($this->table, 'number + id', 'text', 'tadpole', 'id', 4), 108);
+    }
+
+    function test_set_field() {
+        set_field($this->table, 'number', 12345, 'id', 1);
+        $this->assertEqual(get_field($this->table, 'number', 'id', 1), 12345);
+
+        set_field($this->table, 'text', 'newvalue', 'number', 102);
+        $this->assertEqual(get_field($this->table, 'text', 'number', 102), 'newvalue');
+
+        set_field($this->table, 'number', -1, 'text', 'tadpole', 'id', 4);
+        $this->assertEqual(get_field($this->table, 'number', 'text', 'tadpole', 'id', 4), -1);
+    }
+
+    function test_delete_records() {
+        delete_records($this->table, 'id', 666);
+        $this->assertEqual(count_records($this->table), 4);
+        delete_records($this->table, 'id', 1);
+        $this->assertEqual(count_records($this->table), 3);
+        delete_records($this->table, 'text', 'tadpole');
+        $this->assertEqual(count_records($this->table), 1);
+    }
+
+    function test_delete_records2() {
+        delete_records($this->table, 'text', 'tadpole', 'id', 4);
+        $this->assertEqual(count_records($this->table), 3);
+        delete_records($this->table);
+        $this->assertEqual(count_records($this->table), 0);
+    }
+
+    function test_delete_records_select() {
+        delete_records_select($this->table, "text LIKE 't%'");
+        $this->assertEqual(count_records($this->table), 1);
+        delete_records_select($this->table, "'1' = '1'");
+        $this->assertEqual(count_records($this->table), 0);
+    }
+
+//function insert_record($table, $dataobject, $returnid=true, $primarykey='id', $feedback=true) {
+    function test_insert_record() {
+        // Simple insert with $returnid
+        $obj = new stdClass;
+        $obj->text = 'new entry';
+        $obj->number = 123;
+        $this->assertEqual(insert_record($this->table, $obj), 5);
+        $obj->id = 5;
+        $this->assert(new CheckSpecifiedFieldsExpectation($obj, 'Simple insert with returnid (%s)'), get_record($this->table, 'id', 5));
+        
+        // Simple insert without $returnid
+        $obj = new stdClass;
+        $obj->text = 'newer entry';
+        $obj->number = 321;
+        $this->assertEqual(insert_record($this->table, $obj, false), true);
+        $obj->id = 6;
+        $this->assert(new CheckSpecifiedFieldsExpectation($obj, 'Simple insert without returnid (%s)'), get_record($this->table, 'id', 6));
+        
+        // Insert with missing columns - should get defaults.
+        $obj = new stdClass;
+        $obj->text = 'partial entry';
+        $this->assertEqual(insert_record($this->table, $obj), 7);
+        $obj->id = 7;
+        $obj->number = 0xDefa;
+        $got = get_record($this->table, 'id', 7);
+        $this->assert(new CheckSpecifiedFieldsExpectation($obj, 'Insert with missing columns - should get defaults (%s)'), get_record($this->table, 'id', 7));
+        
+        // Insert with extra columns - should be ingnored.
+        $obj = new stdClass;
+        $obj->text = 'entry with extra';
+        $obj->number = 747;
+        $obj->unused = 666;
+        $this->assertEqual(insert_record($this->table, $obj), 8);
+        $obj->id = 8;
+        unset($obj->unused);
+        $this->assert(new CheckSpecifiedFieldsExpectation($obj, 'Insert with extra columns - should be ingnored (%s)'), get_record($this->table, 'id', 8));
+        
+        // Insert into nonexistant table - should fail.
+        $obj = new stdClass;
+        $obj->text = 'new entry';
+        $obj->number = 123;
+        $this->assertFalse(insert_record('nonexistant_table', $obj), 'Insert into nonexistant table');
+        
+        // Insert bad data - error should be printed.
+        $obj = new stdClass;
+        $obj->text = 'new entry';
+        $obj->number = 'not a number';
+        ob_start();
+        $this->assertFalse(insert_record($this->table, $obj), 'Insert bad data - should fail.');
+        $result = ob_get_contents();
+        ob_end_clean();
+        $this->assert(new TextExpectation('ERROR:'), $result, 'Insert bad data - error should have been printed.');
+    }
+}
+
+?>
diff --git a/lib/simpletestlib.php b/lib/simpletestlib.php
new file mode 100644 (file)
index 0000000..4972648
--- /dev/null
@@ -0,0 +1,376 @@
+<?php
+/**
+ * Utility functions to make unit testing easier.
+ * 
+ * These functions, particularly the the database ones, are quick and
+ * dirty methods for getting things done in test cases. None of these 
+ * methods should be used outside test code.
+ *
+ * @copyright &copy; 2006 The Open University
+ * @author T.J.Hunt@open.ac.uk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @version $Id$
+ * @package SimpleTestEx
+ */
+
+require_once(dirname(__FILE__) . '/../config.php');
+require_once($CFG->libdir . '/simpletestlib/simpletest.php');
+require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
+require_once($CFG->libdir . '/simpletestlib/expectation.php');
+
+/**
+ * Recursively visit all the files in the source tree. Calls the callback
+ * function with the pathname of each file found. 
+ * 
+ * @param $path the folder to start searching from. 
+ * @param $callback the function to call with the name of each file found.
+ * @param $fileregexp a regexp used to filter the search (optional).
+ * @param $exclude If true, pathnames that match the regexp will be ingored. If false, 
+ *     only files that match the regexp will be included. (default false).
+ * @param array $ignorefolders will not go into any of these folders (optional).
+ */ 
+function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
+    $files = scandir($path);
+
+    foreach ($files as $file) {
+        $filepath = $path .'/'. $file;
+        if ($file == '.' || $file == '..') {
+            continue;
+        } else if (is_dir($filepath)) {
+            if (!in_array($filepath, $ignorefolders)) {
+                recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
+            }
+        } else if ($exclude xor preg_match($fileregexp, $filepath)) {
+            call_user_func($callback, $filepath);
+        }
+    }
+}
+
+/**
+ * An expectation for comparing strings ignoring whitespace.
+ */
+class IgnoreWhitespaceExpectation extends SimpleExpectation {
+    var $expect;
+
+    function IgnoreWhitespaceExpectation($content, $message = '%s') {
+        $this->SimpleExpectation($message);
+        $this->expect=$this->normalise($content);
+    }
+
+    function test($ip) {
+        return $this->normalise($ip)==$this->expect;
+    }
+
+    function normalise($text) {
+        return preg_replace('/\s+/m',' ',trim($text));
+    }
+
+    function testMessage($ip) {
+        return "Input string [$ip] doesn't match the required value.";
+    }
+}
+
+/**
+ * An Expectation that two arrays contain the same list of values.
+ */
+class ArraysHaveSameValuesExpectation extends SimpleExpectation {
+    var $expect;
+
+    function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
+        $this->SimpleExpectation($message);
+        if (!is_array($expected)) {
+            trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
+                    'with an expected value that is not an array.');
+        }
+        $this->expect = $this->normalise($expected);
+    }
+
+    function test($actual) {
+        return $this->normalise($actual) == $this->expect;
+    }
+
+    function normalise($array) {
+        sort($array);
+        return $array;
+    }
+
+    function testMessage($actual) {
+        return 'Array [' . implode(', ', $actual) .
+                '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
+    }
+}
+
+/**
+ * An Expectation that compares to objects, and ensures that for every field in the
+ * expected object, there is a key of the same name in the actual object, with
+ * the same value. (The actual object may have other fields to, but we ignore them.)
+ */
+class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
+    var $expect;
+
+    function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
+        $this->SimpleExpectation($message);
+        if (!is_object($expected)) {
+            trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
+                    'with an expected value that is not an object.');
+        }
+        $this->expect = $expected;
+    }
+
+    function test($actual) {
+        foreach ($this->expect as $key => $value) {
+            if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
+                // OK
+            } else if (is_null($value) && is_null($actual->$key)) {
+                // OK
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    function testMessage($actual) {
+        $mismatches = array();
+        foreach ($this->expect as $key => $value) {
+            if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
+                // OK
+            } else if (is_null($value) && is_null($actual->$key)) {
+                // OK
+            } else {
+                $mismatches[] = $key;
+            }
+        }
+        return 'Actual object does not have all the same fields with the same values as the expected object (' .
+                implode(', ', $mismatches) . ').';
+    }
+}
+
+/**
+ * Given a table name, a two-dimensional array of data, and a database connection,
+ * creates a table in the database. The array of data should look something like this.
+ *
+ * $testdata = array(
+ *      array('id', 'username', 'firstname', 'lastname', 'email'),
+ *      array(1,    'u1',       'user',      'one',      'u1@example.com'),
+ *      array(2,    'u2',       'user',      'two',      'u2@example.com'),
+ *      array(3,    'u3',       'user',      'three',    'u3@example.com'),
+ *      array(4,    'u4',       'user',      'four',     'u4@example.com'),
+ *      array(5,    'u5',       'user',      'five',     'u5@example.com'),
+ *  );
+ *
+ * The first 'row' of the test data gives the column names. The type of each column
+ * is set to either INT or VARCHAR($strlen), guessed by inspecting the first row of
+ * data. Unless the col name is 'id' in which case the col type will be SERIAL.
+ * The remaining 'rows' of the data array are values loaded into the table. All columns
+ * are created with a default of 0xdefa or 'Default' as appropriate.
+ * 
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param string $tablename the name of the table to create. E.g. 'mdl_unittest_user'.
+ * @param array $data a two-dimensional array of data, in the format described above.
+ * @param object $db an AdoDB database connection.
+ * @param int $strlen the width to use for string fields.
+ */
+function load_test_table($tablename, $data, $db, $strlen = 255) {
+    $colnames = array_shift($data);
+    $coldefs = array();
+    foreach (array_combine($colnames, $data[0]) as $colname => $value) {
+        if ($colname == 'id') {
+            $type = 'SERIAL';
+        } else if (is_int($value)) {
+            $type = 'INTEGER DEFAULT 57082'; // 0xdefa
+        } else {
+            $type = "VARCHAR($strlen) DEFAULT 'Default'";
+        }
+        $coldefs[] = "$colname $type";
+    }
+    _private_execute_sql("CREATE TABLE $tablename (" . join(',', $coldefs) . ');', $db);
+
+    array_unshift($data, $colnames);
+    load_test_data($tablename, $data, $db);
+}
+
+/**
+ * Given a table name, a two-dimensional array of data, and a database connection,
+ * adds data to the database table. The array should have the same format as for
+ * load_test_table(), with the first 'row' giving column names.
+ * 
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param string $tablename the name of the table to populate. E.g. 'mdl_unittest_user'.
+ * @param array $data a two-dimensional array of data, in the format described.
+ * @param object $db an AdoDB database connection.
+ */
+function load_test_data($tablename, $data, $db) {
+    global $CFG;
+    $colnames = array_shift($data);
+    $idcol = array_search('id', $colnames);
+    $maxid = -1;
+    foreach ($data as $row) {
+        _private_execute_sql($db->GetInsertSQL($tablename, array_combine($colnames, $row)), $db);
+        if ($idcol !== false && $row[$idcol] > $maxid) {
+            $maxid = $row[$idcol];
+        }
+    }
+    if ($CFG->dbtype == 'postgres7' && $idcol !== false) {
+        $maxid += 1;
+        _private_execute_sql("ALTER SEQUENCE {$tablename}_id_seq RESTART WITH $maxid;", $db);
+    }
+}
+
+/**
+ * Make multiple tables that are the same as a real table but empty.
+ * 
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param mixed $tablename Array of strings containing the names of the table to populate (without prefix).
+ * @param string $realprefix the prefix used for real tables. E.g. 'mdl_'.
+ * @param string $testprefix the prefix used for test tables. E.g. 'mdl_unittest_'.
+ * @param object $db an AdoDB database connection.
+ */
+function make_test_tables_like_real_one($tablenames, $realprefix, $testprefix, $db,$dropconstraints=false) {
+    foreach($tablenames as $individual) {
+        make_test_table_like_real_one($individual,$realprefix,$testprefix,$db,$dropconstraints);
+    }
+}
+
+/**
+ * Make a test table that has all the same columns as a real moodle table,
+ * but which is empty.
+ *
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param string $tablename Name of the table to populate. E.g. 'user'.
+ * @param string $realprefix the prefix used for real tables. E.g. 'mdl_'.
+ * @param string $testprefix the prefix used for test tables. E.g. 'mdl_unittest_'.
+ * @param object $db an AdoDB database connection.
+ */
+function make_test_table_like_real_one($tablename, $realprefix, $testprefix, $db, $dropconstraints=false) {
+    _private_execute_sql("CREATE TABLE $testprefix$tablename (LIKE $realprefix$tablename INCLUDING DEFAULTS);", $db);
+    if (_private_has_id_column($testprefix . $tablename, $db)) {
+        _private_execute_sql("CREATE SEQUENCE $testprefix{$tablename}_id_seq;", $db);
+        _private_execute_sql("ALTER TABLE $testprefix$tablename ALTER COLUMN id SET DEFAULT nextval('{$testprefix}{$tablename}_id_seq'::regclass);", $db);
+        _private_execute_sql("ALTER TABLE $testprefix$tablename ADD PRIMARY KEY (id);", $db);
+    }
+    if($dropconstraints) {
+        $cols=$db->MetaColumnNames($testprefix.$tablename);
+        foreach($cols as $col) {
+            $rs=_private_execute_sql(
+                "SELECT constraint_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='$testprefix$tablename'",$db);
+            while(!$rs->EOF) {
+                $constraintname=$rs->fields['constraint_name'];
+                _private_execute_sql("ALTER TABLE $testprefix$tablename DROP CONSTRAINT $constraintname",$db);
+                $rs->MoveNext();
+            }
+
+            _private_execute_sql("ALTER TABLE $testprefix$tablename ALTER COLUMN $col DROP NOT NULL",$db);
+        }
+    }
+}
+
+/**
+ * Drops a table from the database pointed to by the database connection.
+ * This undoes the create performed by load_test_table().
+ *
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param string $tablename the name of the table to populate. E.g. 'mdl_unittest_user'.
+ * @param object $db an AdoDB database connection.
+ * @param bool $cascade If true, also drop tables that depend on this one, e.g. through
+ *      foreign key constraints.
+ */
+function remove_test_table($tablename, $db, $cascade = false) {
+    global $CFG;
+    _private_execute_sql('DROP TABLE ' . $tablename . ($cascade ? ' CASCADE' : '') . ';', $db);
+    
+    if ($CFG->dbtype == 'postgres7') {
+        $rs = $db->Execute("SELECT relname FROM pg_class WHERE relname = '{$tablename}_id_seq' AND relkind = 'S';");
+        if ($rs && $rs->RecordCount()) {
+            _private_execute_sql("DROP SEQUENCE {$tablename}_id_seq;", $db);
+        }
+    }
+}
+
+/**
+ * Drops all the tables with a particular prefix from the database pointed to by the database connection.
+ * Useful for cleaning up after a unit test run has crashed leaving the DB full of junk.
+ *
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param string $prefix the prfix of tables to drop 'mdl_unittest_'.
+ * @param object $db an AdoDB database connection.
+ */
+function wipe_tables($prefix, $db) {
+    if (strpos($prefix, 'test') === false) {
+        notice('The wipe_tables function should only be used to wipe test tables.');
+        return;
+    }
+    $tables = $db->Metatables('TABLES', false, "$prefix%");
+    foreach ($tables as $table) {
+        _private_execute_sql("DROP TABLE $table CASCADE", $db);
+    }
+}
+
+/**
+ * Drops all the sequences with a particular prefix from the database pointed to by the database connection.
+ * Useful for cleaning up after a unit test run has crashed leaving the DB full of junk.
+ *
+ * This function should not be used in real code. Only for testing and debugging.
+ *
+ * @param string $prefix the prfix of sequences to drop 'mdl_unittest_'.
+ * @param object $db an AdoDB database connection.
+ */
+function wipe_sequences($prefix, $db) {
+    if ($CFG->dbtype == 'postgres7') {
+        $sequences = $db->GetCol("SELECT relname FROM pg_class WHERE relname LIKE '$prefix%_id_seq' AND relkind = 'S';");
+        if ($sequences) {
+            foreach ($sequences as $sequence) {
+                _private_execute_sql("DROP SEQUENCE $sequence CASCADE", $db);
+            }
+        }
+    }
+}
+
+function _private_has_id_column($table, $db) {
+    return in_array('id', $db->MetaColumnNames($table));
+}
+
+function _private_execute_sql($sql, $db) {
+    if (!$rs = $db->Execute($sql)) {
+        echo '<p>SQL ERROR: ', $db->ErrorMsg(), ". STATEMENT: $sql</p>";
+    }
+    return $rs;
+}
+
+/**
+ * Base class for testcases that want a different DB prefix.
+ * 
+ * That is, when you need to load test data into the database for
+ * unit testing, instead of messing with the real mdl_course table,
+ * we will temporarily change $CFG->prefix from (say) mdl_ to mdl_unittest_
+ * and create a table called mdl_unittest_course to hold the test data.
+ */
+class prefix_changing_test_case extends UnitTestCase {
+    var $old_prefix;
+    
+    function change_prefix() {
+        global $CFG;
+        $this->old_prefix = $CFG->prefix;
+        $CFG->prefix = $CFG->prefix . 'unittest_';
+    }
+
+    function change_prefix_back() {
+        global $CFG;
+        $CFG->prefix = $this->old_prefix;
+    }
+
+    function setUp() {
+        $this->change_prefix();
+    }
+
+    function tearDown() {
+        $this->change_prefix_back();
+    }
+}
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/BACKLOG b/lib/simpletestlib/BACKLOG
new file mode 100644 (file)
index 0000000..03c4730
--- /dev/null
@@ -0,0 +1,46 @@
+BACKLOG
+This is backed up stuff that defines the 1.0.1 release.
+
+$Id$
+
+Unit tester
+-----------
+Add skip facility (4).
+
+Reporter
+--------
+Add a repeating reporter (2).
+Split and refactor dumpers by type (2).
+
+Mock objects
+------------
+
+Parser
+------
+Add U flag to regexes to allow unicode (3).
+Parse fieldsets correctly with labels (5).
+
+Browser
+-------
+Change getUrls() to return more information (2).
+Add a file() loading command (2).
+Investigate bug, clicking keeps encoding again and again (3).
+Fix bug URL losing spaces (3).
+Setting an option by id attribute should only toggle that option (3).
+Add additional parameters to click() method (1).
+Allow index selection of controls (3).
+
+Web tester
+----------
+Add assertClickable() method (1).
+Add assertSubmit() method (1).
+
+Documentation
+-------------
+Extension docs (1).
+Replace all deprecated methods (3).
+Split mock objects off fromtutorial (4).
+Add page for projects that use or extend SimpleTest (2).
+
+Build
+-----
diff --git a/lib/simpletestlib/HELP_MY_TESTS_DONT_WORK_ANYMORE b/lib/simpletestlib/HELP_MY_TESTS_DONT_WORK_ANYMORE
new file mode 100644 (file)
index 0000000..2c9ae6f
--- /dev/null
@@ -0,0 +1,312 @@
+Simple Test interface changes
+=============================
+Because the SimpleTest tool set is still evolving it is likely that tests
+written with earlier versions will fail with the newest ones. The most
+dramatic changes are in the alpha releases. Here is a list of possible
+problems and their fixes...
+
+Failure to connect now emits failures
+-------------------------------------
+It used to be that you would have to use the
+getTransferError() call on the web tester to see if
+there was a socket level error in a fetch. This check
+is now always carried out by the WebTestCase unless
+the fetch is prefaced with WebTestCase::ignoreErrors().
+The ignore directive only lasts for test case fetching
+action such as get() and click().
+
+No method SimpleTestOptions::ignore()
+-------------------------------------
+This is deprecated in version 1.0.1beta and has been moved
+to SimpleTest::ignore() as that is more readable. In
+addition, parent classes are also ignored automatically.
+If you are using PHP5 you can skip this directive simply
+by marking your test case as abstract.
+
+No method assertCopy()
+----------------------
+This is deprecated in 1.0.1 in favour of assertClone().
+The assertClone() method is slightly different in that
+the objects must be identical, but without being a
+reference. It is thus not a strict inversion of
+assertReference().
+
+Constructor wildcard override has no effect in mocks
+----------------------------------------------------
+As of 1.0.1beta this is now set with setWildcard() instead
+of in the constructor.
+
+No methods setStubBaseClass()/getStubBaseClass()
+------------------------------------------------
+As mocks are now used instead of stubs, these methods
+stopped working and are now removed as of the 1.0.1beta
+release. The mock objects may be freely used instead.
+
+No method addPartialMockCode()
+------------------------------
+The ability to insert arbitrary partial mock code
+has been removed. This was a low value feature
+causing needless complications.It was removed
+in the 1.0.1beta release.
+
+No method setMockBaseClass()
+----------------------------
+The ability to change the mock base class has been
+scheduled for removal and is deprecated since the
+1.0.1beta version. This was a rarely used feature
+except as a workaround for PHP5 limitations. As
+these limitations are being resolved it's hoped
+that the bundled mocks can be used directly.
+
+No class Stub
+-------------
+Server stubs are deprecated from 1.0.1 as the mocks now
+have exactly the same interface. Just use mock objects
+instead.
+
+No class SimpleTestOptions
+--------------------------
+This was replced by the shorter SimpleTest in 1.0.1beta1
+and is since deprecated.
+
+No file simple_test.php
+-----------------------
+This was renamed test_case.php in 1.0.1beta to more accurately
+reflect it's purpose. This file should never be directly
+included in test suites though, as it's part of the
+underlying mechanics and has a tendency to be refactored.
+
+No class WantedPatternExpectation
+---------------------------------
+This was deprecated in 1.0.1alpha in favour of the simpler
+name PatternExpectation.
+
+No class NoUnwantedPatternExpectation
+-------------------------------------
+This was deprecated in 1.0.1alpha in favour of the simpler
+name NoPatternExpectation.
+
+No method assertNoUnwantedPattern()
+-----------------------------------
+This has been renamed to assertNoPattern() in 1.0.1alpha and
+the old form is deprecated.
+
+No method assertWantedPattern()
+-------------------------------
+This has been renamed to assertPattern() in 1.0.1alpha and
+the old form is deprecated.
+
+No method assertExpectation()
+-----------------------------
+This was renamed as assert() in 1.0.1alpha and the old form
+has been deprecated.
+
+No class WildcardExpectation
+----------------------------
+This was a mostly internal class for the mock objects. It was
+renamed AnythingExpectation to bring it closer to JMock and
+NMock in version 1.0.1alpha.
+
+Missing UnitTestCase::assertErrorPattern()
+------------------------------------------
+This method is deprecated for version 1.0.1 onwards.
+This method has been subsumed by assertError() that can now
+take an expectation. Simply pass a PatternExpectation
+into assertError() to simulate the old behaviour.
+
+No HTML when matching page elements
+-----------------------------------
+This behaviour has been switched to using plain text as if it
+were seen by the user of the browser. This means that HTML tags
+are suppressed, entities are converted and whitespace is
+normalised. This should make it easier to match items in forms.
+Also images are replaced with their "alt" text so that they
+can be matched as well.
+
+No method SimpleRunner::_getTestCase()
+--------------------------------------
+This was made public as getTestCase() in 1.0RC2.
+
+No method restartSession()
+--------------------------
+This was renamed to restart() in the WebTestCase, SimpleBrowser
+and the underlying SimpleUserAgent in 1.0RC2. Because it was
+undocumented anyway, no attempt was made at backward
+compatibility.
+
+My custom test case ignored by tally()
+--------------------------------------
+The _assertTrue method has had it's signature changed due to a bug
+in the PHP 5.0.1 release. You must now use getTest() from within
+that method to get the test case. Mock compatibility with other
+unit testers is now deprecated as of 1.0.1alpha as PEAR::PHUnit2
+should soon have mock support of it's own.
+
+Broken code extending SimpleRunner
+----------------------------------
+This was replaced with SimpleScorer so that I could use the runner
+name in another class. This happened in RC1 development and there
+is no easy backward compatibility fix. The solution is simply to
+extend SimpleScorer instead.
+
+Missing method getBaseCookieValue()
+-----------------------------------
+This was renamed getCurrentCookieValue() in RC1.
+
+Missing files from the SimpleTest suite
+---------------------------------------
+Versions of SimpleTest prior to Beta6 required a SIMPLE_TEST constant
+to point at the SimpleTest folder location before any of the toolset
+was loaded. This is no longer documented as it is now unnecessary
+for later versions. If you are using an earlier version you may
+need this constant. Consult the documentation that was bundled with
+the release that you are using or upgrade to Beta6 or later.
+
+No method SimpleBrowser::getCurrentUrl()
+--------------------------------------
+This is replaced with the more versatile showRequest() for
+debugging. It only existed in this context for version Beta5.
+Later versions will have SimpleBrowser::getHistory() for tracking
+paths through pages. It is renamed as getUrl() since 1.0RC1.
+
+No method Stub::setStubBaseClass()
+----------------------------------
+This method has finally been removed in 1.0RC1. Use
+SimpleTestOptions::setStubBaseClass() instead.
+
+No class CommandLineReporter
+----------------------------
+This was renamed to TextReporter in Beta3 and the deprecated version
+was removed in 1.0RC1.
+
+No method requireReturn()
+-------------------------
+This was deprecated in Beta3 and is now removed.
+
+No method expectCookie()
+------------------------
+This method was abruptly removed in Beta4 so as to simplify the internals
+until another mechanism can replace it. As a workaround it is necessary
+to assert that the cookie has changed by setting it before the page
+fetch and then assert the desired value.
+
+No method clickSubmitByFormId()
+-------------------------------
+This method had an incorrect name as no button was involved. It was
+renamed to submitByFormId() in Beta4 and the old version deprecated.
+Now removed.
+
+No method paintStart() or paintEnd()
+------------------------------------
+You should only get this error if you have subclassed the lower level
+reporting and test runner machinery. These methods have been broken
+down into events for test methods, events for test cases and events
+for group tests. The new methods are...
+
+paintStart() --> paintMethodStart(), paintCaseStart(), paintGroupStart()
+paintEnd() --> paintMethodEnd(), paintCaseEnd(), paintGroupEnd()
+
+This change was made in Beta3, ironically to make it easier to subclass
+the inner machinery. Simply duplicating the code you had in the previous
+methods should provide a temporary fix.
+
+No class TestDisplay
+--------------------
+This has been folded into SimpleReporter in Beta3 and is now deprecated.
+It was removed in RC1.
+
+No method WebTestCase::fetch()
+------------------------------
+This was renamed get() in Alpha8. It is removed in Beta3.
+
+No method submit()
+------------------
+This has been renamed clickSubmit() in Beta1. The old method was
+removed in Beta2.
+
+No method clearHistory()
+------------------------
+This method is deprecated in Beta2 and removed in RC1.
+
+No method getCallCount()
+------------------------
+This method has been deprecated since Beta1 and has now been
+removed. There are now more ways to set expectations on counts
+and so this method should be unecessery. Removed in RC1.
+
+Cannot find file *
+------------------
+The following public name changes have occoured...
+
+simple_html_test.php --> reporter.php
+simple_mock.php --> mock_objects.php
+simple_unit.php --> unit_tester.php
+simple_web.php --> web_tester.php
+
+The old names were deprecated in Alpha8 and removed in Beta1.
+
+No method attachObserver()
+--------------------------
+Prior to the Alpha8 release the old internal observer pattern was
+gutted and replaced with a visitor. This is to trade flexibility of
+test case expansion against the ease of writing user interfaces.
+
+Code such as...
+
+$test = &new MyTestCase();
+$test->attachObserver(new TestHtmlDisplay());
+$test->run();
+
+...should be rewritten as...
+
+$test = &new MyTestCase();
+$test->run(new HtmlReporter());
+
+If you previously attached multiple observers then the workaround
+is to run the tests twice, once with each, until they can be combined.
+For one observer the old method is simulated in Alpha 8, but is
+removed in Beta1.
+
+No class TestHtmlDisplay
+------------------------
+This class has been renamed to HtmlReporter in Alpha8. It is supported,
+but deprecated in Beta1 and removed in Beta2. If you have subclassed
+the display for your own design, then you will have to extend this
+class (HtmlReporter) instead.
+
+If you have accessed the event queue by overriding the notify() method
+then I am afraid you are in big trouble :(. The reporter is now
+carried around the test suite by the runner classes and the methods
+called directly. In the unlikely event that this is a problem and
+you don't want to upgrade the test tool then simplest is to write your
+own runner class and invoke the tests with...
+
+$test->accept(new MyRunner(new MyReporter()));
+
+...rather than the run method. This should be easier to extend
+anyway and gives much more control. Even this method is overhauled
+in Beta3 where the runner class can be set within the test case. Really
+the best thing to do is to upgrade to this version as whatever you were
+trying to achieve before should now be very much easier.
+
+Missing set options method
+--------------------------
+All test suite options are now in one class called SimpleTestOptions.
+This means that options are set differently...
+
+GroupTest::ignore() --> SimpleTestOptions::ignore()
+Mock::setMockBaseClass() --> SimpleTestOptions::setMockBaseClass()
+
+These changed in Alpha8 and the old versions are now removed in RC1.
+
+No method setExpected*()
+------------------------
+The mock expectations changed their names in Alpha4 and the old names
+ceased to be supported in Alpha8. The changes are...
+
+setExpectedArguments() --> expectArguments()
+setExpectedArgumentsSequence() --> expectArgumentsAt()
+setExpectedCallCount() --> expectCallCount()
+setMaximumCallCount() --> expectMaximumCallCount()
+
+The parameters remained the same.
diff --git a/lib/simpletestlib/LICENSE b/lib/simpletestlib/LICENSE
new file mode 100644 (file)
index 0000000..4a1f720
--- /dev/null
@@ -0,0 +1,502 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                      Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+     51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                 GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/lib/simpletestlib/README b/lib/simpletestlib/README
new file mode 100644 (file)
index 0000000..c2ff9b7
--- /dev/null
@@ -0,0 +1,108 @@
+SimpleTest
+==========
+You probably got this package from...
+http://sourceforge.net/projects/simpletest/
+
+If there is no licence agreement with this package please download
+a version from the location above. You must read and accept that
+licence to use this software. The file is titled simply LICENSE.
+
+What is it? It's a framework for unit testing, web site testing and
+mock objects for PHP 4.2.0+.
+
+If you have used JUnit you will find this PHP unit testing version very
+similar. Also included is a mock objects and server stubs generator.
+The stubs can have return values set for different arguments, can have
+sequences set also by arguments and can return items by reference.
+The mocks inherit all of this functionality and can also have
+expectations set, again in sequences and for different arguments.
+
+A web tester similar in concept to JWebUnit is also included. There is no
+JavaScript or tables support, but forms, authentication, cookies and
+frames are handled.
+
+You can see a release schedule at http://www.lastcraft.com/overview.php
+which is also copied to the documentation folder with this release.
+A full PHPDocumenter API documentation exists at
+http://simpletest.sourceforge.net/.
+
+The user interface is minimal
+in the extreme, but a lot of information flows from the test suite.
+After version 1.0 we will release a better web UI, but we are leaving XUL
+and GTk versions to volunteers as everybody has their own opinion
+on a good GUI, and we don't want to discourage development by shipping
+one with the toolkit.
+
+You are looking at a first full release. The unit tests for SimpleTest
+itself can be run here...
+
+simpletest/test/unit_tests.php
+
+And tests involving live network connections as well are here...
+
+simpletest/test/all_tests.php
+
+The full tests will typically overrun the 8Mb limit usually allowed
+to a PHP process. A workaround is to run the tests on the command
+with a custom php.ini file if you do not have access to your server
+version.
+
+You will have to edit the all_tests.php file if you are accesssing
+the internet through a proxy server. See the comments in all_tests.php
+for instructions.
+
+The full tests read some test data from the LastCraft site. If the site
+is down or has been modified for a later version then you will get
+spurious errors. A unit_tests.php failure on the other hand would be
+very serious. As far as we know we haven't yet managed to check in any
+unit test failures so please correct us if you find one.
+
+Even if all of the tests run please verify that your existing test suites
+also function as expected. If they don't see the file...
+
+HELP_MY_TESTS_DONT_WORK_ANYMORE
+
+This contains information on interface changes. It also points out
+deprecated interfaces so you should read this even if all of
+your current tests appear to run.
+
+There is a documentation folder which contains the core reference information
+in English and French, although this information is fairly basic.
+You can find a tutorial on...
+
+http://www.lastcraft.com/first_test_tutorial.php
+
+...to get you started and this material will eventually become included
+with the project documentation. A French translation exists at...
+
+http://www.onpk.net/index.php/2005/01/12/254-tutoriel-simpletest-decouvrir-les-tests-unitaires.
+
+If you download and use and possibly even extend this tool, please let us
+know. Any feedback, even bad, is always welcome and we will work to get
+your suggestions into the next release. Ideally please send your
+comments to...
+
+simpletest-support@lists.sourceforge.net
+
+...so that others can read them too. We usually try to respond within 48
+hours.
+
+There is no change log as yet except at Sourceforge. You can visit the
+release notes to see the completed TODO list after each cycle and also the
+status of any bugs, but if the bug is recent then it will be fixed in CVS only.
+The CVS check-ins always have all the tests passing and so CVS snapshots should
+be pretty usable, although the code may not look so good internally.
+
+Oh, yes. It is called "Simple" because it should be simple to
+use. We intend to add a complete set of tools for a test first
+and "test as you code" type of development. "Simple" does not
+mean "Lite" in this context.
+
+Thanks to everyone who has sent comments and offered suggestions. They
+really are invaluable, but sadly you are too many to mention in full.
+Thanks to all on the advanced PHP forum on SitePoint, especially Harry
+Feucks. Early adopters are always an inspiration.
+
+yours Marcus Baker, Jason Sweat, Travis Swicegood and Perrick Penet.
+-- 
+marcus@lastcraft.com
diff --git a/lib/simpletestlib/TODO b/lib/simpletestlib/TODO
new file mode 100644 (file)
index 0000000..5d9dc54
--- /dev/null
@@ -0,0 +1,53 @@
+TODO
+This is immediate stuff only for this iteration (1.0.1alpha3) of 44 hours (60 actual).
+
+$Id$
+
+Unit tester
+-----------
+Change assertCopy() to assertClone() and add identical check (1/1) - done.
+The ignore() directive should ignore parent classes as well (1/3) - done.
+The fail() method should issue a line number (2/1) - done.
+Abstract test cases should be ignored automatically (2/2) - done.
+More meaningful message when trying a test in the constructor (1/1) - done.
+Add exception trap (2/2) - done.
+
+Reporter
+--------
+Allow multiple reporters (2/1) - done.
+Add a test case selecting ReporterDecorator (2/4) - done.
+Can we get rid of that useless runner class? yes! (1/2) - done.
+Test case should say what tests are available (1/1) - done.
+
+Mock objects
+------------
+Confirm mocking of SPL classes and derivatives (3/4) - done.
+Optional parameters generate spurious warnings in PHP 5.0.2 (1/2) - done.
+
+Parser
+------
+
+Browser
+-------
+Allow ignoring of cookies as per feature request (2/8) - done.
+Setting a radio button with value 0 just doesn't happen (1/10) - done.
+Fix quoting issue with fields as in bug report (2/2) - done.
+Fix bug with encoded GET parameters not being transmitted in form action (3/2) - done.
+Confirm that button with no name has no value transmitted (2/4) - done.
+Allow setting of checkboxes by setting them to true (1/1) - done.
+Add memory fixes to builder (1/1) - done.
+
+Web tester
+----------
+Failure to connect should be reported unless suppressed (2/2) - done.
+Message bug in web header pattern assertions (2/1) - done.
+
+Documentation
+-------------
+
+Build
+-----
+Fix fatal error in PHP 5.1 (3/0) - done.
+Fix fatal error in PHP 5.0.5 (3/2) - done.
+Test with PHP 4.4.2 (1/1) - done.
+Fix error in PHP 5.0.2 (2/2) - done.
diff --git a/lib/simpletestlib/VERSION b/lib/simpletestlib/VERSION
new file mode 100644 (file)
index 0000000..9980c7d
--- /dev/null
@@ -0,0 +1 @@
+1.0.1alpha3
\ No newline at end of file
diff --git a/lib/simpletestlib/authentication.php b/lib/simpletestlib/authentication.php
new file mode 100644 (file)
index 0000000..98fe411
--- /dev/null
@@ -0,0 +1,238 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+    /**
+     * include http class
+     */
+    require_once(dirname(__FILE__) . '/http.php');
+    
+    /**
+     *    Represents a single security realm's identity.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleRealm {
+        var $_type;
+        var $_root;
+        var $_username;
+        var $_password;
+        
+        /**
+         *    Starts with the initial entry directory.
+         *    @param string $type      Authentication type for this
+         *                             realm. Only Basic authentication
+         *                             is currently supported.
+         *    @param SimpleUrl $url    Somewhere in realm.
+         *    @access public
+         */
+        function SimpleRealm($type, $url) {
+            $this->_type = $type;
+            $this->_root = $url->getBasePath();
+            $this->_username = false;
+            $this->_password = false;
+        }
+        
+        /**
+         *    Adds another location to the realm.
+         *    @param SimpleUrl $url    Somewhere in realm.
+         *    @access public
+         */
+        function stretch($url) {
+            $this->_root = $this->_getCommonPath($this->_root, $url->getPath());
+        }
+        
+        /**
+         *    Finds the common starting path.
+         *    @param string $first        Path to compare.
+         *    @param string $second       Path to compare.
+         *    @return string              Common directories.
+         *    @access private
+         */
+        function _getCommonPath($first, $second) {
+            $first = explode('/', $first);
+            $second = explode('/', $second);
+            for ($i = 0; $i < min(count($first), count($second)); $i++) {
+                if ($first[$i] != $second[$i]) {
+                    return implode('/', array_slice($first, 0, $i)) . '/';
+                }
+            }
+            return implode('/', $first) . '/';
+        }
+        
+        /**
+         *    Sets the identity to try within this realm.
+         *    @param string $username    Username in authentication dialog.
+         *    @param string $username    Password in authentication dialog.
+         *    @access public
+         */
+        function setIdentity($username, $password) {
+            $this->_username = $username;
+            $this->_password = $password;
+        }
+        
+        /**
+         *    Accessor for current identity.
+         *    @return string        Last succesful username.
+         *    @access public
+         */
+        function getUsername() {
+            return $this->_username;
+        }
+        
+        /**
+         *    Accessor for current identity.
+         *    @return string        Last succesful password.
+         *    @access public
+         */
+        function getPassword() {
+            return $this->_password;
+        }
+        
+        /**
+         *    Test to see if the URL is within the directory
+         *    tree of the realm.
+         *    @param SimpleUrl $url    URL to test.
+         *    @return boolean          True if subpath.
+         *    @access public
+         */
+        function isWithin($url) {
+            if ($this->_isIn($this->_root, $url->getBasePath())) {
+                return true;
+            }
+            if ($this->_isIn($this->_root, $url->getBasePath() . $url->getPage() . '/')) {
+                return true;
+            }
+            return false;
+        }
+        
+        /**
+         *    Tests to see if one string is a substring of
+         *    another.
+         *    @param string $part        Small bit.
+         *    @param string $whole       Big bit.
+         *    @return boolean            True if the small bit is
+         *                               in the big bit.
+         *    @access private
+         */
+        function _isIn($part, $whole) {
+            return strpos($whole, $part) === 0;
+        }
+    }
+    
+    /**
+     *    Manages security realms.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleAuthenticator {
+        var $_realms;
+        
+        /**
+         *    Clears the realms.
+         *    @access public
+         */
+        function SimpleAuthenticator() {
+            $this->restartSession();
+        }
+        
+        /**
+         *    Starts with no realms set up.
+         *    @access public
+         */
+        function restartSession() {
+            $this->_realms = array();
+        }
+        
+        /**
+         *    Adds a new realm centered the current URL.
+         *    Browsers vary wildly on their behaviour in this
+         *    regard. Mozilla ignores the realm and presents
+         *    only when challenged, wasting bandwidth. IE
+         *    just carries on presenting until a new challenge
+         *    occours. SimpleTest tries to follow the spirit of
+         *    the original standards committee and treats the
+         *    base URL as the root of a file tree shaped realm.
+         *    @param SimpleUrl $url    Base of realm.
+         *    @param string $type      Authentication type for this
+         *                             realm. Only Basic authentication
+         *                             is currently supported.
+         *    @param string $realm     Name of realm.
+         *    @access public
+         */
+        function addRealm($url, $type, $realm) {
+            $this->_realms[$url->getHost()][$realm] = new SimpleRealm($type, $url);
+        }
+        
+        /**
+         *    Sets the current identity to be presented
+         *    against that realm.
+         *    @param string $host        Server hosting realm.
+         *    @param string $realm       Name of realm.
+         *    @param string $username    Username for realm.
+         *    @param string $password    Password for realm.
+         *    @access public
+         */
+        function setIdentityForRealm($host, $realm, $username, $password) {
+            if (isset($this->_realms[$host][$realm])) {
+                $this->_realms[$host][$realm]->setIdentity($username, $password);
+            }
+        }
+        
+        /**
+         *    Finds the name of the realm by comparing URLs.
+         *    @param SimpleUrl $url        URL to test.
+         *    @return SimpleRealm          Name of realm.
+         *    @access private
+         */
+        function _findRealmFromUrl($url) {
+            if (! isset($this->_realms[$url->getHost()])) {
+                return false;
+            }
+            foreach ($this->_realms[$url->getHost()] as $name => $realm) {
+                if ($realm->isWithin($url)) {
+                    return $realm;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Presents the appropriate headers for this location.
+         *    @param SimpleHttpRequest $request  Request to modify.
+         *    @param SimpleUrl $url              Base of realm.
+         *    @access public
+         */
+        function addHeaders(&$request, $url) {
+            if ($url->getUsername() && $url->getPassword()) {
+                $username = $url->getUsername();
+                $password = $url->getPassword();
+            } elseif ($realm = $this->_findRealmFromUrl($url)) {
+                $username = $realm->getUsername();
+                $password = $realm->getPassword();
+            } else {
+                return;
+            }
+            $this->addBasicHeaders($request, $username, $password);
+        }
+        
+        /**
+         *    Presents the appropriate headers for this
+         *    location for basic authentication.
+         *    @param SimpleHttpRequest $request  Request to modify.
+         *    @param string $username            Username for realm.
+         *    @param string $password            Password for realm.
+         *    @access public
+         *    @static
+         */
+        function addBasicHeaders(&$request, $username, $password) {
+            if ($username && $password) {
+                $request->addHeaderLine(
+                        'Authorization: Basic ' . base64_encode("$username:$password"));
+            }
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/browser.php b/lib/simpletestlib/browser.php
new file mode 100644 (file)
index 0000000..462262a
--- /dev/null
@@ -0,0 +1,1057 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/simpletest.php');
+    require_once(dirname(__FILE__) . '/http.php');
+    require_once(dirname(__FILE__) . '/encoding.php');
+    require_once(dirname(__FILE__) . '/page.php');
+    require_once(dirname(__FILE__) . '/selector.php');
+    require_once(dirname(__FILE__) . '/frames.php');
+    require_once(dirname(__FILE__) . '/user_agent.php');
+    /**#@-*/
+
+    if (!defined('DEFAULT_MAX_NESTED_FRAMES')) {
+        define('DEFAULT_MAX_NESTED_FRAMES', 3);
+    }
+
+    /**
+     *    Browser history list.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleBrowserHistory {
+        var $_sequence;
+        var $_position;
+
+        /**
+         *    Starts empty.
+         *    @access public
+         */
+        function SimpleBrowserHistory() {
+            $this->_sequence = array();
+            $this->_position = -1;
+        }
+
+        /**
+         *    Test for no entries yet.
+         *    @return boolean        True if empty.
+         *    @access private
+         */
+        function _isEmpty() {
+            return ($this->_position == -1);
+        }
+
+        /**
+         *    Test for being at the beginning.
+         *    @return boolean        True if first.
+         *    @access private
+         */
+        function _atBeginning() {
+            return ($this->_position == 0) && ! $this->_isEmpty();
+        }
+
+        /**
+         *    Test for being at the last entry.
+         *    @return boolean        True if last.
+         *    @access private
+         */
+        function _atEnd() {
+            return ($this->_position + 1 >= count($this->_sequence)) && ! $this->_isEmpty();
+        }
+
+        /**
+         *    Adds a successfully fetched page to the history.
+         *    @param SimpleUrl $url                 URL of fetch.
+         *    @param SimpleEncoding $parameters     Any post data with the fetch.
+         *    @access public
+         */
+        function recordEntry($url, $parameters) {
+            $this->_dropFuture();
+            array_push(
+                    $this->_sequence,
+                    array('url' => $url, 'parameters' => $parameters));
+            $this->_position++;
+        }
+
+        /**
+         *    Last fully qualified URL for current history
+         *    position.
+         *    @return SimpleUrl        URL for this position.
+         *    @access public
+         */
+        function getUrl() {
+            if ($this->_isEmpty()) {
+                return false;
+            }
+            return $this->_sequence[$this->_position]['url'];
+        }
+
+        /**
+         *    Parameters of last fetch from current history
+         *    position.
+         *    @return SimpleFormEncoding    Post parameters.
+         *    @access public
+         */
+        function getParameters() {
+            if ($this->_isEmpty()) {
+                return false;
+            }
+            return $this->_sequence[$this->_position]['parameters'];
+        }
+
+        /**
+         *    Step back one place in the history. Stops at
+         *    the first page.
+         *    @return boolean     True if any previous entries.
+         *    @access public
+         */
+        function back() {
+            if ($this->_isEmpty() || $this->_atBeginning()) {
+                return false;
+            }
+            $this->_position--;
+            return true;
+        }
+
+        /**
+         *    Step forward one place. If already at the
+         *    latest entry then nothing will happen.
+         *    @return boolean     True if any future entries.
+         *    @access public
+         */
+        function forward() {
+            if ($this->_isEmpty() || $this->_atEnd()) {
+                return false;
+            }
+            $this->_position++;
+            return true;
+        }
+
+        /**
+         *    Ditches all future entries beyond the current
+         *    point.
+         *    @access private
+         */
+        function _dropFuture() {
+            if ($this->_isEmpty()) {
+                return;
+            }
+            while (! $this->_atEnd()) {
+                array_pop($this->_sequence);
+            }
+        }
+    }
+
+    /**
+     *    Simulated web browser. This is an aggregate of
+     *    the user agent, the HTML parsing, request history
+     *    and the last header set.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleBrowser {
+        var $_user_agent;
+        var $_page;
+        var $_history;
+        var $_ignore_frames;
+        var $_maximum_nested_frames;
+
+        /**
+         *    Starts with a fresh browser with no
+         *    cookie or any other state information. The
+         *    exception is that a default proxy will be
+         *    set up if specified in the options.
+         *    @access public
+         */
+        function SimpleBrowser() {
+            $this->_user_agent = &$this->_createUserAgent();
+            $this->_user_agent->useProxy(
+                    SimpleTest::getDefaultProxy(),
+                    SimpleTest::getDefaultProxyUsername(),
+                    SimpleTest::getDefaultProxyPassword());
+            $this->_page = &new SimplePage();
+            $this->_history = &$this->_createHistory();
+            $this->_ignore_frames = false;
+            $this->_maximum_nested_frames = DEFAULT_MAX_NESTED_FRAMES;
+        }
+
+        /**
+         *    Creates the underlying user agent.
+         *    @return SimpleFetcher    Content fetcher.
+         *    @access protected
+         */
+        function &_createUserAgent() {
+            $user_agent = &new SimpleUserAgent();
+            return $user_agent;
+        }
+
+        /**
+         *    Creates a new empty history list.
+         *    @return SimpleBrowserHistory    New list.
+         *    @access protected
+         */
+        function &_createHistory() {
+            $history = &new SimpleBrowserHistory();
+            return $history;
+        }
+
+        /**
+         *    Disables frames support. Frames will not be fetched
+         *    and the frameset page will be used instead.
+         *    @access public
+         */
+        function ignoreFrames() {
+            $this->_ignore_frames = true;
+        }
+
+        /**
+         *    Enables frames support. Frames will be fetched from
+         *    now on.
+         *    @access public
+         */
+        function useFrames() {
+            $this->_ignore_frames = false;
+        }
+        
+        /**
+         *    Switches off cookie sending and recieving.
+         *    @access public
+         */
+        function ignoreCookies() {
+            $this->_user_agent->ignoreCookies();
+        }
+        
+        /**
+         *    Switches back on the cookie sending and recieving.
+         *    @access public
+         */
+        function useCookies() {
+            $this->_user_agent->useCookies();
+        }
+
+        /**
+         *    Parses the raw content into a page. Will load further
+         *    frame pages unless frames are disabled.
+         *    @param SimpleHttpResponse $response    Response from fetch.
+         *    @param integer $depth                  Nested frameset depth.
+         *    @return SimplePage                     Parsed HTML.
+         *    @access private
+         */
+        function &_parse($response, $depth = 0) {
+            $page = &$this->_buildPage($response);
+            if ($this->_ignore_frames || ! $page->hasFrames() || ($depth > $this->_maximum_nested_frames)) {
+                return $page;
+            }
+            $frameset = &new SimpleFrameset($page);
+            foreach ($page->getFrameset() as $key => $url) {
+                $frame = &$this->_fetch($url, new SimpleGetEncoding(), $depth + 1);
+                $frameset->addFrame($frame, $key);
+            }
+            return $frameset;
+        }
+        
+        /**
+         *    Assembles the parsing machinery and actually parses
+         *    a single page. Frees all of the builder memory and so
+         *    unjams the PHP memory management.
+         *    @param SimpleHttpResponse $response    Response from fetch.
+         *    @return SimplePage                     Parsed top level page.
+         *    @access protected
+         */
+        function &_buildPage($response) {
+            $builder = &new SimplePageBuilder();
+            $page = &$builder->parse($response);
+            $builder->free();
+            unset($builder);
+            return $page;
+        }
+
+        /**
+         *    Fetches a page. Jointly recursive with the _parse()
+         *    method as it descends a frameset.
+         *    @param string/SimpleUrl $url          Target to fetch.
+         *    @param SimpleEncoding $encoding       GET/POST parameters.
+         *    @param integer $depth                 Nested frameset depth protection.
+         *    @return SimplePage                    Parsed page.
+         *    @access private
+         */
+        function &_fetch($url, $encoding, $depth = 0) {
+            $response = &$this->_user_agent->fetchResponse($url, $encoding);
+            if ($response->isError()) {
+                $page = &new SimplePage($response);
+            } else {
+                $page = &$this->_parse($response, $depth);
+            }
+            return $page;
+        }
+
+        /**
+         *    Fetches a page or a single frame if that is the current
+         *    focus.
+         *    @param SimpleUrl $url                   Target to fetch.
+         *    @param SimpleEncoding $parameters       GET/POST parameters.
+         *    @return string                          Raw content of page.
+         *    @access private
+         */
+        function _load($url, $parameters) {
+            $frame = $url->getTarget();
+            if (! $frame || ! $this->_page->hasFrames() || (strtolower($frame) == '_top')) {
+                return $this->_loadPage($url, $parameters);
+            }
+            return $this->_loadFrame(array($frame), $url, $parameters);
+        }
+
+        /**
+         *    Fetches a page and makes it the current page/frame.
+         *    @param string/SimpleUrl $url            Target to fetch as string.
+         *    @param SimplePostEncoding $parameters   POST parameters.
+         *    @return string                          Raw content of page.
+         *    @access private
+         */
+        function _loadPage($url, $parameters) {
+            $this->_page = &$this->_fetch($url, $parameters);
+            $this->_history->recordEntry(
+                    $this->_page->getUrl(),
+                    $this->_page->getRequestData());
+            return $this->_page->getRaw();
+        }
+
+        /**
+         *    Fetches a frame into the existing frameset replacing the
+         *    original.
+         *    @param array $frames                    List of names to drill down.
+         *    @param string/SimpleUrl $url            Target to fetch as string.
+         *    @param SimpleFormEncoding $parameters   POST parameters.
+         *    @return string                          Raw content of page.
+         *    @access private
+         */
+        function _loadFrame($frames, $url, $parameters) {
+            $page = &$this->_fetch($url, $parameters);
+            $this->_page->setFrame($frames, $page);
+        }
+
+        /**
+         *    Removes expired and temporary cookies as if
+         *    the browser was closed and re-opened.
+         *    @param string/integer $date   Time when session restarted.
+         *                                  If omitted then all persistent
+         *                                  cookies are kept.
+         *    @access public
+         */
+        function restart($date = false) {
+            $this->_user_agent->restart($date);
+        }
+
+        /**
+         *    Adds a header to every fetch.
+         *    @param string $header       Header line to add to every
+         *                                request until cleared.
+         *    @access public
+         */
+        function addHeader($header) {
+            $this->_user_agent->addHeader($header);
+        }
+
+        /**
+         *    Ages the cookies by the specified time.
+         *    @param integer $interval    Amount in seconds.
+         *    @access public
+         */
+        function ageCookies($interval) {
+            $this->_user_agent->ageCookies($interval);
+        }
+
+        /**
+         *    Sets an additional cookie. If a cookie has
+         *    the same name and path it is replaced.
+         *    @param string $name       Cookie key.
+         *    @param string $value      Value of cookie.
+         *    @param string $host       Host upon which the cookie is valid.
+         *    @param string $path       Cookie path if not host wide.
+         *    @param string $expiry     Expiry date.
+         *    @access public
+         */
+        function setCookie($name, $value, $host = false, $path = '/', $expiry = false) {
+            $this->_user_agent->setCookie($name, $value, $host, $path, $expiry);
+        }
+
+        /**
+         *    Reads the most specific cookie value from the
+         *    browser cookies.
+         *    @param string $host        Host to search.
+         *    @param string $path        Applicable path.
+         *    @param string $name        Name of cookie to read.
+         *    @return string             False if not present, else the
+         *                               value as a string.
+         *    @access public
+         */
+        function getCookieValue($host, $path, $name) {
+            return $this->_user_agent->getCookieValue($host, $path, $name);
+        }
+
+        /**
+         *    Reads the current cookies for the current URL.
+         *    @param string $name   Key of cookie to find.
+         *    @return string        Null if there is no current URL, false
+         *                          if the cookie is not set.
+         *    @access public
+         */
+        function getCurrentCookieValue($name) {
+            return $this->_user_agent->getBaseCookieValue($name, $this->_page->getUrl());
+        }
+
+        /**
+         *    Sets the maximum number of redirects before
+         *    a page will be loaded anyway.
+         *    @param integer $max        Most hops allowed.
+         *    @access public
+         */
+        function setMaximumRedirects($max) {
+            $this->_user_agent->setMaximumRedirects($max);
+        }
+
+        /**
+         *    Sets the maximum number of nesting of framed pages
+         *    within a framed page to prevent loops.
+         *    @param integer $max        Highest depth allowed.
+         *    @access public
+         */
+        function setMaximumNestedFrames($max) {
+            $this->_maximum_nested_frames = $max;
+        }
+
+        /**
+         *    Sets the socket timeout for opening a connection.
+         *    @param integer $timeout      Maximum time in seconds.
+         *    @access public
+         */
+        function setConnectionTimeout($timeout) {
+            $this->_user_agent->setConnectionTimeout($timeout);
+        }
+
+        /**
+         *    Sets proxy to use on all requests for when
+         *    testing from behind a firewall. Set URL
+         *    to false to disable.
+         *    @param string $proxy        Proxy URL.
+         *    @param string $username     Proxy username for authentication.
+         *    @param string $password     Proxy password for authentication.
+         *    @access public
+         */
+        function useProxy($proxy, $username = false, $password = false) {
+            $this->_user_agent->useProxy($proxy, $username, $password);
+        }
+
+        /**
+         *    Fetches the page content with a HEAD request.
+         *    Will affect cookies, but will not change the base URL.
+         *    @param string/SimpleUrl $url                Target to fetch as string.
+         *    @param hash/SimpleHeadEncoding $parameters  Additional parameters for
+         *                                                HEAD request.
+         *    @return boolean                             True if successful.
+         *    @access public
+         */
+        function head($url, $parameters = false) {
+            if (! is_object($url)) {
+                $url = new SimpleUrl($url);
+            }
+            if ($this->getUrl()) {
+                $url = $url->makeAbsolute($this->getUrl());
+            }
+            $response = &$this->_user_agent->fetchResponse($url, new SimpleHeadEncoding($parameters));
+            return ! $response->isError();
+        }
+
+        /**
+         *    Fetches the page content with a simple GET request.
+         *    @param string/SimpleUrl $url                Target to fetch.
+         *    @param hash/SimpleFormEncoding $parameters  Additional parameters for
+         *                                                GET request.
+         *    @return string                              Content of page or false.
+         *    @access public
+         */
+        function get($url, $parameters = false) {
+            if (! is_object($url)) {
+                $url = new SimpleUrl($url);
+            }
+            if ($this->getUrl()) {
+                $url = $url->makeAbsolute($this->getUrl());
+            }
+            return $this->_load($url, new SimpleGetEncoding($parameters));
+        }
+
+        /**
+         *    Fetches the page content with a POST request.
+         *    @param string/SimpleUrl $url                Target to fetch as string.
+         *    @param hash/SimpleFormEncoding $parameters  POST parameters.
+         *    @return string                              Content of page.
+         *    @access public
+         */
+        function post($url, $parameters = false) {
+            if (! is_object($url)) {
+                $url = new SimpleUrl($url);
+            }
+            if ($this->getUrl()) {
+                $url = $url->makeAbsolute($this->getUrl());
+            }
+            return $this->_load($url, new SimplePostEncoding($parameters));
+        }
+
+        /**
+         *    Equivalent to hitting the retry button on the
+         *    browser. Will attempt to repeat the page fetch. If
+         *    there is no history to repeat it will give false.
+         *    @return string/boolean   Content if fetch succeeded
+         *                             else false.
+         *    @access public
+         */
+        function retry() {
+            $frames = $this->_page->getFrameFocus();
+            if (count($frames) > 0) {
+                $this->_loadFrame(
+                        $frames,
+                        $this->_page->getUrl(),
+                        $this->_page->getRequestData());
+                return $this->_page->getRaw();
+            }
+            if ($url = $this->_history->getUrl()) {
+                $this->_page = &$this->_fetch($url, $this->_history->getParameters());
+                return $this->_page->getRaw();
+            }
+            return false;
+        }
+
+        /**
+         *    Equivalent to hitting the back button on the
+         *    browser. The browser history is unchanged on
+         *    failure. The page content is refetched as there
+         *    is no concept of content caching in SimpleTest.
+         *    @return boolean     True if history entry and
+         *                        fetch succeeded
+         *    @access public
+         */
+        function back() {
+            if (! $this->_history->back()) {
+                return false;
+            }
+            $content = $this->retry();
+            if (! $content) {
+                $this->_history->forward();
+            }
+            return $content;
+        }
+
+        /**
+         *    Equivalent to hitting the forward button on the
+         *    browser. The browser history is unchanged on
+         *    failure. The page content is refetched as there
+         *    is no concept of content caching in SimpleTest.
+         *    @return boolean     True if history entry and
+         *                        fetch succeeded
+         *    @access public
+         */
+        function forward() {
+            if (! $this->_history->forward()) {
+                return false;
+            }
+            $content = $this->retry();
+            if (! $content) {
+                $this->_history->back();
+            }
+            return $content;
+        }
+
+        /**
+         *    Retries a request after setting the authentication
+         *    for the current realm.
+         *    @param string $username    Username for realm.
+         *    @param string $password    Password for realm.
+         *    @return boolean            True if successful fetch. Note
+         *                               that authentication may still have
+         *                               failed.
+         *    @access public
+         */
+        function authenticate($username, $password) {
+            if (! $this->_page->getRealm()) {
+                return false;
+            }
+            $url = $this->_page->getUrl();
+            if (! $url) {
+                return false;
+            }
+            $this->_user_agent->setIdentity(
+                    $url->getHost(),
+                    $this->_page->getRealm(),
+                    $username,
+                    $password);
+            return $this->retry();
+        }
+
+        /**
+         *    Accessor for a breakdown of the frameset.
+         *    @return array   Hash tree of frames by name
+         *                    or index if no name.
+         *    @access public
+         */
+        function getFrames() {
+            return $this->_page->getFrames();
+        }
+
+        /**
+         *    Accessor for current frame focus. Will be
+         *    false if no frame has focus.
+         *    @return integer/string/boolean    Label if any, otherwise
+         *                                      the position in the frameset
+         *                                      or false if none.
+         *    @access public
+         */
+        function getFrameFocus() {
+            return $this->_page->getFrameFocus();
+        }
+
+        /**
+         *    Sets the focus by index. The integer index starts from 1.
+         *    @param integer $choice    Chosen frame.
+         *    @return boolean           True if frame exists.
+         *    @access public
+         */
+        function setFrameFocusByIndex($choice) {
+            return $this->_page->setFrameFocusByIndex($choice);
+        }
+
+        /**
+         *    Sets the focus by name.
+         *    @param string $name    Chosen frame.
+         *    @return boolean        True if frame exists.
+         *    @access public
+         */
+        function setFrameFocus($name) {
+            return $this->_page->setFrameFocus($name);
+        }
+
+        /**
+         *    Clears the frame focus. All frames will be searched
+         *    for content.
+         *    @access public
+         */
+        function clearFrameFocus() {
+            return $this->_page->clearFrameFocus();
+        }
+
+        /**
+         *    Accessor for last error.
+         *    @return string        Error from last response.
+         *    @access public
+         */
+        function getTransportError() {
+            return $this->_page->getTransportError();
+        }
+
+        /**
+         *    Accessor for current MIME type.
+         *    @return string    MIME type as string; e.g. 'text/html'
+         *    @access public
+         */
+        function getMimeType() {
+            return $this->_page->getMimeType();
+        }
+
+        /**
+         *    Accessor for last response code.
+         *    @return integer    Last HTTP response code received.
+         *    @access public
+         */
+        function getResponseCode() {
+            return $this->_page->getResponseCode();
+        }
+
+        /**
+         *    Accessor for last Authentication type. Only valid
+         *    straight after a challenge (401).
+         *    @return string    Description of challenge type.
+         *    @access public
+         */
+        function getAuthentication() {
+            return $this->_page->getAuthentication();
+        }
+
+        /**
+         *    Accessor for last Authentication realm. Only valid
+         *    straight after a challenge (401).
+         *    @return string    Name of security realm.
+         *    @access public
+         */
+        function getRealm() {
+            return $this->_page->getRealm();
+        }
+
+        /**
+         *    Accessor for current URL of page or frame if
+         *    focused.
+         *    @return string    Location of current page or frame as
+         *                      a string.
+         */
+        function getUrl() {
+            $url = $this->_page->getUrl();
+            return $url ? $url->asString() : false;
+        }
+
+        /**
+         *    Accessor for raw bytes sent down the wire.
+         *    @return string      Original text sent.
+         *    @access public
+         */
+        function getRequest() {
+            return $this->_page->getRequest();
+        }
+
+        /**
+         *    Accessor for raw header information.
+         *    @return string      Header block.
+         *    @access public
+         */
+        function getHeaders() {
+            return $this->_page->getHeaders();
+        }
+
+        /**
+         *    Accessor for raw page information.
+         *    @return string      Original text content of web page.
+         *    @access public
+         */
+        function getContent() {
+            return $this->_page->getRaw();
+        }
+
+        /**
+         *    Accessor for plain text version of the page.
+         *    @return string      Normalised text representation.
+         *    @access public
+         */
+        function getContentAsText() {
+            return $this->_page->getText();
+        }
+
+        /**
+         *    Accessor for parsed title.
+         *    @return string     Title or false if no title is present.
+         *    @access public
+         */
+        function getTitle() {
+            return $this->_page->getTitle();
+        }
+
+        /**
+         *    Accessor for a list of all fixed links in current page.
+         *    @return array   List of urls with scheme of
+         *                    http or https and hostname.
+         *    @access public
+         */
+        function getAbsoluteUrls() {
+            return $this->_page->getAbsoluteUrls();
+        }
+
+        /**
+         *    Accessor for a list of all relative links.
+         *    @return array      List of urls without hostname.
+         *    @access public
+         */
+        function getRelativeUrls() {
+            return $this->_page->getRelativeUrls();
+        }
+
+        /**
+         *    Sets all form fields with that name.
+         *    @param string $label   Name or label of field in forms.
+         *    @param string $value   New value of field.
+         *    @return boolean        True if field exists, otherwise false.
+         *    @access public
+         */
+        function setField($label, $value) {
+            return $this->_page->setField(new SimpleByLabelOrName($label), $value);
+        }
+
+        /**
+         *    Sets all form fields with that name. Will use label if
+         *    one is available (not yet implemented).
+         *    @param string $name    Name of field in forms.
+         *    @param string $value   New value of field.
+         *    @return boolean        True if field exists, otherwise false.
+         *    @access public
+         */
+        function setFieldByName($name, $value) {
+            return $this->_page->setField(new SimpleByName($name), $value);
+        }
+
+        /**
+         *    Sets all form fields with that id attribute.
+         *    @param string/integer $id   Id of field in forms.
+         *    @param string $value        New value of field.
+         *    @return boolean             True if field exists, otherwise false.
+         *    @access public
+         */
+        function setFieldById($id, $value) {
+            return $this->_page->setField(new SimpleById($id), $value);
+        }
+
+        /**
+         *    Accessor for a form element value within the page.
+         *    Finds the first match.
+         *    @param string $label       Field label.
+         *    @return string/boolean     A value if the field is
+         *                               present, false if unchecked
+         *                               and null if missing.
+         *    @access public
+         */
+        function getField($label) {
+            return $this->_page->getField(new SimpleByLabelOrName($label));
+        }
+
+        /**
+         *    Accessor for a form element value within the page.
+         *    Finds the first match.
+         *    @param string $name        Field name.
+         *    @return string/boolean     A string if the field is
+         *                               present, false if unchecked
+         *                               and null if missing.
+         *    @access public
+         */
+        function getFieldByName($name) {
+            return $this->_page->getField(new SimpleByName($name));
+        }
+
+        /**
+         *    Accessor for a form element value within the page.
+         *    @param string/integer $id  Id of field in forms.
+         *    @return string/boolean     A string if the field is
+         *                               present, false if unchecked
+         *                               and null if missing.
+         *    @access public
+         */
+        function getFieldById($id) {
+            return $this->_page->getField(new SimpleById($id));
+        }
+
+        /**
+         *    Clicks the submit button by label. The owning
+         *    form will be submitted by this.
+         *    @param string $label    Button label. An unlabeled
+         *                            button can be triggered by 'Submit'.
+         *    @param hash $additional Additional form data.
+         *    @return string/boolean  Page on success.
+         *    @access public
+         */
+        function clickSubmit($label = 'Submit', $additional = false) {
+            if (! ($form = &$this->_page->getFormBySubmit(new SimpleByLabel($label)))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submitButton(new SimpleByLabel($label), $additional));
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Clicks the submit button by name attribute. The owning
+         *    form will be submitted by this.
+         *    @param string $name     Button name.
+         *    @param hash $additional Additional form data.
+         *    @return string/boolean  Page on success.
+         *    @access public
+         */
+        function clickSubmitByName($name, $additional = false) {
+            if (! ($form = &$this->_page->getFormBySubmit(new SimpleByName($name)))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submitButton(new SimpleByName($name), $additional));
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Clicks the submit button by ID attribute of the button
+         *    itself. The owning form will be submitted by this.
+         *    @param string $id       Button ID.
+         *    @param hash $additional Additional form data.
+         *    @return string/boolean  Page on success.
+         *    @access public
+         */
+        function clickSubmitById($id, $additional = false) {
+            if (! ($form = &$this->_page->getFormBySubmit(new SimpleById($id)))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submitButton(new SimpleById($id), $additional));
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Clicks the submit image by some kind of label. Usually
+         *    the alt tag or the nearest equivalent. The owning
+         *    form will be submitted by this. Clicking outside of
+         *    the boundary of the coordinates will result in
+         *    a failure.
+         *    @param string $label    ID attribute of button.
+         *    @param integer $x       X-coordinate of imaginary click.
+         *    @param integer $y       Y-coordinate of imaginary click.
+         *    @param hash $additional Additional form data.
+         *    @return string/boolean  Page on success.
+         *    @access public
+         */
+        function clickImage($label, $x = 1, $y = 1, $additional = false) {
+            if (! ($form = &$this->_page->getFormByImage(new SimpleByLabel($label)))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submitImage(new SimpleByLabel($label), $x, $y, $additional));
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Clicks the submit image by the name. Usually
+         *    the alt tag or the nearest equivalent. The owning
+         *    form will be submitted by this. Clicking outside of
+         *    the boundary of the coordinates will result in
+         *    a failure.
+         *    @param string $name     Name attribute of button.
+         *    @param integer $x       X-coordinate of imaginary click.
+         *    @param integer $y       Y-coordinate of imaginary click.
+         *    @param hash $additional Additional form data.
+         *    @return string/boolean  Page on success.
+         *    @access public
+         */
+        function clickImageByName($name, $x = 1, $y = 1, $additional = false) {
+            if (! ($form = &$this->_page->getFormByImage(new SimpleByName($name)))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submitImage(new SimpleByName($name), $x, $y, $additional));
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Clicks the submit image by ID attribute. The owning
+         *    form will be submitted by this. Clicking outside of
+         *    the boundary of the coordinates will result in
+         *    a failure.
+         *    @param integer/string $id    ID attribute of button.
+         *    @param integer $x            X-coordinate of imaginary click.
+         *    @param integer $y            Y-coordinate of imaginary click.
+         *    @param hash $additional      Additional form data.
+         *    @return string/boolean       Page on success.
+         *    @access public
+         */
+        function clickImageById($id, $x = 1, $y = 1, $additional = false) {
+            if (! ($form = &$this->_page->getFormByImage(new SimpleById($id)))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submitImage(new SimpleById($id), $x, $y, $additional));
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Submits a form by the ID.
+         *    @param string $id       The form ID. No submit button value
+         *                            will be sent.
+         *    @return string/boolean  Page on success.
+         *    @access public
+         */
+        function submitFormById($id) {
+            if (! ($form = &$this->_page->getFormById($id))) {
+                return false;
+            }
+            $success = $this->_load(
+                    $form->getAction(),
+                    $form->submit());
+            return ($success ? $this->getContent() : $success);
+        }
+
+        /**
+         *    Follows a link by label. Will click the first link
+         *    found with this link text by default, or a later
+         *    one if an index is given. The match ignores case and
+         *    white space issues.
+         *    @param string $label     Text between the anchor tags.
+         *    @param integer $index    Link position counting from zero.
+         *    @return string/boolean   Page on success.
+         *    @access public
+         */
+        function clickLink($label, $index = 0) {
+            $urls = $this->_page->getUrlsByLabel($label);
+            if (count($urls) == 0) {
+                return false;
+            }
+            if (count($urls) < $index + 1) {
+                return false;
+            }
+            $this->_load($urls[$index], new SimpleGetEncoding());
+            return $this->getContent();
+        }
+
+        /**
+         *    Tests to see if a link is present by label.
+         *    @param string $label     Text of value attribute.
+         *    @return boolean          True if link present.
+         *    @access public
+         */
+        function isLink($label) {
+            return (count($this->_page->getUrlsByLabel($label)) > 0);
+        }
+
+        /**
+         *    Follows a link by id attribute.
+         *    @param string $id        ID attribute value.
+         *    @return string/boolean   Page on success.
+         *    @access public
+         */
+        function clickLinkById($id) {
+            if (! ($url = $this->_page->getUrlById($id))) {
+                return false;
+            }
+            $this->_load($url, new SimpleGetEncoding());
+            return $this->getContent();
+        }
+
+        /**
+         *    Tests to see if a link is present by ID attribute.
+         *    @param string $id     Text of id attribute.
+         *    @return boolean       True if link present.
+         *    @access public
+         */
+        function isLinkById($id) {
+            return (boolean)$this->_page->getUrlById($id);
+        }
+
+        /**
+         *    Clicks a visible text item. Will first try buttons,
+         *    then links and then images.
+         *    @param string $label        Visible text or alt text.
+         *    @return string/boolean      Raw page or false.
+         *    @access public
+         */
+        function click($label) {
+            $raw = $this->clickSubmit($label);
+            if (! $raw) {
+                $raw = $this->clickLink($label);
+            }
+            if (! $raw) {
+                $raw = $this->clickImage($label);
+            }
+            return $raw;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/collector.php b/lib/simpletestlib/collector.php
new file mode 100644 (file)
index 0000000..34229d8
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+/**
+ * This file contains the following classes: {@link SimpleCollector}, 
+ * {@link SimplePatternCollector}.
+ * 
+ * @author Travis Swicegood <development@domain51.com>
+ * @package SimpleTest
+ * @subpackage UnitTester
+ * @version $Id$
+ */
+
+/**
+ * The basic collector for {@link GroupTest}
+ *
+ * @see collect(), GroupTest::collect()
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class SimpleCollector {
+    
+    /**
+     * Strips off any kind of slash at the end so as to normalise the path
+     *
+     * @param string $path    Path to normalise.
+     */
+    function _removeTrailingSlash($path) {
+        return preg_replace('|[\\/]$|', '', $path);
+        
+       /**
+        * @internal
+        * Try benchmarking the following.  It's more code, but by not using the
+        * regex, it may be faster?  Also, shouldn't be looking for 
+        * DIRECTORY_SEPERATOR instead of a manual "/"? 
+        */
+        if (substr($path, -1) == DIRECTORY_SEPERATOR) {
+            return substr($path, 0, -1);
+        } else {
+            return $path;
+        }
+    }
+
+    /**
+     * Scans the directory and adds what it can.
+     * @param object $test    Group test with {@link GroupTest::addTestFile()} method.
+     * @param string $path    Directory to scan.
+     * @see _attemptToAdd()
+     */
+    function collect(&$test, $path) {
+        $path = $this->_removeTrailingSlash($path);
+        if ($handle = opendir($path)) {
+            while (($entry = readdir($handle)) !== false) {
+                $this->_handle($test, $path . DIRECTORY_SEPARATOR . $entry);
+            }
+            closedir($handle);
+        }
+    }
+    
+    /**
+     * This method determines what should be done with a given file and adds
+     * it via {@link GroupTest::addTestFile()} if necessary.
+     *
+     * This method should be overriden to provide custom matching criteria, 
+     * such as pattern matching, recursive matching, etc.  For an example, see
+     * {@link SimplePatternCollector::_handle()}.
+     *
+     * @param object $test      Group test with {@link GroupTest::addTestFile()} method.
+     * @param string $filename  A filename as generated by {@link collect()}
+     * @see collect()
+     * @access protected
+     */
+    function _handle(&$test, $file) {
+        if (!is_dir($file)) {
+            $test->addTestFile($file);
+        }
+    }
+}
+
+/**
+ * An extension to {@link SimpleCollector} that only adds files matching a
+ * given pattern.
+ *
+ * @package SimpleTest
+ * @subpackage UnitTester
+ * @see SimpleCollector
+ */
+class SimplePatternCollector extends SimpleCollector {
+    var $_pattern;
+    
+    
+    /**
+     *
+     * @param string $pattern   Perl compatible regex to test name against
+     *  See {@link http://us4.php.net/manual/en/reference.pcre.pattern.syntax.php PHP's PCRE}
+     *  for full documentation of valid pattern.s
+     */
+    function SimplePatternCollector($pattern = '/php$/i') {
+        $this->_pattern = $pattern;
+    }
+    
+    
+    /**
+     * Attempts to add files that match a given pattern.
+     *
+     * @see SimpleCollector::_handle()
+     * @param object $test    Group test with {@link GroupTest::addTestFile()} method.
+     * @param string $path    Directory to scan.
+     * @access protected
+     */
+    function _handle(&$test, $filename) {
+        if (preg_match($this->_pattern, $filename)) {
+            parent::_handle($test, $filename);
+        }
+    }
+}
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/compatibility.php b/lib/simpletestlib/compatibility.php
new file mode 100644 (file)
index 0000000..67b806a
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @version        $Id$
+     */
+    
+    /**
+     *  Static methods for compatibility between different
+     *  PHP versions.
+     *  @package       SimpleTest
+     */
+    class SimpleTestCompatibility {
+       
+       /**
+        *        Creates a copy whether in PHP5 or PHP4.
+        *        @param object $object         Thing to copy.
+        *        @return object                        A copy.
+        *        @access public
+        *        @static
+        */
+       function copy($object) {
+            if (version_compare(phpversion(), '5') >= 0) {
+               eval('$copy = clone $object;');
+               return $copy;
+            }
+            return $object;
+       }
+        
+        /**
+         *    Identity test. Drops back to equality + types for PHP5
+         *    objects as the === operator counts as the
+         *    stronger reference constraint.
+         *    @param mixed $first    Test subject.
+         *    @param mixed $second   Comparison object.
+         *       @return boolean                True if identical.
+         *    @access public
+         *    @static
+         */
+        function isIdentical($first, $second) {
+            if ($first != $second) {
+                return false;
+            }
+            if (version_compare(phpversion(), '5') >= 0) {
+                return SimpleTestCompatibility::_isIdenticalType($first, $second);
+            }
+            return ($first === $second);
+        }
+        
+        /**
+         *    Recursive type test.
+         *    @param mixed $first    Test subject.
+         *    @param mixed $second   Comparison object.
+         *       @return boolean                True if same type.
+         *    @access private
+         *    @static
+         */
+        function _isIdenticalType($first, $second) {
+            if (gettype($first) != gettype($second)) {
+                return false;
+            }
+            if (is_object($first) && is_object($second)) {
+                if (get_class($first) != get_class($second)) {
+                    return false;
+                }
+                return SimpleTestCompatibility::_isArrayOfIdenticalTypes(
+                        get_object_vars($first),
+                        get_object_vars($second));
+            }
+            if (is_array($first) && is_array($second)) {
+                return SimpleTestCompatibility::_isArrayOfIdenticalTypes($first, $second);
+            }
+            return true;
+        }
+        
+        /**
+         *    Recursive type test for each element of an array.
+         *    @param mixed $first    Test subject.
+         *    @param mixed $second   Comparison object.
+         *       @return boolean                True if identical.
+         *    @access private
+         *    @static
+         */
+        function _isArrayOfIdenticalTypes($first, $second) {
+            if (array_keys($first) != array_keys($second)) {
+                return false;
+            }
+            foreach (array_keys($first) as $key) {
+                $is_identical = SimpleTestCompatibility::_isIdenticalType(
+                        $first[$key],
+                        $second[$key]);
+                if (! $is_identical) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        
+        /**
+         *    Test for two variables being aliases.
+         *    @param mixed $first    Test subject.
+         *    @param mixed $second   Comparison object.
+         *       @return boolean                True if same.
+         *    @access public
+         *    @static
+         */
+        function isReference(&$first, &$second) {
+            if (version_compare(phpversion(), '5', '>=')
+                   && is_object($first)) {
+                   return ($first === $second);
+               }
+               if (is_object($first) && is_object($second)) {
+                $id = uniqid("test");
+                $first->$id = true;
+                $is_ref = isset($second->$id);
+                unset($first->$id);
+                return $is_ref;
+               }
+               $temp = $first;
+            $first = uniqid("test");
+            $is_ref = ($first === $second);
+            $first = $temp;
+            return $is_ref;
+        }
+        
+        /**
+         *    Test to see if an object is a member of a
+         *    class hiearchy.
+         *    @param object $object    Object to test.
+         *    @param string $class     Root name of hiearchy.
+         *    @return boolean            True if class in hiearchy.
+         *    @access public
+         *    @static
+         */
+        function isA($object, $class) {
+            if (function_exists('is_a')) {
+                return is_a($object, $class);
+            }
+            if (version_compare(phpversion(), '5') >= 0) {
+                if (! class_exists($class, false)) {
+                    if (function_exists('interface_exists')) {
+                        if (! interface_exists($class, false))  {
+                            return false;
+                        }
+                    }
+                }
+                eval("\$is_a = \$object instanceof $class;");
+                return $is_a;
+            }
+            return ((strtolower($class) == get_class($object))
+                    or (is_subclass_of($object, $class)));
+        }
+        
+        /**
+         *    Sets a socket timeout for each chunk.
+         *    @param resource $handle    Socket handle.
+         *    @param integer $timeout    Limit in seconds.
+         *    @access public
+         *    @static
+         */
+        function setTimeout($handle, $timeout) {
+            if (function_exists('stream_set_timeout')) {
+                stream_set_timeout($handle, $timeout, 0);
+            } elseif (function_exists('socket_set_timeout')) {
+                socket_set_timeout($handle, $timeout, 0);
+            } elseif (function_exists('set_socket_timeout')) {
+                set_socket_timeout($handle, $timeout, 0);
+            }
+        }
+        
+        /**
+         *    Gets the current stack trace topmost first.
+         *    @return array        List of stack frames.
+         *    @access public
+         *    @static
+         */
+        function getStackTrace() {
+            if (function_exists('debug_backtrace')) {
+                return array_reverse(debug_backtrace());
+            }
+            return array();
+        }
+    }
+?>
diff --git a/lib/simpletestlib/cookies.php b/lib/simpletestlib/cookies.php
new file mode 100644 (file)
index 0000000..b346a79
--- /dev/null
@@ -0,0 +1,380 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/url.php');
+    /**#@-*/
+    
+    /**
+     *    Cookie data holder. Cookie rules are full of pretty
+     *    arbitary stuff. I have used...
+     *    http://wp.netscape.com/newsref/std/cookie_spec.html
+     *    http://www.cookiecentral.com/faq/
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleCookie {
+        var $_host;
+        var $_name;
+        var $_value;
+        var $_path;
+        var $_expiry;
+        var $_is_secure;
+        
+        /**
+         *    Constructor. Sets the stored values.
+         *    @param string $name            Cookie key.
+         *    @param string $value           Value of cookie.
+         *    @param string $path            Cookie path if not host wide.
+         *    @param string $expiry          Expiry date as string.
+         *    @param boolean $is_secure      Currently ignored.
+         */
+        function SimpleCookie($name, $value = false, $path = false, $expiry = false, $is_secure = false) {
+            $this->_host = false;
+            $this->_name = $name;
+            $this->_value = $value;
+            $this->_path = ($path ? $this->_fixPath($path) : "/");
+            $this->_expiry = false;
+            if (is_string($expiry)) {
+                $this->_expiry = strtotime($expiry);
+            } elseif (is_integer($expiry)) {
+                $this->_expiry = $expiry;
+            }
+            $this->_is_secure = $is_secure;
+        }
+        
+        /**
+         *    Sets the host. The cookie rules determine
+         *    that the first two parts are taken for
+         *    certain TLDs and three for others. If the
+         *    new host does not match these rules then the
+         *    call will fail.
+         *    @param string $host       New hostname.
+         *    @return boolean           True if hostname is valid.
+         *    @access public
+         */
+        function setHost($host) {
+            if ($host = $this->_truncateHost($host)) {
+                $this->_host = $host;
+                return true;
+            }
+            return false;
+        }
+        
+        /**
+         *    Accessor for the truncated host to which this
+         *    cookie applies.
+         *    @return string       Truncated hostname.
+         *    @access public
+         */
+        function getHost() {
+            return $this->_host;
+        }
+        
+        /**
+         *    Test for a cookie being valid for a host name.
+         *    @param string $host    Host to test against.
+         *    @return boolean        True if the cookie would be valid
+         *                           here.
+         */
+        function isValidHost($host) {
+            return ($this->_truncateHost($host) === $this->getHost());
+        }
+        
+        /**
+         *    Extracts just the domain part that determines a
+         *    cookie's host validity.
+         *    @param string $host    Host name to truncate.
+         *    @return string        Domain or false on a bad host.
+         *    @access private
+         */
+        function _truncateHost($host) {
+            $tlds = SimpleUrl::getAllTopLevelDomains();
+            if (preg_match('/[a-z\-]+\.(' . $tlds . ')$/i', $host, $matches)) {
+                return $matches[0];
+            } elseif (preg_match('/[a-z\-]+\.[a-z\-]+\.[a-z\-]+$/i', $host, $matches)) {
+                return $matches[0];
+            }
+            return false;
+        }
+        
+        /**
+         *    Accessor for name.
+         *    @return string       Cookie key.
+         *    @access public
+         */
+        function getName() {
+            return $this->_name;
+        }
+        
+        /**
+         *    Accessor for value. A deleted cookie will
+         *    have an empty string for this.
+         *    @return string       Cookie value.
+         *    @access public
+         */
+        function getValue() {
+            return $this->_value;
+        }
+        
+        /**
+         *    Accessor for path.
+         *    @return string       Valid cookie path.
+         *    @access public
+         */
+        function getPath() {
+            return $this->_path;
+        }
+        
+        /**
+         *    Tests a path to see if the cookie applies
+         *    there. The test path must be longer or
+         *    equal to the cookie path.
+         *    @param string $path       Path to test against.
+         *    @return boolean           True if cookie valid here.
+         *    @access public
+         */
+        function isValidPath($path) {
+            return (strncmp(
+                    $this->_fixPath($path),
+                    $this->getPath(),
+                    strlen($this->getPath())) == 0);
+        }
+        
+        /**
+         *    Accessor for expiry.
+         *    @return string       Expiry string.
+         *    @access public
+         */
+        function getExpiry() {
+            if (! $this->_expiry) {
+                return false;
+            }
+            return gmdate("D, d M Y H:i:s", $this->_expiry) . " GMT";
+        }
+        
+        /**
+         *    Test to see if cookie is expired against
+         *    the cookie format time or timestamp.
+         *    Will give true for a session cookie.
+         *    @param integer/string $now  Time to test against. Result
+         *                                will be false if this time
+         *                                is later than the cookie expiry.
+         *                                Can be either a timestamp integer
+         *                                or a cookie format date.
+         *    @access public
+         */
+        function isExpired($now) {
+            if (! $this->_expiry) {
+                return true;
+            }
+            if (is_string($now)) {
+                $now = strtotime($now);
+            }
+            return ($this->_expiry < $now);
+        }
+        
+        /**
+         *    Ages the cookie by the specified number of
+         *    seconds.
+         *    @param integer $interval   In seconds.
+         *    @public
+         */
+        function agePrematurely($interval) {
+            if ($this->_expiry) {
+                $this->_expiry -= $interval;
+            }
+        }
+        
+        /**
+         *    Accessor for the secure flag.
+         *    @return boolean       True if cookie needs SSL.
+         *    @access public
+         */
+        function isSecure() {
+            return $this->_is_secure;
+        }
+        
+        /**
+         *    Adds a trailing and leading slash to the path
+         *    if missing.
+         *    @param string $path            Path to fix.
+         *    @access private
+         */
+        function _fixPath($path) {
+            if (substr($path, 0, 1) != '/') {
+                $path = '/' . $path;
+            }
+            if (substr($path, -1, 1) != '/') {
+                $path .= '/';
+            }
+            return $path;
+        }
+    }
+    
+    /**
+     *    Repository for cookies. This stuff is a
+     *    tiny bit browser dependent.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleCookieJar {
+        var $_cookies;
+        
+        /**
+         *    Constructor. Jar starts empty.
+         *    @access public
+         */
+        function SimpleCookieJar() {
+            $this->_cookies = array();
+        }
+        
+        /**
+         *    Removes expired and temporary cookies as if
+         *    the browser was closed and re-opened.
+         *    @param string/integer $now   Time to test expiry against.
+         *    @access public
+         */
+        function restartSession($date = false) {
+            $surviving_cookies = array();
+            for ($i = 0; $i < count($this->_cookies); $i++) {
+                if (! $this->_cookies[$i]->getValue()) {
+                    continue;
+                }
+                if (! $this->_cookies[$i]->getExpiry()) {
+                    continue;
+                }
+                if ($date && $this->_cookies[$i]->isExpired($date)) {
+                    continue;
+                }
+                $surviving_cookies[] = $this->_cookies[$i];
+            }
+            $this->_cookies = $surviving_cookies;
+        }
+        
+        /**
+         *    Ages all cookies in the cookie jar.
+         *    @param integer $interval     The old session is moved
+         *                                 into the past by this number
+         *                                 of seconds. Cookies now over
+         *                                 age will be removed.
+         *    @access public
+         */
+        function agePrematurely($interval) {
+            for ($i = 0; $i < count($this->_cookies); $i++) {
+                $this->_cookies[$i]->agePrematurely($interval);
+            }
+        }
+        
+        /**
+         *    Sets an additional cookie. If a cookie has
+         *    the same name and path it is replaced.
+         *    @param string $name       Cookie key.
+         *    @param string $value      Value of cookie.
+         *    @param string $host       Host upon which the cookie is valid.
+         *    @param string $path       Cookie path if not host wide.
+         *    @param string $expiry     Expiry date.
+         *    @access public
+         */
+        function setCookie($name, $value, $host = false, $path = '/', $expiry = false) {
+            $cookie = new SimpleCookie($name, $value, $path, $expiry);
+            if ($host) {
+                $cookie->setHost($host);
+            }
+            $this->_cookies[$this->_findFirstMatch($cookie)] = $cookie;
+        }
+        
+        /**
+         *    Finds a matching cookie to write over or the
+         *    first empty slot if none.
+         *    @param SimpleCookie $cookie    Cookie to write into jar.
+         *    @return integer                Available slot.
+         *    @access private
+         */
+        function _findFirstMatch($cookie) {
+            for ($i = 0; $i < count($this->_cookies); $i++) {
+                $is_match = $this->_isMatch(
+                        $cookie,
+                        $this->_cookies[$i]->getHost(),
+                        $this->_cookies[$i]->getPath(),
+                        $this->_cookies[$i]->getName());
+                if ($is_match) {
+                    return $i;
+                }
+            }
+            return count($this->_cookies);
+        }
+        
+        /**
+         *    Reads the most specific cookie value from the
+         *    browser cookies. Looks for the longest path that
+         *    matches.
+         *    @param string $host        Host to search.
+         *    @param string $path        Applicable path.
+         *    @param string $name        Name of cookie to read.
+         *    @return string             False if not present, else the
+         *                               value as a string.
+         *    @access public
+         */
+        function getCookieValue($host, $path, $name) {
+            $longest_path = '';
+            foreach ($this->_cookies as $cookie) {
+                if ($this->_isMatch($cookie, $host, $path, $name)) {
+                    if (strlen($cookie->getPath()) > strlen($longest_path)) {
+                        $value = $cookie->getValue();
+                        $longest_path = $cookie->getPath();
+                    }
+                }
+            }
+            return (isset($value) ? $value : false);
+        }
+        
+        /**
+         *    Tests cookie for matching against search
+         *    criteria.
+         *    @param SimpleTest $cookie    Cookie to test.
+         *    @param string $host          Host must match.
+         *    @param string $path          Cookie path must be shorter than
+         *                                 this path.
+         *    @param string $name          Name must match.
+         *    @return boolean              True if matched.
+         *    @access private
+         */
+        function _isMatch($cookie, $host, $path, $name) {
+            if ($cookie->getName() != $name) {
+                return false;
+            }
+            if ($host && $cookie->getHost() && ! $cookie->isValidHost($host)) {
+                return false;
+            }
+            if (! $cookie->isValidPath($path)) {
+                return false;
+            }
+            return true;
+        }
+        
+        /**
+         *    Uses a URL to sift relevant cookies by host and
+         *    path. Results are list of strings of form "name=value".
+         *    @param SimpleUrl $url       Url to select by.
+         *    @return array               Valid name and value pairs.
+         *    @access public
+         */
+        function selectAsPairs($url) {
+            $pairs = array();
+            foreach ($this->_cookies as $cookie) {
+                if ($this->_isMatch($cookie, $url->getHost(), $url->getPath(), $cookie->getName())) {
+                    $pairs[] = $cookie->getName() . '=' . $cookie->getValue();
+                }
+            }
+            return $pairs;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/detached.php b/lib/simpletestlib/detached.php
new file mode 100644 (file)
index 0000000..35ac133
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/xml.php');
+    require_once(dirname(__FILE__) . '/shell_tester.php');
+    /**#@-*/
+
+    /**
+     *    Runs an XML formated test in a separate process.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class DetachedTestCase {
+        var $_command;
+        var $_dry_command;
+        var $_size;
+
+        /**
+         *    Sets the location of the remote test.
+         *    @param string $command       Test script.
+         *    @param string $dry_command   Script for dry run.
+         *    @access public
+         */
+        function DetachedTestCase($command, $dry_command = false) {
+            $this->_command = $command;
+            $this->_dry_command = $dry_command ? $dry_command : $command;
+            $this->_size = false;
+        }
+
+        /**
+         *    Accessor for the test name for subclasses.
+         *    @return string       Name of the test.
+         *    @access public
+         */
+        function getLabel() {
+            return $this->_command;
+        }
+
+        /**
+         *    Runs the top level test for this class. Currently
+         *    reads the data as a single chunk. I'll fix this
+         *    once I have added iteration to the browser.
+         *    @param SimpleReporter $reporter    Target of test results.
+         *    @returns boolean                   True if no failures.
+         *    @access public
+         */
+        function run(&$reporter) {
+                       $shell = &new SimpleShell();
+                       $shell->execute($this->_command);
+            $parser = &$this->_createParser($reporter);
+            if (! $parser->parse($shell->getOutput())) {
+                trigger_error('Cannot parse incoming XML from [' . $this->_command . ']');
+                return false;
+            }
+            return true;
+        }
+
+        /**
+         *    Accessor for the number of subtests.
+         *    @return integer       Number of test cases.
+         *    @access public
+         */
+        function getSize() {
+            if ($this->_size === false) {
+                               $shell = &new SimpleShell();
+                               $shell->execute($this->_dry_command);
+                $reporter = &new SimpleReporter();
+                $parser = &$this->_createParser($reporter);
+                if (! $parser->parse($shell->getOutput())) {
+                    trigger_error('Cannot parse incoming XML from [' . $this->_dry_command . ']');
+                    return false;
+                }
+                $this->_size = $reporter->getTestCaseCount();
+            }
+            return $this->_size;
+        }
+
+        /**
+         *    Creates the XML parser.
+         *    @param SimpleReporter $reporter    Target of test results.
+         *    @return SimpleTestXmlListener      XML reader.
+         *    @access protected
+         */
+        function &_createParser(&$reporter) {
+            return new SimpleTestXmlParser($reporter);
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/dumper.php b/lib/simpletestlib/dumper.php
new file mode 100644 (file)
index 0000000..63af5ea
--- /dev/null
@@ -0,0 +1,402 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+    /**
+     * does type matter
+     */
+    if (! defined('TYPE_MATTERS')) {
+        define('TYPE_MATTERS', true);
+    }
+    
+    /**
+     *    Displays variables as text and does diffs.
+        *        @package      SimpleTest
+        *        @subpackage   UnitTester
+     */
+    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 "Boolean":
+                    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";
+            }
+            return "Unknown";
+        }
+        
+        /**
+         *    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";
+            } elseif (is_bool($value)) {
+                return "Boolean";
+            } elseif (is_string($value)) {
+                return "String";
+            } elseif (is_integer($value)) {
+                return "Integer";
+            } elseif (is_float($value)) {
+                return "Float";
+            } elseif (is_array($value)) {
+                return "Array";
+            } elseif (is_resource($value)) {
+                return "Resource";
+            } elseif (is_object($value)) {
+                return "Object";
+            }
+            return "Unknown";
+        }
+
+        /**
+         *    Creates a human readable description of the
+         *    difference between two variables. Uses a
+         *    dynamic call.
+         *    @param mixed $first        First variable.
+         *    @param mixed $second       Value to compare with.
+         *    @param boolean $identical  If true then type anomolies count.
+         *    @return string             Description of difference.
+         *    @access public
+         */
+        function describeDifference($first, $second, $identical = false) {
+            if ($identical) {
+                if (! $this->_isTypeMatch($first, $second)) {
+                    return "with type mismatch as [" . $this->describeValue($first) .
+                        "] does not match [" . $this->describeValue($second) . "]";
+                }
+            }
+            $type = $this->getType($first);
+            if ($type == "Unknown") {
+                return "with unknown type";
+            }
+            $method = '_describe' . $type . 'Difference';
+            return $this->$method($first, $second, $identical);
+        }
+        
+        /**
+         *    Tests to see if types match.
+         *    @param mixed $first        First variable.
+         *    @param mixed $second       Value to compare with.
+         *    @return boolean            True if matches.
+         *    @access private
+         */
+        function _isTypeMatch($first, $second) {
+            return ($this->getType($first) == $this->getType($second));
+        }
+
+        /**
+         *    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 ? "..." : "");
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between two variables. The minimal
+         *    version.
+         *    @param null $first          First value.
+         *    @param mixed $second        Value to compare with.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeGenericDifference($first, $second) {
+            return "as [" . $this->describeValue($first) .
+                    "] does not match [" .
+                    $this->describeValue($second) . "]";
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between a null and another variable.
+         *    @param null $first          First null.
+         *    @param mixed $second        Null to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeNullDifference($first, $second, $identical) {
+            return $this->_describeGenericDifference($first, $second);
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between a boolean and another variable.
+         *    @param boolean $first       First boolean.
+         *    @param mixed $second        Boolean to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeBooleanDifference($first, $second, $identical) {
+            return $this->_describeGenericDifference($first, $second);
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between a string and another variable.
+         *    @param string $first        First string.
+         *    @param mixed $second        String to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeStringDifference($first, $second, $identical) {
+            if (is_object($second) || is_array($second)) {
+                return $this->_describeGenericDifference($first, $second);
+            }
+            $position = $this->_stringDiffersAt($first, $second);
+            $message = "at character $position";
+            $message .= " with [" .
+                    $this->clipString($first, 200, $position) . "] and [" .
+                    $this->clipString($second, 200, $position) . "]";
+            return $message;
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between an integer and another variable.
+         *    @param integer $first       First number.
+         *    @param mixed $second        Number to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeIntegerDifference($first, $second, $identical) {
+            if (is_object($second) || is_array($second)) {
+                return $this->_describeGenericDifference($first, $second);
+            }
+            return "because [" . $this->describeValue($first) .
+                    "] differs from [" .
+                    $this->describeValue($second) . "] by " .
+                    abs($first - $second);
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between two floating point numbers.
+         *    @param float $first         First float.
+         *    @param mixed $second        Float to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeFloatDifference($first, $second, $identical) {
+            if (is_object($second) || is_array($second)) {
+                return $this->_describeGenericDifference($first, $second);
+            }
+            return "because [" . $this->describeValue($first) .
+                    "] differs from [" .
+                    $this->describeValue($second) . "] by " .
+                    abs($first - $second);
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between two arrays.
+         *    @param array $first         First array.
+         *    @param mixed $second        Array to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeArrayDifference($first, $second, $identical) {
+            if (! is_array($second)) {
+                return $this->_describeGenericDifference($first, $second);
+            }
+            if (! $this->_isMatchingKeys($first, $second, $identical)) {
+                return "as key list [" .
+                        implode(", ", array_keys($first)) . "] does not match key list [" .
+                        implode(", ", array_keys($second)) . "]";
+            }
+            foreach (array_keys($first) as $key) {
+                if ($identical && ($first[$key] === $second[$key])) {
+                    continue;
+                }
+                if (! $identical && ($first[$key] == $second[$key])) {
+                    continue;
+                }
+                return "with member [$key] " . $this->describeDifference(
+                        $first[$key],
+                        $second[$key],
+                        $identical);
+            }
+            return "";
+        }
+        
+        /**
+         *    Compares two arrays to see if their key lists match.
+         *    For an identical match, the ordering and types of the keys
+         *    is significant.
+         *    @param array $first         First array.
+         *    @param array $second        Array to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return boolean             True if matching.
+         *    @access private
+         */
+        function _isMatchingKeys($first, $second, $identical) {
+            $first_keys = array_keys($first);
+            $second_keys = array_keys($second);
+            if ($identical) {
+                return ($first_keys === $second_keys);
+            }
+            sort($first_keys);
+            sort($second_keys);
+            return ($first_keys == $second_keys);
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between a resource and another variable.
+         *    @param resource $first       First resource.
+         *    @param mixed $second         Resource to compare with.
+         *    @param boolean $identical    If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeResourceDifference($first, $second, $identical) {
+            return $this->_describeGenericDifference($first, $second);
+        }
+        
+        /**
+         *    Creates a human readable description of the
+         *    difference between two objects.
+         *    @param object $first        First object.
+         *    @param mixed $second        Object to compare with.
+         *    @param boolean $identical   If true then type anomolies count.
+         *    @return string              Human readable description.
+         *    @access private
+         */
+        function _describeObjectDifference($first, $second, $identical) {
+            if (! is_object($second)) {
+                return $this->_describeGenericDifference($first, $second);
+            }
+            return $this->_describeArrayDifference(
+                    get_object_vars($first),
+                    get_object_vars($second),
+                    $identical);
+        }
+        
+        /**
+         *    Find the first character position that differs
+         *    in two strings by binary chop.
+         *    @param string $first        First string.
+         *    @param string $second       String to compare with.
+         *    @return integer             Position of first differing
+         *                                character.
+         *    @access private
+         */
+        function _stringDiffersAt($first, $second) {
+            if (! $first || ! $second) {
+                return 0;
+            }
+            if (strlen($first) < strlen($second)) {
+                list($first, $second) = array($second, $first);
+            }
+            $position = 0;
+            $step = strlen($first);
+            while ($step > 1) {
+                $step = (integer)(($step + 1) / 2);
+                if (strncmp($first, $second, $position + $step) == 0) {
+                    $position += $step;
+                }
+            }
+            return $position;
+        }
+        
+        /**
+         *    Sends a formatted dump of a variable to a string.
+         *    @param mixed $variable    Variable to display.
+         *    @return string            Output from print_r().
+         *    @access public
+         *    @static
+         */
+        function dump($variable) {
+            ob_start();
+            print_r($variable);
+            $formatted = ob_get_contents();
+            ob_end_clean();
+            return $formatted;
+        }
+
+        /**
+         *    Extracts the last assertion that was not within
+         *    Simpletest itself. The name must start with "assert".
+         *    @param array $stack      List of stack frames.
+         *    @access public
+         *    @static
+         */
+        function getFormattedAssertionLine($stack) {
+            foreach ($stack as $frame) {
+                if (isset($frame['file'])) {
+                    if (strpos($frame['file'], SIMPLE_TEST) !== false) {
+                        if (dirname($frame['file']) . '/' == SIMPLE_TEST) {
+                            continue;
+                        }
+                    }
+                }
+                if (SimpleDumper::_stackFrameIsAnAssertion($frame)) {
+                    return ' at [' . $frame['file'] . ' line ' . $frame['line'] . ']';
+                }
+            }
+            return '';
+        }
+        
+        /**
+         *    Tries to determine if the method call is an assertion.
+         *    @param array $frame     PHP stack frame.
+         *    @access private
+         *    @static
+         */
+        function _stackFrameIsAnAssertion($frame) {
+            if (($frame['function'] == 'fail') || ($frame['function'] == 'pass')) {
+                return true;
+            }
+            if (strncmp($frame['function'], 'assert', 6) == 0) {
+                return true;
+            }
+            if (strncmp($frame['function'], 'expect', 6) == 0) {
+                return true;
+            }
+            return false;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/eclipse.php b/lib/simpletestlib/eclipse.php
new file mode 100644 (file)
index 0000000..abd7251
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/**
+*      base include file for eclipse plugin 
+*      @package        SimpleTest
+*      @version        $Id$
+*/
+include_once "xml.php";
+include_once "invoker.php";
+include_once "socket.php";
+class EclipseReporter extends XmlReporter {
+       var $_port;
+       function EclipseReporter($port){
+               $this->_port = $port;
+               $this->XmlReporter();
+       }
+       
+       function &createInvoker(&$invoker){
+               $eclinvoker = &new EclipseInvoker($invoker, $this->_port);
+               return $eclinvoker;
+       }
+       
+       function paintMethodStart($method) {
+               parent::paintGroupStart($this->_group, $this->_size);
+               parent::paintCaseStart($this->_case);
+               parent::paintMethodStart($method);
+       }
+       
+       function paintMethodEnd($method){
+               parent::paintMethodEnd($method);
+               parent::paintCaseEnd($this->_case);
+               parent::paintGroupEnd($this->_group);
+               
+       }
+       
+       function paintCaseStart($case){
+               $this->_case = $case;
+       }
+       
+       function paintCaseEnd($case){
+               $this->_case = "";
+       }
+       function paintGroupStart($group,$size){
+               $this->_group = $group;
+       }
+       function paintGroupEnd($group){
+               $this->_group = "";
+       }
+}
+
+class EclipseInvoker extends SimpleInvokerDecorator{
+       var $_port;
+       function EclipseInvoker(&$invoker,$port) {
+               $this->_port = $port;
+               $this->SimpleInvokerDecorator($invoker);
+       }
+       
+       function invoke($method) {
+               ob_start();
+               parent::invoke($method);
+               $output = ob_get_contents();
+               ob_end_clean();
+
+               $sock = new SimpleSocket("127.0.0.1",$this->_port,5);
+               $sock->write($output);
+               $sock->close();
+               echo $sock->getError();
+       }
+}
+       
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/encoding.php b/lib/simpletestlib/encoding.php
new file mode 100644 (file)
index 0000000..a1a0d76
--- /dev/null
@@ -0,0 +1,521 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+     
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/socket.php');
+    /**#@-*/
+
+    /**
+     *    Single post parameter.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleEncodedPair {
+        var $_key;
+        var $_value;
+        
+        /**
+         *    Stashes the data for rendering later.
+         *    @param string $key       Form element name.
+         *    @param string $value     Data to send.
+         */
+        function SimpleEncodedPair($key, $value) {
+            $this->_key = $key;
+            $this->_value = $value;
+        }
+        
+        /**
+         *    The pair as a single string.
+         *    @return string        Encoded pair.
+         *    @access public
+         */
+        function asRequest() {
+            return $this->_key . '=' . urlencode($this->_value);
+        }
+        
+        /**
+         *    The MIME part as a string.
+         *    @return string        MIME part encoding.
+         *    @access public
+         */
+        function asMime() {
+            $part = 'Content-Disposition: form-data; ';
+            $part .= "name=\"" . $this->_key . "\"\r\n";
+            $part .= "\r\n" . $this->_value;
+            return $part;
+        }
+        
+        /**
+         *    Is this the value we are looking for?
+         *    @param string $key    Identifier.
+         *    @return boolean       True if matched.
+         *    @access public
+         */
+        function isKey($key) {
+            return $key == $this->_key;
+        }
+        
+        /**
+         *    Is this the value we are looking for?
+         *    @return string       Identifier.
+         *    @access public
+         */
+        function getKey() {
+            return $this->_key;
+        }
+        
+        /**
+         *    Is this the value we are looking for?
+         *    @return string       Content.
+         *    @access public
+         */
+        function getValue() {
+            return $this->_value;
+        }
+    }
+
+    /**
+     *    Single post parameter.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleAttachment {
+        var $_key;
+        var $_content;
+        var $_filename;
+        
+        /**
+         *    Stashes the data for rendering later.
+         *    @param string $key          Key to add value to.
+         *    @param string $content      Raw data.
+         *    @param hash $filename       Original filename.
+         */
+        function SimpleAttachment($key, $content, $filename) {
+            $this->_key = $key;
+            $this->_content = $content;
+            $this->_filename = $filename;
+        }
+        
+        /**
+         *    The pair as a single string.
+         *    @return string        Encoded pair.
+         *    @access public
+         */
+        function asRequest() {
+            return '';
+        }
+        
+        /**
+         *    The MIME part as a string.
+         *    @return string        MIME part encoding.
+         *    @access public
+         */
+        function asMime() {
+            $part = 'Content-Disposition: form-data; ';
+            $part .= 'name="' . $this->_key . '"; ';
+            $part .= 'filename="' . $this->_filename . '"';
+            $part .= "\r\nContent-Type: " . $this->_deduceMimeType();
+            $part .= "\r\n\r\n" . $this->_content;
+            return $part;
+        }
+        
+        /**
+         *    Attempts to figure out the MIME type from the
+         *    file extension and the content.
+         *    @return string        MIME type.
+         *    @access private
+         */
+        function _deduceMimeType() {
+            if ($this->_isOnlyAscii($this->_content)) {
+                return 'text/plain';
+            }
+            return 'application/octet-stream';
+        }
+        
+        /**
+         *    Tests each character is in the range 0-127.
+         *    @param string $ascii    String to test.
+         *    @access private
+         */
+        function _isOnlyAscii($ascii) {
+            for ($i = 0, $length = strlen($ascii); $i < $length; $i++) {
+                if (ord($ascii[$i]) > 127) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        
+        /**
+         *    Is this the value we are looking for?
+         *    @param string $key    Identifier.
+         *    @return boolean       True if matched.
+         *    @access public
+         */
+        function isKey($key) {
+            return $key == $this->_key;
+        }
+        
+        /**
+         *    Is this the value we are looking for?
+         *    @return string       Identifier.
+         *    @access public
+         */
+        function getKey() {
+            return $this->_key;
+        }
+        
+        /**
+         *    Is this the value we are looking for?
+         *    @return string       Content.
+         *    @access public
+         */
+        function getValue() {
+            return $this->_filename;
+        }
+    }
+
+    /**
+     *    Bundle of GET/POST parameters. Can include
+     *    repeated parameters.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleEncoding {
+        var $_request;
+        
+        /**
+         *    Starts empty.
+         *    @param array $query       Hash of parameters.
+         *                              Multiple values are
+         *                              as lists on a single key.
+         *    @access public
+         */
+        function SimpleEncoding($query = false) {
+            if (! $query) {
+                $query = array();
+            }
+            $this->clear();
+            $this->merge($query);
+        }
+        
+        /**
+         *    Empties the request of parameters.
+         *    @access public
+         */
+        function clear() {
+            $this->_request = array();
+        }
+        
+        /**
+         *    Adds a parameter to the query.
+         *    @param string $key            Key to add value to.
+         *    @param string/array $value    New data.
+         *    @access public
+         */
+        function add($key, $value) {
+            if ($value === false) {
+                return;
+            }
+            if (is_array($value)) {
+                foreach ($value as $item) {
+                    $this->_addPair($key, $item);
+                }
+            } else {
+                $this->_addPair($key, $value);
+            }
+        }
+        
+        /**
+         *    Adds a new value into the request.
+         *    @param string $key            Key to add value to.
+         *    @param string/array $value    New data.
+         *    @access private
+         */
+        function _addPair($key, $value) {
+            $this->_request[] = new SimpleEncodedPair($key, $value);
+        }
+        
+        /**
+         *    Adds a MIME part to the query. Does nothing for a
+         *    form encoded packet.
+         *    @param string $key          Key to add value to.
+         *    @param string $content      Raw data.
+         *    @param hash $filename       Original filename.
+         *    @access public
+         */
+        function attach($key, $content, $filename) {
+            $this->_request[] = new SimpleAttachment($key, $content, $filename);
+        }
+        
+        /**
+         *    Adds a set of parameters to this query.
+         *    @param array/SimpleQueryString $query  Multiple values are
+         *                                           as lists on a single key.
+         *    @access public
+         */
+        function merge($query) {
+            if (is_object($query)) {
+                $this->_request = array_merge($this->_request, $query->getAll());
+            } elseif (is_array($query)) {
+                foreach ($query as $key => $value) {
+                    $this->add($key, $value);
+                }
+            }
+        }
+        
+        /**
+         *    Accessor for single value.
+         *    @return string/array    False if missing, string
+         *                            if present and array if
+         *                            multiple entries.
+         *    @access public
+         */
+        function getValue($key) {
+            $values = array();
+            foreach ($this->_request as $pair) {
+                if ($pair->isKey($key)) {
+                    $values[] = $pair->getValue();
+                }
+            }
+            if (count($values) == 0) {
+                return false;
+            } elseif (count($values) == 1) {
+                return $values[0];
+            } else {
+                return $values;
+            }
+        }
+        
+        /**
+         *    Accessor for listing of pairs.
+         *    @return array        All pair objects.
+         *    @access public
+         */
+        function getAll() {
+            return $this->_request;
+        }
+        
+        /**
+         *    Renders the query string as a URL encoded
+         *    request part.
+         *    @return string        Part of URL.
+         *    @access protected
+         */
+        function _encode() {
+            $statements = array();
+            foreach ($this->_request as $pair) {
+                if ($statement = $pair->asRequest()) {
+                    $statements[] = $statement;
+                }
+            }
+            return implode('&', $statements);
+        }
+    }
+    
+    /**
+     *    Bundle of GET parameters. Can include
+     *    repeated parameters.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleGetEncoding extends SimpleEncoding {
+        
+        /**
+         *    Starts empty.
+         *    @param array $query       Hash of parameters.
+         *                              Multiple values are
+         *                              as lists on a single key.
+         *    @access public
+         */
+        function SimpleGetEncoding($query = false) {
+            $this->SimpleEncoding($query);
+        }
+        
+        /**
+         *    HTTP request method.
+         *    @return string        Always GET.
+         *    @access public
+         */
+        function getMethod() {
+            return 'GET';
+        }
+        
+        /**
+         *    Writes no extra headers.
+         *    @param SimpleSocket $socket        Socket to write to.
+         *    @access public
+         */
+        function writeHeadersTo(&$socket) {
+        }
+        
+        /**
+         *    No data is sent to the socket as the data is encoded into
+         *    the URL.
+         *    @param SimpleSocket $socket        Socket to write to.
+         *    @access public
+         */
+        function writeTo(&$socket) {
+        }
+        
+        /**
+         *    Renders the query string as a URL encoded
+         *    request part for attaching to a URL.
+         *    @return string        Part of URL.
+         *    @access public
+         */
+        function asUrlRequest() {
+            return $this->_encode();
+        }
+    }
+    
+    /**
+     *    Bundle of URL parameters for a HEAD request.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleHeadEncoding extends SimpleGetEncoding {
+        
+        /**
+         *    Starts empty.
+         *    @param array $query       Hash of parameters.
+         *                              Multiple values are
+         *                              as lists on a single key.
+         *    @access public
+         */
+        function SimpleHeadEncoding($query = false) {
+            $this->SimpleGetEncoding($query);
+        }
+        
+        /**
+         *    HTTP request method.
+         *    @return string        Always HEAD.
+         *    @access public
+         */
+        function getMethod() {
+            return 'HEAD';
+        }
+    }
+    
+    /**
+     *    Bundle of POST parameters. Can include
+     *    repeated parameters.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimplePostEncoding extends SimpleEncoding {
+        
+        /**
+         *    Starts empty.
+         *    @param array $query       Hash of parameters.
+         *                              Multiple values are
+         *                              as lists on a single key.
+         *    @access public
+         */
+        function SimplePostEncoding($query = false) {
+            $this->SimpleEncoding($query);
+        }
+        
+        /**
+         *    HTTP request method.
+         *    @return string        Always POST.
+         *    @access public
+         */
+        function getMethod() {
+            return 'POST';
+        }
+        
+        /**
+         *    Dispatches the form headers down the socket.
+         *    @param SimpleSocket $socket        Socket to write to.
+         *    @access public
+         */
+        function writeHeadersTo(&$socket) {
+            $socket->write("Content-Length: " . (integer)strlen($this->_encode()) . "\r\n");
+            $socket->write("Content-Type: application/x-www-form-urlencoded\r\n");
+        }
+        
+        /**
+         *    Dispatches the form data down the socket.
+         *    @param SimpleSocket $socket        Socket to write to.
+         *    @access public
+         */
+        function writeTo(&$socket) {
+            $socket->write($this->_encode());
+        }
+        
+        /**
+         *    Renders the query string as a URL encoded
+         *    request part for attaching to a URL.
+         *    @return string        Part of URL.
+         *    @access public
+         */
+        function asUrlRequest() {
+            return '';
+        }
+    }
+    
+    /**
+     *    Bundle of POST parameters in the multipart
+     *    format. Can include file uploads.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleMultipartEncoding extends SimplePostEncoding {
+        var $_boundary;
+        
+        /**
+         *    Starts empty.
+         *    @param array $query       Hash of parameters.
+         *                              Multiple values are
+         *                              as lists on a single key.
+         *    @access public
+         */
+        function SimpleMultipartEncoding($query = false, $boundary = false) {
+            $this->SimplePostEncoding($query);
+            $this->_boundary = ($boundary === false ? uniqid('st') : $boundary);
+        }
+        
+        /**
+         *    Dispatches the form headers down the socket.
+         *    @param SimpleSocket $socket        Socket to write to.
+         *    @access public
+         */
+        function writeHeadersTo(&$socket) {
+            $socket->write("Content-Length: " . (integer)strlen($this->_encode()) . "\r\n");
+            $socket->write("Content-Type: multipart/form-data, boundary=" . $this->_boundary . "\r\n");
+        }
+        
+        /**
+         *    Dispatches the form data down the socket.
+         *    @param SimpleSocket $socket        Socket to write to.
+         *    @access public
+         */
+        function writeTo(&$socket) {
+            $socket->write($this->_encode());
+        }
+        
+        /**
+         *    Renders the query string as a URL encoded
+         *    request part.
+         *    @return string        Part of URL.
+         *    @access public
+         */
+        function _encode() {
+            $stream = '';
+            foreach ($this->_request as $pair) {
+                $stream .= "--" . $this->_boundary . "\r\n";
+                $stream .= $pair->asMime() . "\r\n";
+            }
+            $stream .= "--" . $this->_boundary . "--\r\n";
+            return $stream;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/errors.php b/lib/simpletestlib/errors.php
new file mode 100644 (file)
index 0000000..99b5096
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /** @ignore - PHP5 compatibility fix. */
+    if (! defined('E_STRICT')) {
+        define('E_STRICT', 2048);
+    }
+
+    /**#@+
+     * Includes SimpleTest files.
+     */
+    require_once(dirname(__FILE__) . '/invoker.php');
+
+    /**
+     *    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) {
+            set_error_handler('simpleTestErrorHandler');
+            parent::invoke($method);
+            $queue = &SimpleErrorQueue::instance();
+            while (list($severity, $message, $file, $line, $globals) = $queue->extract()) {
+                $severity = SimpleErrorQueue::getSeverityAsString($severity);
+                $test_case = &$this->getTestCase();
+                $test_case->error($severity, $message, $file, $line);
+            }
+            restore_error_handler();
+        }
+    }
+
+    /**
+     *    Singleton error queue used to record trapped
+     *    errors.
+        *        @package      SimpleTest
+        *        @subpackage   UnitTester
+     */
+    class SimpleErrorQueue {
+        var $_queue;
+
+        /**
+         *    Starts with an empty queue.
+         *    @access public
+         */
+        function SimpleErrorQueue() {
+            $this->clear();
+        }
+
+        /**
+         *    Adds an error to the front of the queue.
+         *    @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.
+         *    @access public
+         */
+        function add($severity, $message, $filename, $line, $super_globals) {
+            array_push(
+                    $this->_queue,
+                    array($severity, $message, $filename, $line, $super_globals));
+        }
+
+        /**
+         *    Pulls the earliest error from the queue.
+         *    @return     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;
+        }
+
+        /**
+         *    Discards the contents of the error queue.
+         *    @access public
+         */
+        function clear() {
+            $this->_queue = array();
+        }
+
+        /**
+         *    Tests to see if the queue is empty.
+         *    @return        True if empty.
+         */
+        function isEmpty() {
+            return (count($this->_queue) == 0);
+        }
+
+        /**
+         *    Global access to a single error queue.
+         *    @return        Global error queue object.
+         *    @access public
+         *    @static
+         */
+        function &instance() {
+            static $queue = false;
+            if (! $queue) {
+                $queue = new SimpleErrorQueue();
+            }
+            return $queue;
+        }
+
+        /**
+         *    Converst 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');
+            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, $line, $super_globals) {
+        if ($severity = $severity & error_reporting()) {
+            restore_error_handler();
+            if (ini_get('log_errors')) {
+                $label = SimpleErrorQueue::getSeverityAsString($severity);
+                error_log("$label: $message in $filename on line $line");
+            }
+            $queue = &SimpleErrorQueue::instance();
+            $queue->add($severity, $message, $filename, $line, $super_globals);
+            set_error_handler('simpleTestErrorHandler');
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/exceptions.php b/lib/simpletestlib/exceptions.php
new file mode 100644 (file)
index 0000000..d8b14ef
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * Includes SimpleTest files and defined the root constant
+     * for dependent libraries.
+     */
+    require_once(dirname(__FILE__) . '/invoker.php');
+
+    /**
+     *    Extension that traps exceptions and turns them into
+     *    an error message.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    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 and dispatches any
+         *    untrapped errors.
+         *    @param string $method    Test method to call.
+         *    @access public
+         */
+        function invoke($method) {
+            try {
+                parent::invoke($method);
+            } catch (Exception $exception) {
+                $test_case = &$this->getTestCase();
+                $test_case->tearDown(); // Added by T.J.Hunt@open.ac.uk.
+                $test_case->exception($exception);
+            }
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/expectation.php b/lib/simpletestlib/expectation.php
new file mode 100644 (file)
index 0000000..7cb4cf8
--- /dev/null
@@ -0,0 +1,720 @@
+<?php
+    /**
+     *    base include file for SimpleTest
+     *    @package    SimpleTest
+     *    @subpackage    UnitTester
+     *    @version    $Id$
+     */
+     
+    /**#@+
+     *    include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/dumper.php');
+    require_once(dirname(__FILE__) . '/compatibility.php');
+    /**#@-*/
+    
+    /**
+     *    Assertion that can display failure information.
+     *    Also includes various helper methods.
+     *    @package SimpleTest
+     *    @subpackage UnitTester
+     *    @abstract
+     */
+    class SimpleExpectation {
+        var $_dumper;
+        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->_dumper = &new SimpleDumper();
+            $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.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function overlayMessage($compare) {
+            return sprintf($this->_message, $this->testMessage($compare));
+        }
+        
+        /**
+         *    Accessor for the dumper.
+         *    @return SimpleDumper    Current value dumper.
+         *    @access protected
+         */
+        function &_getDumper() {
+            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) &&
+                    SimpleTestCompatibility::isA($expectation, 'SimpleExpectation');
+        }
+    }
+    
+    /**
+     *    Test for equality.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class EqualExpectation extends SimpleExpectation {
+        var $_value;
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param mixed $value        Test value to match.
+         *    @param string $message     Customised message on failure.
+         *    @access public
+         */
+        function EqualExpectation($value, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_value = $value;
+        }
+        
+        /**
+         *    Tests the expectation. True if it matches the
+         *    held value.
+         *    @param mixed $compare        Comparison value.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return (($this->_value == $compare) && ($compare == $this->_value));
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Comparison value.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if ($this->test($compare)) {
+                return "Equal expectation [" . $this->_dumper->describeValue($this->_value) . "]";
+            } else {
+                return "Equal expectation fails " .
+                        $this->_dumper->describeDifference($this->_value, $compare);
+            }
+        }
+
+        /**
+         *    Accessor for comparison value.
+         *    @return mixed       Held value to compare with.
+         *    @access protected
+         */
+        function _getValue() {
+            return $this->_value;
+        }
+    }
+    
+    /**
+     *    Test for inequality.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class NotEqualExpectation extends EqualExpectation {
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param mixed $value       Test value to match.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function NotEqualExpectation($value, $message = '%s') {
+            $this->EqualExpectation($value, $message);
+        }
+        
+        /**
+         *    Tests the expectation. True if it differs from the
+         *    held value.
+         *    @param mixed $compare        Comparison value.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return ! parent::test($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();
+            if ($this->test($compare)) {
+                return "Not equal expectation passes " .
+                        $dumper->describeDifference($this->_getValue(), $compare);
+            } else {
+                return "Not equal expectation fails [" .
+                        $dumper->describeValue($this->_getValue()) .
+                        "] matches";
+            }
+        }
+    }
+    
+    /**
+     *    Test for being within a range.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class WithinMarginExpectation extends SimpleExpectation {
+        var $_upper;
+        var $_lower;
+        
+        /**
+         *    Sets the value to compare against and the fuzziness of
+         *    the match. Used for comparing floating point values.
+         *    @param mixed $value        Test value to match.
+         *    @param mixed $margin       Fuzziness of match.
+         *    @param string $message     Customised message on failure.
+         *    @access public
+         */
+        function WithinMarginExpectation($value, $margin, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_upper = $value + $margin;
+            $this->_lower = $value - $margin;
+        }
+        
+        /**
+         *    Tests the expectation. True if it matches the
+         *    held value.
+         *    @param mixed $compare        Comparison value.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return (($compare <= $this->_upper) && ($compare >= $this->_lower));
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Comparison value.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if ($this->test($compare)) {
+                return $this->_withinMessage($compare);
+            } else {
+                return $this->_outsideMessage($compare);
+            }
+        }
+        
+        /**
+         *    Creates a the message for being within the range.
+         *    @param mixed $compare        Value being tested.
+         *    @access private
+         */
+        function _withinMessage($compare) {
+            return "Within expectation [" . $this->_dumper->describeValue($this->_lower) . "] and [" .
+                    $this->_dumper->describeValue($this->_upper) . "]";
+        }
+        
+        /**
+         *    Creates a the message for being within the range.
+         *    @param mixed $compare        Value being tested.
+         *    @access private
+         */
+        function _outsideMessage($compare) {
+            if ($compare > $this->_upper) {
+                return "Outside expectation " .
+                        $this->_dumper->describeDifference($compare, $this->_upper);
+            } else {
+                return "Outside expectation " .
+                        $this->_dumper->describeDifference($compare, $this->_lower);
+            }
+        }
+    }
+    
+    /**
+     *    Test for being outside of a range.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class OutsideMarginExpectation extends WithinMarginExpectation {
+        
+        /**
+         *    Sets the value to compare against and the fuzziness of
+         *    the match. Used for comparing floating point values.
+         *    @param mixed $value        Test value to not match.
+         *    @param mixed $margin       Fuzziness of match.
+         *    @param string $message     Customised message on failure.
+         *    @access public
+         */
+        function OutsideMarginExpectation($value, $margin, $message = '%s') {
+            $this->WithinMarginExpectation($value, $margin, $message);
+        }
+        
+        /**
+         *    Tests the expectation. True if it matches the
+         *    held value.
+         *    @param mixed $compare        Comparison value.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return ! parent::test($compare);
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Comparison value.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if (! $this->test($compare)) {
+                return $this->_withinMessage($compare);
+            } else {
+                return $this->_outsideMessage($compare);
+            }
+        }
+    }
+    
+    /**
+     *    Test for identity.
+     *    @package SimpleTest
+     *    @subpackage UnitTester
+     */
+    class IdenticalExpectation extends EqualExpectation {
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param mixed $value       Test value to match.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function IdenticalExpectation($value, $message = '%s') {
+            $this->EqualExpectation($value, $message);
+        }
+        
+        /**
+         *    Tests the expectation. True if it exactly
+         *    matches the held value.
+         *    @param mixed $compare        Comparison value.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return SimpleTestCompatibility::isIdentical($this->_getValue(), $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();
+            if ($this->test($compare)) {
+                return "Identical expectation [" . $dumper->describeValue($this->_getValue()) . "]";
+            } else {
+                return "Identical expectation [" . $dumper->describeValue($this->_getValue()) .
+                        "] fails with [" .
+                        $dumper->describeValue($compare) . "] " .
+                        $dumper->describeDifference($this->_getValue(), $compare, TYPE_MATTERS);
+            }
+        }
+    }
+    
+    /**
+     *    Test for non-identity.
+     *    @package SimpleTest
+     *    @subpackage UnitTester
+     */
+    class NotIdenticalExpectation extends IdenticalExpectation {
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param mixed $value        Test value to match.
+         *    @param string $message     Customised message on failure.
+         *    @access public
+         */
+        function NotIdenticalExpectation($value, $message = '%s') {
+            $this->IdenticalExpectation($value, $message);
+        }
+        
+        /**
+         *    Tests the expectation. True if it differs from the
+         *    held value.
+         *    @param mixed $compare        Comparison value.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return ! parent::test($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();
+            if ($this->test($compare)) {
+                return "Not identical expectation passes " .
+                        $dumper->describeDifference($this->_getValue(), $compare, TYPE_MATTERS);
+            } else {
+                return "Not identical expectation [" . $dumper->describeValue($this->_getValue()) . "] matches";
+            }
+        }
+    }
+    
+    /**
+     *    Test for a pattern using Perl regex rules.
+     *    @package SimpleTest
+     *    @subpackage UnitTester
+     */
+    class PatternExpectation extends SimpleExpectation {
+        var $_pattern;
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param string $pattern    Pattern to search for.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function PatternExpectation($pattern, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_pattern = $pattern;
+        }
+        
+        /**
+         *    Accessor for the pattern.
+         *    @return string       Perl regex as string.
+         *    @access protected
+         */
+        function _getPattern() {
+            return $this->_pattern;
+        }
+        
+        /**
+         *    Tests the expectation. True if the Perl regex
+         *    matches the comparison value.
+         *    @param string $compare        Comparison value.
+         *    @return boolean               True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return (boolean)preg_match($this->_getPattern(), $compare);
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Comparison value.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if ($this->test($compare)) {
+                return $this->_describePatternMatch($this->_getPattern(), $compare);
+            } else {
+                $dumper = &$this->_getDumper();
+                return "Pattern [" . $this->_getPattern() .
+                        "] not detected in [" .
+                        $dumper->describeValue($compare) . "]";
+            }
+        }
+        
+        /**
+         *    Describes a pattern match including the string
+         *    found and it's position.
+     *    @package SimpleTest
+     *    @subpackage UnitTester
+         *    @param string $pattern        Regex to match against.
+         *    @param string $subject        Subject to search.
+         *    @access protected
+         */
+        function _describePatternMatch($pattern, $subject) {
+            preg_match($pattern, $subject, $matches);
+            $position = strpos($subject, $matches[0]);
+            $dumper = &$this->_getDumper();
+            return "Pattern [$pattern] detected at character [$position] in [" .
+                    $dumper->describeValue($subject) . "] as [" .
+                    $matches[0] . "] in region [" .
+                    $dumper->clipString($subject, 100, $position) . "]";
+        }
+    }
+    
+    /**
+     *      @deprecated
+     */
+    class WantedPatternExpectation extends PatternExpectation {
+    }
+    
+    /**
+     *    Fail if a pattern is detected within the
+     *    comparison.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class NoPatternExpectation extends PatternExpectation {
+        
+        /**
+         *    Sets the reject pattern
+         *    @param string $pattern    Pattern to search for.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function NoPatternExpectation($pattern, $message = '%s') {
+            $this->PatternExpectation($pattern, $message);
+        }
+        
+        /**
+         *    Tests the expectation. False if the Perl regex
+         *    matches the comparison value.
+         *    @param string $compare        Comparison value.
+         *    @return boolean               True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return ! parent::test($compare);
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param string $compare      Comparison value.
+         *    @return string              Description of success
+         *                                or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if ($this->test($compare)) {
+                $dumper = &$this->_getDumper();
+                return "Pattern [" . $this->_getPattern() .
+                        "] not detected in [" .
+                        $dumper->describeValue($compare) . "]";
+            } else {
+                return $this->_describePatternMatch($this->_getPattern(), $compare);
+            }
+        }
+    }
+    
+    /**
+     *    @package SimpleTest
+     *    @subpackage UnitTester
+     *      @deprecated
+     */
+    class UnwantedPatternExpectation extends NoPatternExpectation {
+    }
+    
+    /**
+     *    Tests either type or class name if it's an object.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class IsAExpectation extends SimpleExpectation {
+        var $_type;
+        
+        /**
+         *    Sets the type to compare with.
+         *    @param string $type       Type or class name.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function IsAExpectation($type, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_type = $type;
+        }
+        
+        /**
+         *    Accessor for type to check against.
+         *    @return string    Type or class name.
+         *    @access protected
+         */
+        function _getType() {
+            return $this->_type;
+        }
+        
+        /**
+         *    Tests the expectation. True if the type or
+         *    class matches the string value.
+         *    @param string $compare        Comparison value.
+         *    @return boolean               True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            if (is_object($compare)) {
+                return SimpleTestCompatibility::isA($compare, $this->_type);
+            } else {
+                return (strtolower(gettype($compare)) == $this->_canonicalType($this->_type));
+            }
+        }
+
+        /**
+         *    Coerces type name into a gettype() match.
+         *    @param string $type        User type.
+         *    @return string             Simpler type.
+         *    @access private
+         */
+        function _canonicalType($type) {
+            $type = strtolower($type);
+            $map = array(
+                    'bool' => 'boolean',
+                    'float' => 'double',
+                    'real' => 'double',
+                    'int' => 'integer');
+            if (isset($map[$type])) {
+                $type = $map[$type];
+            }
+            return $type;
+        }
+
+        /**
+         *    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 "Value [" . $dumper->describeValue($compare) .
+                    "] should be type [" . $this->_type . "]";
+        }
+    }
+    
+    /**
+     *    Tests either type or class name if it's an object.
+     *    Will succeed if the type does not match.
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class NotAExpectation extends IsAExpectation {
+        var $_type;
+        
+        /**
+         *    Sets the type to compare with.
+         *    @param string $type       Type or class name.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function NotAExpectation($type, $message = '%s') {
+            $this->IsAExpectation($type, $message);
+        }
+        
+        /**
+         *    Tests the expectation. False if the type or
+         *    class matches the string value.
+         *    @param string $compare        Comparison value.
+         *    @return boolean               True if different.
+         *    @access public
+         */
+        function test($compare) {
+            return ! parent::test($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 "Value [" . $dumper->describeValue($compare) .
+                    "] should not be type [" . $this->_getType() . "]";
+        }
+    }
+
+    /**
+     *    Tests for existance of a method in an object
+     *      @package SimpleTest
+     *      @subpackage UnitTester
+     */
+    class MethodExistsExpectation extends SimpleExpectation {
+        var $_method;
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param string $method     Method to check.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         *    @return void
+         */
+        function MethodExistsExpectation($method, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_method = &$method;
+        }
+        
+        /**
+         *    Tests the expectation. True if the method exists in the test object.
+         *    @param string $compare        Comparison method name.
+         *    @return boolean               True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return (boolean)(is_object($compare) && method_exists($compare, $this->_method));
+        }
+        
+        /**
+         *    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();
+            if (! is_object($compare)) {
+                return 'No method on non-object [' . $dumper->describeValue($compare) . ']';
+            }
+            $method = $this->_method;
+            return "Object [" . $dumper->describeValue($compare) .
+                    "] should contain method [$method]";
+        }
+    }
+?>
diff --git a/lib/simpletestlib/extensions/pear_test_case.php b/lib/simpletestlib/extensions/pear_test_case.php
new file mode 100644 (file)
index 0000000..7c91fb0
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+    /**
+     * adapter for SimpleTest to use PEAR PHPUnit test cases
+     * @package        SimpleTest
+     * @subpackage Extensions
+     * @version        $Id$
+     */
+    
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/../dumper.php');
+    require_once(dirname(__FILE__) . '/../compatibility.php');
+    require_once(dirname(__FILE__) . '/../test_case.php');
+    require_once(dirname(__FILE__) . '/../expectation.php');
+       /**#@-*/
+   
+    /**
+     *    Adapter for PEAR PHPUnit test case to allow
+     *    legacy PEAR test cases to be used with SimpleTest.
+     *    @package      SimpleTest
+     *    @subpackage   Extensions
+     */
+    class PHPUnit_TestCase extends SimpleTestCase {
+        var $_loosely_typed;
+        
+        /**
+         *    Constructor. Sets the test name.
+         *    @param $label        Test name to display.
+         *    @public
+         */
+        function PHPUnit_TestCase($label = false) {
+            $this->SimpleTestCase($label);
+            $this->_loosely_typed = false;
+        }
+        
+        /**
+         *    Will test straight equality if set to loose
+         *    typing, or identity if not.
+         *    @param $first          First value.
+         *    @param $second         Comparison value.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertEquals($first, $second, $message = "%s", $delta = 0) {
+            if ($this->_loosely_typed) {
+                $expectation = &new EqualExpectation($first);
+            } else {
+                $expectation = &new IdenticalExpectation($first);
+            }
+            $this->assert($expectation, $second, $message);
+        }
+        
+        /**
+         *    Passes if the value tested is not null.
+         *    @param $value          Value to test against.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertNotNull($value, $message = "%s") {
+            parent::assertTrue(isset($value), $message);
+        }
+        
+        /**
+         *    Passes if the value tested is null.
+         *    @param $value          Value to test against.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertNull($value, $message = "%s") {
+            parent::assertTrue(!isset($value), $message);
+        }
+        
+        /**
+         *    In PHP5 the identity test tests for the same
+         *    object. This is a reference test in PHP4.
+         *    @param $first          First object handle.
+         *    @param $second         Hopefully the same handle.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertSame(&$first, &$second, $message = "%s") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($first) .
+                            "] and [" . $dumper->describeValue($second) .
+                            "] should reference the same object");
+            return $this->assertTrue(
+                    SimpleTestCompatibility::isReference($first, $second),
+                    $message);
+        }
+        
+        /**
+         *    In PHP5 the identity test tests for the same
+         *    object. This is a reference test in PHP4.
+         *    @param $first          First object handle.
+         *    @param $second         Hopefully a different handle.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertNotSame(&$first, &$second, $message = "%s") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($first) .
+                            "] and [" . $dumper->describeValue($second) .
+                            "] should not be the same object");
+            return $this->assertFalse(
+                    SimpleTestCompatibility::isReference($first, $second),
+                    $message);
+        }
+        
+        /**
+         *    Sends pass if the test condition resolves true,
+         *    a fail otherwise.
+         *    @param $condition      Condition to test true.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertTrue($condition, $message = "%s") {
+            parent::assertTrue($condition, $message);
+        }
+        
+        /**
+         *    Sends pass if the test condition resolves false,
+         *    a fail otherwise.
+         *    @param $condition      Condition to test false.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertFalse($condition, $message = "%s") {
+            parent::assertTrue(!$condition, $message);
+        }
+        
+        /**
+         *    Tests a regex match. Needs refactoring.
+         *    @param $pattern        Regex to match.
+         *    @param $subject        String to search in.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertRegExp($pattern, $subject, $message = "%s") {
+            $this->assert(new PatternExpectation($pattern), $subject, $message);
+        }
+        
+        /**
+         *    Tests the type of a value.
+         *    @param $value          Value to take type of.
+         *    @param $type           Hoped for type.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertType($value, $type, $message = "%s") {
+            parent::assertTrue(gettype($value) == strtolower($type), $message);
+        }
+        
+        /**
+         *    Sets equality operation to act as a simple equal
+         *    comparison only, allowing a broader range of
+         *    matches.
+         *    @param $loosely_typed     True for broader comparison.
+         *    @public
+         */
+        function setLooselyTyped($loosely_typed) {
+            $this->_loosely_typed = $loosely_typed;
+        }
+
+        /**
+         *    For progress indication during
+         *    a test amongst other things.
+         *    @return            Usually one.
+         *    @public
+         */
+        function countTestCases() {
+            return $this->getSize();
+        }
+        
+        /**
+         *    Accessor for name, normally just the class
+         *    name.
+         *    @public
+         */
+        function getName() {
+            return $this->getLabel();
+        }
+        
+        /**
+         *    Does nothing. For compatibility only.
+         *    @param $name        Dummy
+         *    @public
+         */
+        function setName($name) {
+        }
+    }
+?>
diff --git a/lib/simpletestlib/extensions/phpunit_test_case.php b/lib/simpletestlib/extensions/phpunit_test_case.php
new file mode 100644 (file)
index 0000000..7646b9c
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+    /**
+     * adapter for SimpleTest to use PHPUnit test cases
+     * @package        SimpleTest
+     * @subpackage Extensions
+     * @version        $Id$
+     */
+    
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/../unit_tester.php');
+    require_once(dirname(__FILE__) . '/../expectation.php');
+    /**#@-*/
+    
+    /**
+     *    Adapter for sourceforge PHPUnit test case to allow
+     *    legacy test cases to be used with SimpleTest.
+     *    @package             SimpleTest
+     *    @subpackage  Extensions
+     */
+    class TestCase extends SimpleTestCase {
+        
+        /**
+         *    Constructor. Sets the test name.
+         *    @param $label        Test name to display.
+         *    @public
+         */
+        function TestCase($label) {
+            $this->SimpleTestCase($label);
+        }
+        
+        /**
+         *    Sends pass if the test condition resolves true,
+         *    a fail otherwise.
+         *    @param $condition      Condition to test true.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assert($condition, $message = false) {
+            parent::assertTrue($condition, $message);
+        }
+        
+        /**
+         *    Will test straight equality if set to loose
+         *    typing, or identity if not.
+         *    @param $first          First value.
+         *    @param $second         Comparison value.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertEquals($first, $second, $message = false) {
+            parent::assert(new EqualExpectation($first), $second, $message);
+        }
+        
+        /**
+         *    Simple string equality.
+         *    @param $first          First value.
+         *    @param $second         Comparison value.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertEqualsMultilineStrings($first, $second, $message = false) {
+            parent::assert(new EqualExpectation($first), $second, $message);
+        }                             
+        
+        /**
+         *    Tests a regex match.
+         *    @param $pattern        Regex to match.
+         *    @param $subject        String to search in.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function assertRegexp($pattern, $subject, $message = false) {
+            parent::assert(new PatternExpectation($pattern), $subject, $message);
+        }
+        
+        /**
+         *    Sends an error which we interpret as a fail
+         *    with a different message for compatibility.
+         *    @param $message        Message to display.
+         *    @public
+         */
+        function error($message) {
+            parent::fail("Error triggered [$message]");
+        }
+         
+        /**
+         *    Accessor for name.
+         *    @public
+         */
+       function name() {
+            return $this->getLabel();
+        }
+    }
+?>
diff --git a/lib/simpletestlib/form.php b/lib/simpletestlib/form.php
new file mode 100644 (file)
index 0000000..f05a7f7
--- /dev/null
@@ -0,0 +1,352 @@
+<?php
+    /**
+     * Base include file for SimpleTest.
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+     
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/tag.php');
+    require_once(dirname(__FILE__) . '/encoding.php');
+    require_once(dirname(__FILE__) . '/selector.php');
+    /**#@-*/
+    
+    /**
+     *    Form tag class to hold widget values.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleForm {
+        var $_method;
+        var $_action;
+        var $_encoding;
+        var $_default_target;
+        var $_id;
+        var $_buttons;
+        var $_images;
+        var $_widgets;
+        var $_radios;
+        var $_checkboxes;
+        
+        /**
+         *    Starts with no held controls/widgets.
+         *    @param SimpleTag $tag        Form tag to read.
+         *    @param SimpleUrl $url        Location of holding page.
+         */
+        function SimpleForm($tag, $url) {
+            $this->_method = $tag->getAttribute('method');
+            $this->_action = $this->_createAction($tag->getAttribute('action'), $url);
+            $this->_encoding = $this->_setEncodingClass($tag);
+            $this->_default_target = false;
+            $this->_id = $tag->getAttribute('id');
+            $this->_buttons = array();
+            $this->_images = array();
+            $this->_widgets = array();
+            $this->_radios = array();
+            $this->_checkboxes = array();
+        }
+        
+        /**
+         *    Creates the request packet to be sent by the form.
+         *    @param SimpleTag $tag        Form tag to read.
+         *    @return string               Packet class.
+         *    @access private
+         */
+        function _setEncodingClass($tag) {
+            if (strtolower($tag->getAttribute('method')) == 'post') {
+                if (strtolower($tag->getAttribute('enctype')) == 'multipart/form-data') {
+                    return 'SimpleMultipartEncoding';
+                }
+                return 'SimplePostEncoding';
+            }
+            return 'SimpleGetEncoding';
+        }
+        
+        /**
+         *    Sets the frame target within a frameset.
+         *    @param string $frame        Name of frame.
+         *    @access public
+         */
+        function setDefaultTarget($frame) {
+            $this->_default_target = $frame;
+        }
+        
+        /**
+         *    Accessor for method of form submission.
+         *    @return string           Either get or post.
+         *    @access public
+         */
+        function getMethod() {
+            return ($this->_method ? strtolower($this->_method) : 'get');
+        }
+        
+        /**
+         *    Combined action attribute with current location
+         *    to get an absolute form target.
+         *    @param string $action    Action attribute from form tag.
+         *    @param SimpleUrl $base   Page location.
+         *    @return SimpleUrl        Absolute form target.
+         */
+        function _createAction($action, $base) {
+            if (($action === '') || ($action === false)) {
+                return $base;
+            }
+            $url = new SimpleUrl($action);
+            return $url->makeAbsolute($base);
+        }
+        
+        /**
+         *    Absolute URL of the target.
+         *    @return SimpleUrl           URL target.
+         *    @access public
+         */
+        function getAction() {
+            $url = $this->_action;
+            if ($this->_default_target && ! $url->getTarget()) {
+                $url->setTarget($this->_default_target);
+            }
+            return $url;
+        }
+       
+        /**
+         *    Creates the encoding for the current values in the
+         *    form.
+         *    @return SimpleFormEncoding    Request to submit.
+         *    @access private
+         */
+        function _encode() {
+            $class = $this->_encoding;
+            $encoding = new $class();
+            for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) {
+                $this->_widgets[$i]->write($encoding);
+            }
+            return $encoding;
+        }
+                
+        /**
+         *    ID field of form for unique identification.
+         *    @return string           Unique tag ID.
+         *    @access public
+         */
+        function getId() {
+            return $this->_id;
+        }
+        
+        /**
+         *    Adds a tag contents to the form.
+         *    @param SimpleWidget $tag        Input tag to add.
+         *    @access public
+         */
+        function addWidget(&$tag) {
+            if (strtolower($tag->getAttribute('type')) == 'submit') {
+                $this->_buttons[] = &$tag;
+            } elseif (strtolower($tag->getAttribute('type')) == 'image') {
+                $this->_images[] = &$tag;
+            } elseif ($tag->getName()) {
+                $this->_setWidget($tag);
+            }
+        }
+        
+        /**
+         *    Sets the widget into the form, grouping radio
+         *    buttons if any.
+         *    @param SimpleWidget $tag   Incoming form control.
+         *    @access private
+         */
+        function _setWidget(&$tag) {
+            if (strtolower($tag->getAttribute('type')) == 'radio') {
+                $this->_addRadioButton($tag);
+            } elseif (strtolower($tag->getAttribute('type')) == 'checkbox') {
+                $this->_addCheckbox($tag);
+            } else {
+                $this->_widgets[] = &$tag;
+            }
+        }
+        
+        /**
+         *    Adds a radio button, building a group if necessary.
+         *    @param SimpleRadioButtonTag $tag   Incoming form control.
+         *    @access private
+         */
+        function _addRadioButton(&$tag) {
+            if (! isset($this->_radios[$tag->getName()])) {
+                $this->_widgets[] = &new SimpleRadioGroup();
+                $this->_radios[$tag->getName()] = count($this->_widgets) - 1;
+            }
+            $this->_widgets[$this->_radios[$tag->getName()]]->addWidget($tag);
+        }
+        
+        /**
+         *    Adds a checkbox, making it a group on a repeated name.
+         *    @param SimpleCheckboxTag $tag   Incoming form control.
+         *    @access private
+         */
+        function _addCheckbox(&$tag) {
+            if (! isset($this->_checkboxes[$tag->getName()])) {
+                $this->_widgets[] = &$tag;
+                $this->_checkboxes[$tag->getName()] = count($this->_widgets) - 1;
+            } else {
+                $index = $this->_checkboxes[$tag->getName()];
+                if (! SimpleTestCompatibility::isA($this->_widgets[$index], 'SimpleCheckboxGroup')) {
+                    $previous = &$this->_widgets[$index];
+                    $this->_widgets[$index] = &new SimpleCheckboxGroup();
+                    $this->_widgets[$index]->addWidget($previous);
+                }
+                $this->_widgets[$index]->addWidget($tag);
+            }
+        }
+        
+        /**
+         *    Extracts current value from form.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @return string/array              Value(s) as string or null
+         *                                      if not set.
+         *    @access public
+         */
+        function getValue($selector) {
+            for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) {
+                if ($selector->isMatch($this->_widgets[$i])) {
+                    return $this->_widgets[$i]->getValue();
+                }
+            }
+            foreach ($this->_buttons as $button) {
+                if ($selector->isMatch($button)) {
+                    return $button->getValue();
+                }
+            }
+            return null;
+        }
+        
+        /**
+         *    Sets a widget value within the form.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @param string $value              Value to input into the widget.
+         *    @return boolean                   True if value is legal, false
+         *                                      otherwise. If the field is not
+         *                                      present, nothing will be set.
+         *    @access public
+         */
+        function setField($selector, $value) {
+            $success = false;
+            for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) {
+                if ($selector->isMatch($this->_widgets[$i])) {
+                    if ($this->_widgets[$i]->setValue($value)) {
+                        $success = true;
+                    }
+                }
+            }
+            return $success;
+        }
+        
+        /**
+         *    Used by the page object to set widgets labels to
+         *    external label tags.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @access public
+         */
+        function attachLabelBySelector($selector, $label) {
+            for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) {
+                if ($selector->isMatch($this->_widgets[$i])) {
+                    if (method_exists($this->_widgets[$i], 'setLabel')) {
+                        $this->_widgets[$i]->setLabel($label);
+                        return;
+                    }
+                }
+            }
+        }
+        
+        /**
+         *    Test to see if a form has a submit button.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @return boolean                   True if present.
+         *    @access public
+         */
+        function hasSubmit($selector) {
+            foreach ($this->_buttons as $button) {
+                if ($selector->isMatch($button)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Test to see if a form has an image control.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @return boolean                   True if present.
+         *    @access public
+         */
+        function hasImage($selector) {
+            foreach ($this->_images as $image) {
+                if ($selector->isMatch($image)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+       
+        /**
+         *    Gets the submit values for a selected button.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @param hash $additional           Additional data for the form.
+         *    @return SimpleEncoding            Submitted values or false
+         *                                      if there is no such button
+         *                                      in the form.
+         *    @access public
+         */
+        function submitButton($selector, $additional = false) {
+            $additional = $additional ? $additional : array();
+            foreach ($this->_buttons as $button) {
+                if ($selector->isMatch($button)) {
+                    $encoding = $this->_encode();
+                    $button->write($encoding);
+                    if ($additional) {
+                        $encoding->merge($additional);
+                    }
+                    return $encoding;           
+                }
+            }
+            return false;
+        }
+         
+        /**
+         *    Gets the submit values for an image.
+         *    @param SimpleSelector $selector   Criteria to apply.
+         *    @param integer $x                 X-coordinate of click.
+         *    @param integer $y                 Y-coordinate of click.
+         *    @param hash $additional           Additional data for the form.
+         *    @return SimpleEncoding            Submitted values or false
+         *                                      if there is no such button in the
+         *                                      form.
+         *    @access public
+         */
+        function submitImage($selector, $x, $y, $additional = false) {
+            $additional = $additional ? $additional : array();
+            foreach ($this->_images as $image) {
+                if ($selector->isMatch($image)) {
+                    $encoding = $this->_encode();
+                    $image->write($encoding, $x, $y);
+                    if ($additional) {
+                        $encoding->merge($additional);
+                    }
+                    return $encoding;           
+                }
+            }
+            return false;
+        }
+      
+        /**
+         *    Simply submits the form without the submit button
+         *    value. Used when there is only one button or it
+         *    is unimportant.
+         *    @return hash           Submitted values.
+         *    @access public
+         */
+        function submit() {
+            return $this->_encode();
+        }
+    }
+?>
diff --git a/lib/simpletestlib/frames.php b/lib/simpletestlib/frames.php
new file mode 100644 (file)
index 0000000..ce669ea
--- /dev/null
@@ -0,0 +1,588 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/page.php');
+    require_once(dirname(__FILE__) . '/user_agent.php');
+    /**#@-*/
+
+    /**
+     *    A composite page. Wraps a frameset page and
+     *    adds subframes. The original page will be
+     *    mostly ignored. Implements the SimplePage
+     *    interface so as to be interchangeable.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleFrameset {
+        var $_frameset;
+        var $_frames;
+        var $_focus;
+        var $_names;
+
+        /**
+         *    Stashes the frameset page. Will make use of the
+         *    browser to fetch the sub frames recursively.
+         *    @param SimplePage $page        Frameset page.
+         */
+        function SimpleFrameset(&$page) {
+            $this->_frameset = &$page;
+            $this->_frames = array();
+            $this->_focus = false;
+            $this->_names = array();
+        }
+
+        /**
+         *    Adds a parsed page to the frameset.
+         *    @param SimplePage $page    Frame page.
+         *    @param string $name        Name of frame in frameset.
+         *    @access public
+         */
+        function addFrame(&$page, $name = false) {
+            $this->_frames[] = &$page;
+            if ($name) {
+                $this->_names[$name] = count($this->_frames) - 1;
+            }
+        }
+
+        /**
+         *    Replaces existing frame with another. If the
+         *    frame is nested, then the call is passed down
+         *    one level.
+         *    @param array $path        Path of frame in frameset.
+         *    @param SimplePage $page   Frame source.
+         *    @access public
+         */
+        function setFrame($path, &$page) {
+            $name = array_shift($path);
+            if (isset($this->_names[$name])) {
+                $index = $this->_names[$name];
+            } else {
+                $index = $name - 1;
+            }
+            if (count($path) == 0) {
+                $this->_frames[$index] = &$page;
+                return;
+            }
+            $this->_frames[$index]->setFrame($path, $page);
+        }
+
+        /**
+         *    Accessor for current frame focus. Will be
+         *    false if no frame has focus. Will have the nested
+         *    frame focus if any.
+         *    @return array     Labels or indexes of nested frames.
+         *    @access public
+         */
+        function getFrameFocus() {
+            if ($this->_focus === false) {
+                return array();
+            }
+            return array_merge(
+                    array($this->_getPublicNameFromIndex($this->_focus)),
+                    $this->_frames[$this->_focus]->getFrameFocus());
+        }
+
+        /**
+         *    Turns an internal array index into the frames list
+         *    into a public name, or if none, then a one offset
+         *    index.
+         *    @param integer $subject    Internal index.
+         *    @return integer/string     Public name.
+         *    @access private
+         */
+        function _getPublicNameFromIndex($subject) {
+            foreach ($this->_names as $name => $index) {
+                if ($subject == $index) {
+                    return $name;
+                }
+            }
+            return $subject + 1;
+        }
+
+        /**
+         *    Sets the focus by index. The integer index starts from 1.
+         *    If already focused and the target frame also has frames,
+         *    then the nested frame will be focused.
+         *    @param integer $choice    Chosen frame.
+         *    @return boolean           True if frame exists.
+         *    @access public
+         */
+        function setFrameFocusByIndex($choice) {
+            if (is_integer($this->_focus)) {
+                if ($this->_frames[$this->_focus]->hasFrames()) {
+                    return $this->_frames[$this->_focus]->setFrameFocusByIndex($choice);
+                }
+            }
+            if (($choice < 1) || ($choice > count($this->_frames))) {
+                return false;
+            }
+            $this->_focus = $choice - 1;
+            return true;
+        }
+
+        /**
+         *    Sets the focus by name. If already focused and the
+         *    target frame also has frames, then the nested frame
+         *    will be focused.
+         *    @param string $name    Chosen frame.
+         *    @return boolean        True if frame exists.
+         *    @access public
+         */
+        function setFrameFocus($name) {
+            if (is_integer($this->_focus)) {
+                if ($this->_frames[$this->_focus]->hasFrames()) {
+                    return $this->_frames[$this->_focus]->setFrameFocus($name);
+                }
+            }
+            if (in_array($name, array_keys($this->_names))) {
+                $this->_focus = $this->_names[$name];
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         *    Clears the frame focus.
+         *    @access public
+         */
+        function clearFrameFocus() {
+            $this->_focus = false;
+            $this->_clearNestedFramesFocus();
+        }
+
+        /**
+         *    Clears the frame focus for any nested frames.
+         *    @access private
+         */
+        function _clearNestedFramesFocus() {
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $this->_frames[$i]->clearFrameFocus();
+            }
+        }
+
+        /**
+         *    Test for the presence of a frameset.
+         *    @return boolean        Always true.
+         *    @access public
+         */
+        function hasFrames() {
+            return true;
+        }
+
+        /**
+         *    Accessor for frames information.
+         *    @return array/string      Recursive hash of frame URL strings.
+         *                              The key is either a numerical
+         *                              index or the name attribute.
+         *    @access public
+         */
+        function getFrames() {
+            $report = array();
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $report[$this->_getPublicNameFromIndex($i)] =
+                        $this->_frames[$i]->getFrames();
+            }
+            return $report;
+        }
+
+        /**
+         *    Accessor for raw text of either all the pages or
+         *    the frame in focus.
+         *    @return string        Raw unparsed content.
+         *    @access public
+         */
+        function getRaw() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getRaw();
+            }
+            $raw = '';
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $raw .= $this->_frames[$i]->getRaw();
+            }
+            return $raw;
+        }
+
+        /**
+         *    Accessor for plain text of either all the pages or
+         *    the frame in focus.
+         *    @return string        Plain text content.
+         *    @access public
+         */
+        function getText() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getText();
+            }
+            $raw = '';
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $raw .= ' ' . $this->_frames[$i]->getText();
+            }
+            return trim($raw);
+        }
+
+        /**
+         *    Accessor for last error.
+         *    @return string        Error from last response.
+         *    @access public
+         */
+        function getTransportError() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getTransportError();
+            }
+            return $this->_frameset->getTransportError();
+        }
+
+        /**
+         *    Request method used to fetch this frame.
+         *    @return string      GET, POST or HEAD.
+         *    @access public
+         */
+        function getMethod() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getMethod();
+            }
+            return $this->_frameset->getMethod();
+        }
+
+        /**
+         *    Original resource name.
+         *    @return SimpleUrl        Current url.
+         *    @access public
+         */
+        function getUrl() {
+            if (is_integer($this->_focus)) {
+                $url = $this->_frames[$this->_focus]->getUrl();
+                $url->setTarget($this->_getPublicNameFromIndex($this->_focus));
+            } else {
+                $url = $this->_frameset->getUrl();
+            }
+            return $url;
+        }
+
+        /**
+         *    Original request data.
+         *    @return mixed              Sent content.
+         *    @access public
+         */
+        function getRequestData() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getRequestData();
+            }
+            return $this->_frameset->getRequestData();
+        }
+
+        /**
+         *    Accessor for current MIME type.
+         *    @return string    MIME type as string; e.g. 'text/html'
+         *    @access public
+         */
+        function getMimeType() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getMimeType();
+            }
+            return $this->_frameset->getMimeType();
+        }
+
+        /**
+         *    Accessor for last response code.
+         *    @return integer    Last HTTP response code received.
+         *    @access public
+         */
+        function getResponseCode() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getResponseCode();
+            }
+            return $this->_frameset->getResponseCode();
+        }
+
+        /**
+         *    Accessor for last Authentication type. Only valid
+         *    straight after a challenge (401).
+         *    @return string    Description of challenge type.
+         *    @access public
+         */
+        function getAuthentication() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getAuthentication();
+            }
+            return $this->_frameset->getAuthentication();
+        }
+
+        /**
+         *    Accessor for last Authentication realm. Only valid
+         *    straight after a challenge (401).
+         *    @return string    Name of security realm.
+         *    @access public
+         */
+        function getRealm() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getRealm();
+            }
+            return $this->_frameset->getRealm();
+        }
+
+        /**
+         *    Accessor for outgoing header information.
+         *    @return string      Header block.
+         *    @access public
+         */
+        function getRequest() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getRequest();
+            }
+            return $this->_frameset->getRequest();
+        }
+
+        /**
+         *    Accessor for raw header information.
+         *    @return string      Header block.
+         *    @access public
+         */
+        function getHeaders() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getHeaders();
+            }
+            return $this->_frameset->getHeaders();
+        }
+
+        /**
+         *    Accessor for parsed title.
+         *    @return string     Title or false if no title is present.
+         *    @access public
+         */
+        function getTitle() {
+            return $this->_frameset->getTitle();
+        }
+
+        /**
+         *    Accessor for a list of all fixed links.
+         *    @return array   List of urls with scheme of
+         *                    http or https and hostname.
+         *    @access public
+         */
+        function getAbsoluteUrls() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getAbsoluteUrls();
+            }
+            $urls = array();
+            foreach ($this->_frames as $frame) {
+                $urls = array_merge($urls, $frame->getAbsoluteUrls());
+            }
+            return array_values(array_unique($urls));
+        }
+
+        /**
+         *    Accessor for a list of all relative links.
+         *    @return array      List of urls without hostname.
+         *    @access public
+         */
+        function getRelativeUrls() {
+            if (is_integer($this->_focus)) {
+                return $this->_frames[$this->_focus]->getRelativeUrls();
+            }
+            $urls = array();
+            foreach ($this->_frames as $frame) {
+                $urls = array_merge($urls, $frame->getRelativeUrls());
+            }
+            return array_values(array_unique($urls));
+        }
+
+        /**
+         *    Accessor for URLs by the link label. Label will match
+         *    regardess of whitespace issues and case.
+         *    @param string $label    Text of link.
+         *    @return array           List of links with that label.
+         *    @access public
+         */
+        function getUrlsByLabel($label) {
+            if (is_integer($this->_focus)) {
+                return $this->_tagUrlsWithFrame(
+                        $this->_frames[$this->_focus]->getUrlsByLabel($label),
+                        $this->_focus);
+            }
+            $urls = array();
+            foreach ($this->_frames as $index => $frame) {
+                $urls = array_merge(
+                        $urls,
+                        $this->_tagUrlsWithFrame(
+                                    $frame->getUrlsByLabel($label),
+                                    $index));
+            }
+            return $urls;
+        }
+
+        /**
+         *    Accessor for a URL by the id attribute. If in a frameset
+         *    then the first link found with that ID attribute is
+         *    returned only. Focus on a frame if you want one from
+         *    a specific part of the frameset.
+         *    @param string $id       Id attribute of link.
+         *    @return string          URL with that id.
+         *    @access public
+         */
+        function getUrlById($id) {
+            foreach ($this->_frames as $index => $frame) {
+                if ($url = $frame->getUrlById($id)) {
+                    if (! $url->gettarget()) {
+                        $url->setTarget($this->_getPublicNameFromIndex($index));
+                    }
+                    return $url;
+                }
+            }
+            return false;
+        }
+
+        /**
+         *    Attaches the intended frame index to a list of URLs.
+         *    @param array $urls        List of SimpleUrls.
+         *    @param string $frame      Name of frame or index.
+         *    @return array             List of tagged URLs.
+         *    @access private
+         */
+        function _tagUrlsWithFrame($urls, $frame) {
+            $tagged = array();
+            foreach ($urls as $url) {
+                if (! $url->getTarget()) {
+                    $url->setTarget($this->_getPublicNameFromIndex($frame));
+                }
+                $tagged[] = $url;
+            }
+            return $tagged;
+        }
+
+        /**
+         *    Finds a held form by button label. Will only
+         *    search correctly built forms.
+         *    @param SimpleSelector $selector       Button finder.
+         *    @return SimpleForm                    Form object containing
+         *                                          the button.
+         *    @access public
+         */
+        function &getFormBySubmit($selector) {
+            $form = &$this->_findForm('getFormBySubmit', $selector);
+            return $form;
+        }
+
+        /**
+         *    Finds a held form by image using a selector.
+         *    Will only search correctly built forms. The first
+         *    form found either within the focused frame, or
+         *    across frames, will be the one returned.
+         *    @param SimpleSelector $selector  Image finder.
+         *    @return SimpleForm               Form object containing
+         *                                     the image.
+         *    @access public
+         */
+        function &getFormByImage($selector) {
+            $form = &$this->_findForm('getFormByImage', $selector);
+            return $form;
+        }
+
+        /**
+         *    Finds a held form by the form ID. A way of
+         *    identifying a specific form when we have control
+         *    of the HTML code. The first form found
+         *    either within the focused frame, or across frames,
+         *    will be the one returned.
+         *    @param string $id     Form label.
+         *    @return SimpleForm    Form object containing the matching ID.
+         *    @access public
+         */
+        function &getFormById($id) {
+            $form = &$this->_findForm('getFormById', $id);
+            return $form;
+        }
+
+        /**
+         *    General form finder. Will search all the frames or
+         *    just the one in focus.
+         *    @param string $method    Method to use to find in a page.
+         *    @param string $attribute Label, name or ID.
+         *    @return SimpleForm    Form object containing the matching ID.
+         *    @access private
+         */
+        function &_findForm($method, $attribute) {
+            if (is_integer($this->_focus)) {
+                $form = &$this->_findFormInFrame(
+                        $this->_frames[$this->_focus],
+                        $this->_focus,
+                        $method,
+                        $attribute);
+                return $form;
+            }
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $form = &$this->_findFormInFrame(
+                        $this->_frames[$i],
+                        $i,
+                        $method,
+                        $attribute);
+                if ($form) {
+                    return $form;
+                }
+            }
+            $null = null;
+            return $null;
+        }
+
+        /**
+         *    Finds a form in a page using a form finding method. Will
+         *    also tag the form with the frame name it belongs in.
+         *    @param SimplePage $page  Page content of frame.
+         *    @param integer $index    Internal frame representation.
+         *    @param string $method    Method to use to find in a page.
+         *    @param string $attribute Label, name or ID.
+         *    @return SimpleForm       Form object containing the matching ID.
+         *    @access private
+         */
+        function &_findFormInFrame(&$page, $index, $method, $attribute) {
+            $form = &$this->_frames[$index]->$method($attribute);
+            if (isset($form)) {
+                $form->setDefaultTarget($this->_getPublicNameFromIndex($index));
+            }
+            return $form;
+        }
+
+        /**
+         *    Sets a field on each form in which the field is
+         *    available.
+         *    @param SimpleSelector $selector    Field finder.
+         *    @param string $value               Value to set field to.
+         *    @return boolean                    True if value is valid.
+         *    @access public
+         */
+        function setField($selector, $value) {
+            if (is_integer($this->_focus)) {
+                $this->_frames[$this->_focus]->setField($selector, $value);
+            } else {
+                for ($i = 0; $i < count($this->_frames); $i++) {
+                    $this->_frames[$i]->setField($selector, $value);
+                }
+            }
+        }
+
+        /**
+         *    Accessor for a form element value within a page.
+         *    @param SimpleSelector $selector    Field finder.
+         *    @return string/boolean             A string if the field is
+         *                                       present, false if unchecked
+         *                                       and null if missing.
+         *    @access public
+         */
+        function getField($selector) {
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $value = $this->_frames[$i]->getField($selector);
+                if (isset($value)) {
+                    return $value;
+                }
+            }
+            return null;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/http.php b/lib/simpletestlib/http.php
new file mode 100644 (file)
index 0000000..120d74c
--- /dev/null
@@ -0,0 +1,624 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/socket.php');
+    require_once(dirname(__FILE__) . '/cookies.php');
+    require_once(dirname(__FILE__) . '/url.php');
+    /**#@-*/
+    
+    /**
+     *    Creates HTTP headers for the end point of
+     *    a HTTP request.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleRoute {
+        var $_url;
+        
+        /**
+         *    Sets the target URL.
+         *    @param SimpleUrl $url   URL as object.
+         *    @access public
+         */
+        function SimpleRoute($url) {
+            $this->_url = $url;
+        }
+        
+        /**
+         *    Resource name.
+         *    @return SimpleUrl        Current url.
+         *    @access protected
+         */
+        function getUrl() {
+            return $this->_url;
+        }
+        
+        /**
+         *    Creates the first line which is the actual request.
+         *    @param string $method   HTTP request method, usually GET.
+         *    @return string          Request line content.
+         *    @access protected
+         */
+        function _getRequestLine($method) {
+            return $method . ' ' . $this->_url->getPath() .
+                    $this->_url->getEncodedRequest() . ' HTTP/1.0';
+        }
+        
+        /**
+         *    Creates the host part of the request.
+         *    @return string          Host line content.
+         *    @access protected
+         */
+        function _getHostLine() {
+            $line = 'Host: ' . $this->_url->getHost();
+            if ($this->_url->getPort()) {
+                $line .= ':' . $this->_url->getPort();
+            }
+            return $line;
+        }
+        
+        /**
+         *    Opens a socket to the route.
+         *    @param string $method      HTTP request method, usually GET.
+         *    @param integer $timeout    Connection timeout.
+         *    @return SimpleSocket       New socket.
+         *    @access public
+         */
+        function &createConnection($method, $timeout) {
+            $default_port = ('https' == $this->_url->getScheme()) ? 443 : 80;
+            $socket = &$this->_createSocket(
+                    $this->_url->getScheme() ? $this->_url->getScheme() : 'http',
+                    $this->_url->getHost(),
+                    $this->_url->getPort() ? $this->_url->getPort() : $default_port,
+                    $timeout);
+            if (! $socket->isError()) {
+                $socket->write($this->_getRequestLine($method) . "\r\n");
+                $socket->write($this->_getHostLine() . "\r\n");
+                $socket->write("Connection: close\r\n");
+            }
+            return $socket;
+        }
+        
+        /**
+         *    Factory for socket.
+         *    @param string $scheme                   Protocol to use.
+         *    @param string $host                     Hostname to connect to.
+         *    @param integer $port                    Remote port.
+         *    @param integer $timeout                 Connection timeout.
+         *    @return SimpleSocket/SimpleSecureSocket New socket.
+         *    @access protected
+         */
+        function &_createSocket($scheme, $host, $port, $timeout) {
+            if (in_array($scheme, array('https'))) {
+                $socket = &new SimpleSecureSocket($host, $port, $timeout);
+            } else {
+                $socket = &new SimpleSocket($host, $port, $timeout);
+            }
+            return $socket;
+        }
+    }
+    
+    /**
+     *    Creates HTTP headers for the end point of
+     *    a HTTP request via a proxy server.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleProxyRoute extends SimpleRoute {
+        var $_proxy;
+        var $_username;
+        var $_password;
+        
+        /**
+         *    Stashes the proxy address.
+         *    @param SimpleUrl $url     URL as object.
+         *    @param string $proxy      Proxy URL.
+         *    @param string $username   Username for autentication.
+         *    @param string $password   Password for autentication.
+         *    @access public
+         */
+        function SimpleProxyRoute($url, $proxy, $username = false, $password = false) {
+            $this->SimpleRoute($url);
+            $this->_proxy = $proxy;
+            $this->_username = $username;
+            $this->_password = $password;
+        }
+        
+        /**
+         *    Creates the first line which is the actual request.
+         *    @param string $method   HTTP request method, usually GET.
+         *    @param SimpleUrl $url   URL as object.
+         *    @return string          Request line content.
+         *    @access protected
+         */
+        function _getRequestLine($method) {
+            $url = $this->getUrl();
+            $scheme = $url->getScheme() ? $url->getScheme() : 'http';
+            $port = $url->getPort() ? ':' . $url->getPort() : '';
+            return $method . ' ' . $scheme . '://' . $url->getHost() . $port .
+                    $url->getPath() . $url->getEncodedRequest() . ' HTTP/1.0';
+        }
+        
+        /**
+         *    Creates the host part of the request.
+         *    @param SimpleUrl $url   URL as object.
+         *    @return string          Host line content.
+         *    @access protected
+         */
+        function _getHostLine() {
+            $host = 'Host: ' . $this->_proxy->getHost();
+            $port = $this->_proxy->getPort() ? $this->_proxy->getPort() : 8080;
+            return "$host:$port";
+        }
+        
+        /**
+         *    Opens a socket to the route.
+         *    @param string $method       HTTP request method, usually GET.
+         *    @param integer $timeout     Connection timeout.
+         *    @return SimpleSocket        New socket.
+         *    @access public
+         */
+        function &createConnection($method, $timeout) {
+            $socket = &$this->_createSocket(
+                    $this->_proxy->getScheme() ? $this->_proxy->getScheme() : 'http',
+                    $this->_proxy->getHost(),
+                    $this->_proxy->getPort() ? $this->_proxy->getPort() : 8080,
+                    $timeout);
+            if ($socket->isError()) {
+                return $socket;
+            }
+            $socket->write($this->_getRequestLine($method) . "\r\n");
+            $socket->write($this->_getHostLine() . "\r\n");
+            if ($this->_username && $this->_password) {
+                $socket->write('Proxy-Authorization: Basic ' .
+                        base64_encode($this->_username . ':' . $this->_password) .
+                        "\r\n");
+            }
+            $socket->write("Connection: close\r\n");
+            return $socket;
+        }
+    }
+
+    /**
+     *    HTTP request for a web page. Factory for
+     *    HttpResponse object.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleHttpRequest {
+        var $_route;
+        var $_encoding;
+        var $_headers;
+        var $_cookies;
+        
+        /**
+         *    Builds the socket request from the different pieces.
+         *    These include proxy information, URL, cookies, headers,
+         *    request method and choice of encoding.
+         *    @param SimpleRoute $route              Request route.
+         *    @param SimpleFormEncoding $encoding    Content to send with
+         *                                           request.
+         *    @access public
+         */
+        function SimpleHttpRequest(&$route, $encoding) {
+            $this->_route = &$route;
+            $this->_encoding = $encoding;
+            $this->_headers = array();
+            $this->_cookies = array();
+        }
+        
+        /**
+         *    Dispatches the content to the route's socket.
+         *    @param integer $timeout      Connection timeout.
+         *    @return SimpleHttpResponse   A response which may only have
+         *                                 an error, but hopefully has a
+         *                                 complete web page.
+         *    @access public
+         */
+        function &fetch($timeout) {
+            $socket = &$this->_route->createConnection($this->_encoding->getMethod(), $timeout);
+            if (! $socket->isError()) {
+                $this->_dispatchRequest($socket, $this->_encoding);
+            }
+            $response = &$this->_createResponse($socket);
+            return $response;
+        }
+        
+        /**
+         *    Sends the headers.
+         *    @param SimpleSocket $socket           Open socket.
+         *    @param string $method                 HTTP request method,
+         *                                          usually GET.
+         *    @param SimpleFormEncoding $encoding   Content to send with request.
+         *    @access private
+         */
+        function _dispatchRequest(&$socket, $encoding) {
+            foreach ($this->_headers as $header_line) {
+                $socket->write($header_line . "\r\n");
+            }
+            if (count($this->_cookies) > 0) {
+                $socket->write("Cookie: " . implode(";", $this->_cookies) . "\r\n");
+            }
+            $encoding->writeHeadersTo($socket);
+            $socket->write("\r\n");
+            $encoding->writeTo($socket);
+        }
+        
+        /**
+         *    Adds a header line to the request.
+         *    @param string $header_line    Text of full header line.
+         *    @access public
+         */
+        function addHeaderLine($header_line) {
+            $this->_headers[] = $header_line;
+        }
+        
+        /**
+         *    Reads all the relevant cookies from the
+         *    cookie jar.
+         *    @param SimpleCookieJar $jar     Jar to read
+         *    @param SimpleUrl $url           Url to use for scope.
+         *    @access public
+         */
+        function readCookiesFromJar($jar, $url) {
+            $this->_cookies = $jar->selectAsPairs($url);
+        }
+        
+        /**
+         *    Wraps the socket in a response parser.
+         *    @param SimpleSocket $socket   Responding socket.
+         *    @return SimpleHttpResponse    Parsed response object.
+         *    @access protected
+         */
+        function &_createResponse(&$socket) {
+            $response = &new SimpleHttpResponse(
+                    $socket,
+                    $this->_route->getUrl(),
+                    $this->_encoding);
+            return $response;
+        }
+    }
+    
+    /**
+     *    Collection of header lines in the response.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleHttpHeaders {
+        var $_raw_headers;
+        var $_response_code;
+        var $_http_version;
+        var $_mime_type;
+        var $_location;
+        var $_cookies;
+        var $_authentication;
+        var $_realm;
+        
+        /**
+         *    Parses the incoming header block.
+         *    @param string $headers     Header block.
+         *    @access public
+         */
+        function SimpleHttpHeaders($headers) {
+            $this->_raw_headers = $headers;
+            $this->_response_code = false;
+            $this->_http_version = false;
+            $this->_mime_type = '';
+            $this->_location = false;
+            $this->_cookies = array();
+            $this->_authentication = false;
+            $this->_realm = false;
+            foreach (split("\r\n", $headers) as $header_line) {
+                $this->_parseHeaderLine($header_line);
+            }
+        }
+        
+        /**
+         *    Accessor for parsed HTTP protocol version.
+         *    @return integer           HTTP error code.
+         *    @access public
+         */
+        function getHttpVersion() {
+            return $this->_http_version;
+        }
+        
+        /**
+         *    Accessor for raw header block.
+         *    @return string        All headers as raw string.
+         *    @access public
+         */
+        function getRaw() {
+            return $this->_raw_headers;
+        }
+        
+        /**
+         *    Accessor for parsed HTTP error code.
+         *    @return integer           HTTP error code.
+         *    @access public
+         */
+        function getResponseCode() {
+            return (integer)$this->_response_code;
+        }
+        
+        /**
+         *    Returns the redirected URL or false if
+         *    no redirection.
+         *    @return string      URL or false for none.
+         *    @access public
+         */
+        function getLocation() {
+            return $this->_location;
+        }
+        
+        /**
+         *    Test to see if the response is a valid redirect.
+         *    @return boolean       True if valid redirect.
+         *    @access public
+         */
+        function isRedirect() {
+            return in_array($this->_response_code, array(301, 302, 303, 307)) &&
+                    (boolean)$this->getLocation();
+        }
+        
+        /**
+         *    Test to see if the response is an authentication
+         *    challenge.
+         *    @return boolean       True if challenge.
+         *    @access public
+         */
+        function isChallenge() {
+            return ($this->_response_code == 401) &&
+                    (boolean)$this->_authentication &&
+                    (boolean)$this->_realm;
+        }
+        
+        /**
+         *    Accessor for MIME type header information.
+         *    @return string           MIME type.
+         *    @access public
+         */
+        function getMimeType() {
+            return $this->_mime_type;
+        }
+        
+        /**
+         *    Accessor for authentication type.
+         *    @return string        Type.
+         *    @access public
+         */
+        function getAuthentication() {
+            return $this->_authentication;
+        }
+        
+        /**
+         *    Accessor for security realm.
+         *    @return string        Realm.
+         *    @access public
+         */
+        function getRealm() {
+            return $this->_realm;
+        }
+        
+        /**
+         *    Writes new cookies to the cookie jar.
+         *    @param SimpleCookieJar $jar   Jar to write to.
+         *    @param SimpleUrl $url         Host and path to write under.
+         *    @access public
+         */
+        function writeCookiesToJar(&$jar, $url) {
+            foreach ($this->_cookies as $cookie) {
+                $jar->setCookie(
+                        $cookie->getName(),
+                        $cookie->getValue(),
+                        $url->getHost(),
+                        $cookie->getPath(),
+                        $cookie->getExpiry());
+            }
+        }
+
+        /**
+         *    Called on each header line to accumulate the held
+         *    data within the class.
+         *    @param string $header_line        One line of header.
+         *    @access protected
+         */
+        function _parseHeaderLine($header_line) {
+            if (preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)/i', $header_line, $matches)) {
+                $this->_http_version = $matches[1];
+                $this->_response_code = $matches[2];
+            }
+            if (preg_match('/Content-type:\s*(.*)/i', $header_line, $matches)) {
+                $this->_mime_type = trim($matches[1]);
+            }
+            if (preg_match('/Location:\s*(.*)/i', $header_line, $matches)) {
+                $this->_location = trim($matches[1]);
+            }
+            if (preg_match('/Set-cookie:(.*)/i', $header_line, $matches)) {
+                $this->_cookies[] = $this->_parseCookie($matches[1]);
+            }
+            if (preg_match('/WWW-Authenticate:\s+(\S+)\s+realm=\"(.*?)\"/i', $header_line, $matches)) {
+                $this->_authentication = $matches[1];
+                $this->_realm = trim($matches[2]);
+            }
+        }
+        
+        /**
+         *    Parse the Set-cookie content.
+         *    @param string $cookie_line    Text after "Set-cookie:"
+         *    @return SimpleCookie          New cookie object.
+         *    @access private
+         */
+        function _parseCookie($cookie_line) {
+            $parts = split(";", $cookie_line);
+            $cookie = array();
+            preg_match('/\s*(.*?)\s*=(.*)/', array_shift($parts), $cookie);
+            foreach ($parts as $part) {
+                if (preg_match('/\s*(.*?)\s*=(.*)/', $part, $matches)) {
+                    $cookie[$matches[1]] = trim($matches[2]);
+                }
+            }
+            return new SimpleCookie(
+                    $cookie[1],
+                    trim($cookie[2]),
+                    isset($cookie["path"]) ? $cookie["path"] : "",
+                    isset($cookie["expires"]) ? $cookie["expires"] : false);
+        }
+    }
+    
+    /**
+     *    Basic HTTP response.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleHttpResponse extends SimpleStickyError {
+        var $_url;
+        var $_encoding;
+        var $_sent;
+        var $_content;
+        var $_headers;
+        
+        /**
+         *    Constructor. Reads and parses the incoming
+         *    content and headers.
+         *    @param SimpleSocket $socket   Network connection to fetch
+         *                                  response text from.
+         *    @param SimpleUrl $url         Resource name.
+         *    @param mixed $encoding        Record of content sent.
+         *    @access public
+         */
+        function SimpleHttpResponse(&$socket, $url, $encoding) {
+            $this->SimpleStickyError();
+            $this->_url = $url;
+            $this->_encoding = $encoding;
+            $this->_sent = $socket->getSent();
+            $this->_content = false;
+            $raw = $this->_readAll($socket);
+            if ($socket->isError()) {
+                $this->_setError('Error reading socket [' . $socket->getError() . ']');
+                return;
+            }
+            $this->_parse($raw);
+        }
+        
+        /**
+         *    Splits up the headers and the rest of the content.
+         *    @param string $raw    Content to parse.
+         *    @access private
+         */
+        function _parse($raw) {
+            if (! $raw) {
+                $this->_setError('Nothing fetched');
+                $this->_headers = &new SimpleHttpHeaders('');
+            } elseif (! strstr($raw, "\r\n\r\n")) {
+                $this->_setError('Could not split headers from content');
+                $this->_headers = &new SimpleHttpHeaders($raw);
+            } else {
+                list($headers, $this->_content) = split("\r\n\r\n", $raw, 2);
+                $this->_headers = &new SimpleHttpHeaders($headers);
+            }
+        }
+        
+        /**
+         *    Original request method.
+         *    @return string        GET, POST or HEAD.
+         *    @access public
+         */
+        function getMethod() {
+            return $this->_encoding->getMethod();
+        }
+        
+        /**
+         *    Resource name.
+         *    @return SimpleUrl        Current url.
+         *    @access public
+         */
+        function getUrl() {
+            return $this->_url;
+        }
+        
+        /**
+         *    Original request data.
+         *    @return mixed              Sent content.
+         *    @access public
+         */
+        function getRequestData() {
+            return $this->_encoding;
+        }
+        
+        /**
+         *    Raw request that was sent down the wire.
+         *    @return string        Bytes actually sent.
+         *    @access public
+         */
+        function getSent() {
+            return $this->_sent;
+        }
+        
+        /**
+         *    Accessor for the content after the last
+         *    header line.
+         *    @return string           All content.
+         *    @access public
+         */
+        function getContent() {
+            return $this->_content;
+        }
+        
+        /**
+         *    Accessor for header block. The response is the
+         *    combination of this and the content.
+         *    @return SimpleHeaders        Wrapped header block.
+         *    @access public
+         */
+        function getHeaders() {
+            return $this->_headers;
+        }
+        
+        /**
+         *    Accessor for any new cookies.
+         *    @return array       List of new cookies.
+         *    @access public
+         */
+        function getNewCookies() {
+            return $this->_headers->getNewCookies();
+        }
+        
+        /**
+         *    Reads the whole of the socket output into a
+         *    single string.
+         *    @param SimpleSocket $socket  Unread socket.
+         *    @return string               Raw output if successful
+         *                                 else false.
+         *    @access private
+         */
+        function _readAll(&$socket) {
+            $all = '';
+            while (! $this->_isLastPacket($next = $socket->read())) {
+                $all .= $next;
+            }
+            return $all;
+        }
+        
+        /**
+         *    Test to see if the packet from the socket is the
+         *    last one.
+         *    @param string $packet    Chunk to interpret.
+         *    @return boolean          True if empty or EOF.
+         *    @access private
+         */
+        function _isLastPacket($packet) {
+            if (is_string($packet)) {
+                return $packet === '';
+            }
+            return ! $packet;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/invoker.php b/lib/simpletestlib/invoker.php
new file mode 100644 (file)
index 0000000..22867fa
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * Includes SimpleTest files and defined the root constant
+     * for dependent libraries.
+     */
+    require_once(dirname(__FILE__) . '/errors.php');
+    require_once(dirname(__FILE__) . '/compatibility.php');
+    require_once(dirname(__FILE__) . '/scorer.php');
+    require_once(dirname(__FILE__) . '/expectation.php');
+    require_once(dirname(__FILE__) . '/dumper.php');
+    if (! defined('SIMPLE_TEST')) {
+        define('SIMPLE_TEST', dirname(__FILE__) . '/');
+    }
+    /**#@-*/
+
+    /**
+     *    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/lib/simpletestlib/mock_objects.php b/lib/simpletestlib/mock_objects.php
new file mode 100644 (file)
index 0000000..ccb16df
--- /dev/null
@@ -0,0 +1,1273 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     MockObjects
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/expectation.php');
+    require_once(dirname(__FILE__) . '/simpletest.php');
+    require_once(dirname(__FILE__) . '/dumper.php');
+    if (version_compare(phpversion(), '5') >= 0) {
+        require_once(dirname(__FILE__) . '/reflection_php5.php');
+    } else {
+        require_once(dirname(__FILE__) . '/reflection_php4.php');
+    }
+    /**#@-*/
+
+    /**
+     * Default character simpletest will substitute for any value
+     */
+    if (! defined('MOCK_ANYTHING')) {
+        define('MOCK_ANYTHING', '*');
+    }
+
+    /**
+     *    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) . ']';
+        }
+    }
+
+    /**
+     *    Parameter comparison assertion.
+        *    @package SimpleTest
+        *    @subpackage MockObjects
+     */
+    class ParametersExpectation extends SimpleExpectation {
+        var $_expected;
+
+        /**
+         *    Sets the expected parameter list.
+         *    @param array $parameters  Array of parameters including
+         *                              those that are wildcarded.
+         *                              If the value is not an array
+         *                              then it is considered to match any.
+         *    @param mixed $wildcard    Any parameter matching this
+         *                              will always match.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function ParametersExpectation($expected = false, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_expected = $expected;
+        }
+
+        /**
+         *    Tests the assertion. True if correct.
+         *    @param array $parameters     Comparison values.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($parameters) {
+            if (! is_array($this->_expected)) {
+                return true;
+            }
+            if (count($this->_expected) != count($parameters)) {
+                return false;
+            }
+            for ($i = 0; $i < count($this->_expected); $i++) {
+                if (! $this->_testParameter($parameters[$i], $this->_expected[$i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         *    Tests an individual parameter.
+         *    @param mixed $parameter    Value to test.
+         *    @param mixed $expected     Comparison value.
+         *    @return boolean            True if expectation
+         *                               fulfilled.
+         *    @access private
+         */
+        function _testParameter($parameter, $expected) {
+            $comparison = $this->_coerceToExpectation($expected);
+            return $comparison->test($parameter);
+        }
+
+        /**
+         *    Returns a human readable test message.
+         *    @param array $comparison   Incoming parameter list.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($parameters) {
+            if ($this->test($parameters)) {
+                return "Expectation of " . count($this->_expected) .
+                        " arguments of [" . $this->_renderArguments($this->_expected) .
+                        "] is correct";
+            } else {
+                return $this->_describeDifference($this->_expected, $parameters);
+            }
+        }
+
+        /**
+         *    Message to display if expectation differs from
+         *    the parameters actually received.
+         *    @param array $expected      Expected parameters as list.
+         *    @param array $parameters    Actual parameters received.
+         *    @return string              Description of difference.
+         *    @access private
+         */
+        function _describeDifference($expected, $parameters) {
+            if (count($expected) != count($parameters)) {
+                return "Expected " . count($expected) .
+                        " arguments of [" . $this->_renderArguments($expected) .
+                        "] but got " . count($parameters) .
+                        " arguments of [" . $this->_renderArguments($parameters) . "]";
+            }
+            $messages = array();
+            for ($i = 0; $i < count($expected); $i++) {
+                $comparison = $this->_coerceToExpectation($expected[$i]);
+                if (! $comparison->test($parameters[$i])) {
+                    $messages[] = "parameter " . ($i + 1) . " with [" .
+                            $comparison->overlayMessage($parameters[$i]) . "]";
+                }
+            }
+            return "Parameter expectation differs at " . implode(" and ", $messages);
+        }
+
+        /**
+         *    Creates an identical expectation if the
+         *    object/value is not already some type
+         *    of expectation.
+         *    @param mixed $expected      Expected value.
+         *    @return SimpleExpectation   Expectation object.
+         *    @access private
+         */
+        function _coerceToExpectation($expected) {
+            if (SimpleExpectation::isExpectation($expected)) {
+                return $expected;
+            }
+            return new IdenticalExpectation($expected);
+        }
+
+        /**
+         *    Renders the argument list as a string for
+         *    messages.
+         *    @param array $args    Incoming arguments.
+         *    @return string        Simple description of type and value.
+         *    @access private
+         */
+        function _renderArguments($args) {
+            $descriptions = array();
+            if (is_array($args)) {
+                foreach ($args as $arg) {
+                    $dumper = &new SimpleDumper();
+                    $descriptions[] = $dumper->describeValue($arg);
+                }
+            }
+            return implode(', ', $descriptions);
+        }
+    }
+
+    /**
+     *    Confirms that the number of calls on a method is as expected.
+     */
+    class CallCountExpectation extends SimpleExpectation {
+        var $_method;
+        var $_count;
+
+        /**
+         *    Stashes the method and expected count for later
+         *    reporting.
+         *    @param string $method    Name of method to confirm against.
+         *    @param integer $count    Expected number of calls.
+         *    @param string $message   Custom error message.
+         */
+        function CallCountExpectation($method, $count, $message = '%s') {
+            $this->_method = $method;
+            $this->_count = $count;
+            $this->SimpleExpectation($message);
+        }
+
+        /**
+         *    Tests the assertion. True if correct.
+         *    @param integer $compare     Measured call count.
+         *    @return boolean             True if expected.
+         *    @access public
+         */
+        function test($compare) {
+            return ($this->_count == $compare);
+        }
+
+        /**
+         *    Reports the comparison.
+         *    @param integer $compare     Measured call count.
+         *    @return string              Message to show.
+         *    @access public
+         */
+        function testMessage($compare) {
+            return 'Expected call count for [' . $this->_method .
+                    '] was [' . $this->_count .
+                    '] got [' . $compare . ']';
+        }
+    }
+
+    /**
+     *    Confirms that the number of calls on a method is as expected.
+     */
+    class MinimumCallCountExpectation extends SimpleExpectation {
+        var $_method;
+        var $_count;
+
+        /**
+         *    Stashes the method and expected count for later
+         *    reporting.
+         *    @param string $method    Name of method to confirm against.
+         *    @param integer $count    Minimum number of calls.
+         *    @param string $message   Custom error message.
+         */
+        function MinimumCallCountExpectation($method, $count, $message = '%s') {
+            $this->_method = $method;
+            $this->_count = $count;
+            $this->SimpleExpectation($message);
+        }
+
+        /**
+         *    Tests the assertion. True if correct.
+         *    @param integer $compare     Measured call count.
+         *    @return boolean             True if enough.
+         *    @access public
+         */
+        function test($compare) {
+            return ($this->_count <= $compare);
+        }
+
+        /**
+         *    Reports the comparison.
+         *    @param integer $compare     Measured call count.
+         *    @return string              Message to show.
+         *    @access public
+         */
+        function testMessage($compare) {
+            return 'Minimum call count for [' . $this->_method .
+                    '] was [' . $this->_count .
+                    '] got [' . $compare . ']';
+        }
+    }
+
+    /**
+     *    Confirms that the number of calls on a method is as expected.
+     */
+    class MaximumCallCountExpectation extends SimpleExpectation {
+        var $_method;
+        var $_count;
+
+        /**
+         *    Stashes the method and expected count for later
+         *    reporting.
+         *    @param string $method    Name of method to confirm against.
+         *    @param integer $count    Minimum number of calls.
+         *    @param string $message   Custom error message.
+         */
+        function MaximumCallCountExpectation($method, $count, $message = '%s') {
+            $this->_method = $method;
+            $this->_count = $count;
+            $this->SimpleExpectation($message);
+        }
+
+        /**
+         *    Tests the assertion. True if correct.
+         *    @param integer $compare     Measured call count.
+         *    @return boolean             True if not over.
+         *    @access public
+         */
+        function test($compare) {
+            return ($this->_count >= $compare);
+        }
+
+        /**
+         *    Reports the comparison.
+         *    @param integer $compare     Measured call count.
+         *    @return string              Message to show.
+         *    @access public
+         */
+        function testMessage($compare) {
+            return 'Maximum call count for [' . $this->_method .
+                    '] was [' . $this->_count .
+                    '] got [' . $compare . ']';
+        }
+    }
+
+    /**
+     *    Retrieves values and references by searching the
+     *    parameter lists until a match is found.
+        *    @package SimpleTest
+        *    @subpackage MockObjects
+     */
+    class CallMap {
+        var $_map;
+
+        /**
+         *    Creates an empty call map.
+         *    @access public
+         */
+        function CallMap() {
+            $this->_map = array();
+        }
+
+        /**
+         *    Stashes a value against a method call.
+         *    @param array $parameters    Arguments including wildcards.
+         *    @param mixed $value         Value copied into the map.
+         *    @access public
+         */
+        function addValue($parameters, $value) {
+            $this->addReference($parameters, $value);
+        }
+
+        /**
+         *    Stashes a reference against a method call.
+         *    @param array $parameters    Array of arguments (including wildcards).
+         *    @param mixed $reference     Array reference placed in the map.
+         *    @access public
+         */
+        function addReference($parameters, &$reference) {
+            $place = count($this->_map);
+            $this->_map[$place] = array();
+            $this->_map[$place]["params"] = new ParametersExpectation($parameters);
+            $this->_map[$place]["content"] = &$reference;
+        }
+
+        /**
+         *    Searches the call list for a matching parameter
+         *    set. Returned by reference.
+         *    @param array $parameters    Parameters to search by
+         *                                without wildcards.
+         *    @return object              Object held in the first matching
+         *                                slot, otherwise null.
+         *    @access public
+         */
+        function &findFirstMatch($parameters) {
+            $slot = $this->_findFirstSlot($parameters);
+            if (!isset($slot)) {
+                $null = null;
+                return $null;
+            }
+            return $slot["content"];
+        }
+
+        /**
+         *    Searches the call list for a matching parameter
+         *    set. True if successful.
+         *    @param array $parameters    Parameters to search by
+         *                                without wildcards.
+         *    @return boolean             True if a match is present.
+         *    @access public
+         */
+        function isMatch($parameters) {
+            return ($this->_findFirstSlot($parameters) != null);
+        }
+
+        /**
+         *    Searches the map for a matching item.
+         *    @param array $parameters    Parameters to search by
+         *                                without wildcards.
+         *    @return array               Reference to slot or null.
+         *    @access private
+         */
+        function &_findFirstSlot($parameters) {
+            $count = count($this->_map);
+            for ($i = 0; $i < $count; $i++) {
+                if ($this->_map[$i]["params"]->test($parameters)) {
+                    return $this->_map[$i];
+                }
+            }
+            $null = null;
+            return $null;
+        }
+    }
+
+    /**
+     *    An empty collection of methods that can have their
+     *    return values set and expectations made of the
+     *    calls upon them. The mock will assert the
+     *    expectations against it's attached test case in
+     *    addition to the server stub behaviour.
+        *    @package SimpleTest
+        *    @subpackage MockObjects
+     */
+    class SimpleMock {
+        var $_wildcard = MOCK_ANYTHING;
+        var $_is_strict = true;
+        var $_returns;
+        var $_return_sequence;
+        var $_call_counts;
+        var $_expected_counts;
+        var $_max_counts;
+        var $_expected_args;
+        var $_expected_args_at;
+
+        /**
+         *    Creates an empty return list and expectation list.
+         *    All call counts are set to zero.
+         *    @param SimpleTestCase $test    Test case to test expectations in.
+         *    @param mixed $wildcard         Parameter matching wildcard.
+         *    @param boolean $is_strict      Enables method name checks on
+         *                                   expectations.
+         */
+        function SimpleMock() {
+            $this->_returns = array();
+            $this->_return_sequence = array();
+            $this->_call_counts = array();
+            $test = &$this->_getCurrentTestCase();
+            $test->tell($this);
+            $this->_expected_counts = array();
+            $this->_max_counts = array();
+            $this->_expected_args = array();
+            $this->_expected_args_at = array();
+        }
+
+        /**
+         *    Disables a name check when setting expectations.
+         *    This hack is needed for the partial mocks.
+         *    @access public
+         */
+        function disableExpectationNameChecks() {
+            $this->_is_strict = false;
+        }
+
+        /**
+         *    Changes the default wildcard object.
+         *    @param mixed $wildcard         Parameter matching wildcard.
+         *    @access public
+         */
+        function setWildcard($wildcard) {
+            $this->_wildcard = $wildcard;
+        }
+
+        /**
+         *    Finds currently running test.
+         *    @return SimpeTestCase    Current test case.
+         *    @access protected
+         */
+        function &_getCurrentTestCase() {
+            return SimpleTest::getCurrent();
+        }
+
+        /**
+         *    Die if bad arguments array is passed
+         *    @param mixed $args     The arguments value to be checked.
+         *    @param string $task    Description of task attempt.
+         *    @return boolean        Valid arguments
+         *    @access private
+         */
+        function _checkArgumentsIsArray($args, $task) {
+               if (! is_array($args)) {
+                       trigger_error(
+                               "Cannot $task as \$args parameter is not an array",
+                               E_USER_ERROR);
+               }
+        }
+
+        /**
+         *    Triggers a PHP error if the method is not part
+         *    of this object.
+         *    @param string $method        Name of method.
+         *    @param string $task          Description of task attempt.
+         *    @access protected
+         */
+        function _dieOnNoMethod($method, $task) {
+            if ($this->_is_strict && ! method_exists($this, $method)) {
+                trigger_error(
+                        "Cannot $task as no ${method}() in class " . get_class($this),
+                        E_USER_ERROR);
+            }
+        }
+
+        /**
+         *    Replaces wildcard matches with wildcard
+         *    expectations in the argument list.
+         *    @param array $args      Raw argument list.
+         *    @return array           Argument list with
+         *                            expectations.
+         *    @access private
+         */
+        function _replaceWildcards($args) {
+            if ($args === false) {
+                return false;
+            }
+            for ($i = 0; $i < count($args); $i++) {
+                if ($args[$i] === $this->_wildcard) {
+                    $args[$i] = new AnythingExpectation();
+                }
+            }
+            return $args;
+        }
+
+        /**
+         *    Adds one to the call count of a method.
+         *    @param string $method        Method called.
+         *    @param array $args           Arguments as an array.
+         *    @access protected
+         */
+        function _addCall($method, $args) {
+            if (!isset($this->_call_counts[$method])) {
+                $this->_call_counts[$method] = 0;
+            }
+            $this->_call_counts[$method]++;
+        }
+
+        /**
+         *    Fetches the call count of a method so far.
+         *    @param string $method        Method name called.
+         *    @return                      Number of calls so far.
+         *    @access public
+         */
+        function getCallCount($method) {
+            $this->_dieOnNoMethod($method, "get call count");
+            $method = strtolower($method);
+            if (! isset($this->_call_counts[$method])) {
+                return 0;
+            }
+            return $this->_call_counts[$method];
+        }
+
+        /**
+         *    Sets a return for a parameter list that will
+         *    be passed by value for all calls to this method.
+         *    @param string $method       Method name.
+         *    @param mixed $value         Result of call passed by value.
+         *    @param array $args          List of parameters to match
+         *                                including wildcards.
+         *    @access public
+         */
+        function setReturnValue($method, $value, $args = false) {
+            $this->_dieOnNoMethod($method, "set return value");
+            $args = $this->_replaceWildcards($args);
+            $method = strtolower($method);
+            if (! isset($this->_returns[$method])) {
+                $this->_returns[$method] = new CallMap();
+            }
+            $this->_returns[$method]->addValue($args, $value);
+        }
+
+        /**
+         *    Sets a return for a parameter list that will
+         *    be passed by value only when the required call count
+         *    is reached.
+         *    @param integer $timing   Number of calls in the future
+         *                             to which the result applies. If
+         *                             not set then all calls will return
+         *                             the value.
+         *    @param string $method    Method name.
+         *    @param mixed $value      Result of call passed by value.
+         *    @param array $args       List of parameters to match
+         *                             including wildcards.
+         *    @access public
+         */
+        function setReturnValueAt($timing, $method, $value, $args = false) {
+            $this->_dieOnNoMethod($method, "set return value sequence");
+            $args = $this->_replaceWildcards($args);
+            $method = strtolower($method);
+            if (! isset($this->_return_sequence[$method])) {
+                $this->_return_sequence[$method] = array();
+            }
+            if (! isset($this->_return_sequence[$method][$timing])) {
+                $this->_return_sequence[$method][$timing] = new CallMap();
+            }
+            $this->_return_sequence[$method][$timing]->addValue($args, $value);
+        }
+
+        /**
+         *    Sets a return for a parameter list that will
+         *    be passed by reference for all calls.
+         *    @param string $method       Method name.
+         *    @param mixed $reference     Result of the call will be this object.
+         *    @param array $args          List of parameters to match
+         *                                including wildcards.
+         *    @access public
+         */
+        function setReturnReference($method, &$reference, $args = false) {
+            $this->_dieOnNoMethod($method, "set return reference");
+            $args = $this->_replaceWildcards($args);
+            $method = strtolower($method);
+            if (! isset($this->_returns[$method])) {
+                $this->_returns[$method] = new CallMap();
+            }
+            $this->_returns[$method]->addReference($args, $reference);
+        }
+
+        /**
+         *    Sets a return for a parameter list that will
+         *    be passed by value only when the required call count
+         *    is reached.
+         *    @param integer $timing    Number of calls in the future
+         *                              to which the result applies. If
+         *                              not set then all calls will return
+         *                              the value.
+         *    @param string $method     Method name.
+         *    @param mixed $reference   Result of the call will be this object.
+         *    @param array $args        List of parameters to match
+         *                              including wildcards.
+         *    @access public
+         */
+        function setReturnReferenceAt($timing, $method, &$reference, $args = false) {
+            $this->_dieOnNoMethod($method, "set return reference sequence");
+            $args = $this->_replaceWildcards($args);
+            $method = strtolower($method);
+            if (! isset($this->_return_sequence[$method])) {
+                $this->_return_sequence[$method] = array();
+            }
+            if (! isset($this->_return_sequence[$method][$timing])) {
+                $this->_return_sequence[$method][$timing] = new CallMap();
+            }
+            $this->_return_sequence[$method][$timing]->addReference($args, $reference);
+        }
+
+        /**
+         *    Sets up an expected call with a set of
+         *    expected parameters in that call. All
+         *    calls will be compared to these expectations
+         *    regardless of when the call is made.
+         *    @param string $method        Method call to test.
+         *    @param array $args           Expected parameters for the call
+         *                                 including wildcards.
+         *    @param string $message       Overridden message.
+         *    @access public
+         */
+        function expect($method, $args, $message = '%s') {
+            $this->_dieOnNoMethod($method, 'set expected arguments');
+            $this->_checkArgumentsIsArray($args, 'set expected arguments');
+            $args = $this->_replaceWildcards($args);
+            $message .= Mock::getExpectationLine();
+            $this->_expected_args[strtolower($method)] =
+                    new ParametersExpectation($args, $message);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function expectArguments($method, $args, $message = '%s') {
+            return $this->expect($method, $args, $message);
+        }
+
+        /**
+         *    Sets up an expected call with a set of
+         *    expected parameters in that call. The
+         *    expected call count will be adjusted if it
+         *    is set too low to reach this call.
+         *    @param integer $timing    Number of calls in the future at
+         *                              which to test. Next call is 0.
+         *    @param string $method     Method call to test.
+         *    @param array $args        Expected parameters for the call
+         *                              including wildcards.
+         *    @param string $message    Overridden message.
+         *    @access public
+         */
+        function expectAt($timing, $method, $args, $message = '%s') {
+            $this->_dieOnNoMethod($method, 'set expected arguments at time');
+            $this->_checkArgumentsIsArray($args, 'set expected arguments at time');
+            $args = $this->_replaceWildcards($args);
+            if (! isset($this->_expected_args_at[$timing])) {
+                $this->_expected_args_at[$timing] = array();
+            }
+            $method = strtolower($method);
+            $message .= Mock::getExpectationLine();
+            $this->_expected_args_at[$timing][$method] =
+                    new ParametersExpectation($args, $message);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function expectArgumentsAt($timing, $method, $args, $message = '%s') {
+            return $this->expectAt($timing, $method, $args, $message);
+        }
+
+        /**
+         *    Sets an expectation for the number of times
+         *    a method will be called. The tally method
+         *    is used to check this.
+         *    @param string $method        Method call to test.
+         *    @param integer $count        Number of times it should
+         *                                 have been called at tally.
+         *    @param string $message       Overridden message.
+         *    @access public
+         */
+        function expectCallCount($method, $count, $message = '%s') {
+            $this->_dieOnNoMethod($method, 'set expected call count');
+            $message .= Mock::getExpectationLine();
+            $this->_expected_counts[strtolower($method)] =
+                    new CallCountExpectation($method, $count, $message);
+        }
+
+        /**
+         *    Sets the number of times a method may be called
+         *    before a test failure is triggered.
+         *    @param string $method        Method call to test.
+         *    @param integer $count        Most number of times it should
+         *                                 have been called.
+         *    @param string $message       Overridden message.
+         *    @access public
+         */
+        function expectMaximumCallCount($method, $count, $message = '%s') {
+            $this->_dieOnNoMethod($method, 'set maximum call count');
+            $message .= Mock::getExpectationLine();
+            $this->_max_counts[strtolower($method)] =
+                    new MaximumCallCountExpectation($method, $count, $message);
+        }
+
+        /**
+         *    Sets the number of times to call a method to prevent
+         *    a failure on the tally.
+         *    @param string $method      Method call to test.
+         *    @param integer $count      Least number of times it should
+         *                               have been called.
+         *    @param string $message     Overridden message.
+         *    @access public
+         */
+        function expectMinimumCallCount($method, $count, $message = '%s') {
+            $this->_dieOnNoMethod($method, 'set minimum call count');
+            $message .= Mock::getExpectationLine();
+            $this->_expected_counts[strtolower($method)] =
+                    new MinimumCallCountExpectation($method, $count, $message);
+        }
+
+        /**
+         *    Convenience method for barring a method
+         *    call.
+         *    @param string $method        Method call to ban.
+         *    @param string $message       Overridden message.
+         *    @access public
+         */
+        function expectNever($method, $message = '%s') {
+            $this->expectMaximumCallCount($method, 0, $message);
+        }
+
+        /**
+         *    Convenience method for a single method
+         *    call.
+         *    @param string $method     Method call to track.
+         *    @param array $args        Expected argument list or
+         *                              false for any arguments.
+         *    @param string $message    Overridden message.
+         *    @access public
+         */
+        function expectOnce($method, $args = false, $message = '%s') {
+            $this->expectCallCount($method, 1, $message);
+            if ($args !== false) {
+                $this->expectArguments($method, $args, $message);
+            }
+        }
+
+        /**
+         *    Convenience method for requiring a method
+         *    call.
+         *    @param string $method       Method call to track.
+         *    @param array $args          Expected argument list or
+         *                                false for any arguments.
+         *    @param string $message      Overridden message.
+         *    @access public
+         */
+        function expectAtLeastOnce($method, $args = false, $message = '%s') {
+            $this->expectMinimumCallCount($method, 1, $message);
+            if ($args !== false) {
+                $this->expectArguments($method, $args, $message);
+            }
+        }
+
+        /**
+         *    @deprecated
+         */
+        function tally() {
+        }
+
+        /**
+         *    Receives event from unit test that the current
+         *    test method has finished. Totals up the call
+         *    counts and triggers a test assertion if a test
+         *    is present for expected call counts.
+         *    @param string $method    Current method name.
+         *    @access public
+         */
+        function atTestEnd($method) {
+            foreach ($this->_expected_counts as $method => $expectation) {
+                $this->_assertTrue(
+                        $expectation->test($this->getCallCount($method)),
+                        $expectation->overlayMessage($this->getCallCount($method)));
+            }
+            foreach ($this->_max_counts as $method => $expectation) {
+                if ($expectation->test($this->getCallCount($method))) {
+                    $this->_assertTrue(
+                            true,
+                            $expectation->overlayMessage($this->getCallCount($method)));
+                }
+            }
+        }
+
+        /**
+         *    Returns the expected value for the method name
+         *    and checks expectations. Will generate any
+         *    test assertions as a result of expectations
+         *    if there is a test present.
+         *    @param string $method       Name of method to simulate.
+         *    @param array $args          Arguments as an array.
+         *    @return mixed               Stored return.
+         *    @access private
+         */
+        function &_invoke($method, $args) {
+            $method = strtolower($method);
+            $step = $this->getCallCount($method);
+            $this->_addCall($method, $args);
+            $this->_checkExpectations($method, $args, $step);
+            $result = &$this->_getReturn($method, $args, $step);
+            return $result;
+        }
+        /**
+         *    Finds the return value matching the incoming
+         *    arguments. If there is no matching value found
+         *    then an error is triggered.
+         *    @param string $method      Method name.
+         *    @param array $args         Calling arguments.
+         *    @param integer $step       Current position in the
+         *                               call history.
+         *    @return mixed              Stored return.
+         *    @access protected
+         */
+        function &_getReturn($method, $args, $step) {
+            if (isset($this->_return_sequence[$method][$step])) {
+                if ($this->_return_sequence[$method][$step]->isMatch($args)) {
+                    $result = &$this->_return_sequence[$method][$step]->findFirstMatch($args);
+                    return $result;
+                }
+            }
+            if (isset($this->_returns[$method])) {
+                $result = &$this->_returns[$method]->findFirstMatch($args);
+                return $result;
+            }
+            $null = null;
+            return $null;
+        }
+
+        /**
+         *    Tests the arguments against expectations.
+         *    @param string $method        Method to check.
+         *    @param array $args           Argument list to match.
+         *    @param integer $timing       The position of this call
+         *                                 in the call history.
+         *    @access private
+         */
+        function _checkExpectations($method, $args, $timing) {
+            if (isset($this->_max_counts[$method])) {
+                if (! $this->_max_counts[$method]->test($timing + 1)) {
+                    $this->_assertTrue(
+                            false,
+                            $this->_max_counts[$method]->overlayMessage($timing + 1));
+                }
+            }
+            if (isset($this->_expected_args_at[$timing][$method])) {
+                $this->_assertTrue(
+                        $this->_expected_args_at[$timing][$method]->test($args),
+                        "Mock method [$method] at [$timing] -> " .
+                                $this->_expected_args_at[$timing][$method]->overlayMessage($args));
+            } elseif (isset($this->_expected_args[$method])) {
+                $this->_assertTrue(
+                        $this->_expected_args[$method]->test($args),
+                        "Mock method [$method] -> " . $this->_expected_args[$method]->overlayMessage($args));
+            }
+        }
+
+        /**
+         *    Triggers an assertion on the held test case.
+         *    Should be overridden when using another test
+         *    framework other than the SimpleTest one if the
+         *    assertion method has a different name.
+         *    @param boolean $assertion     True will pass.
+         *    @param string $message        Message that will go with
+         *                                  the test event.
+         *    @access protected
+         */
+        function _assertTrue($assertion, $message) {
+            $test = &$this->_getCurrentTestCase();
+            $test->assertTrue($assertion, $message);
+        }
+    }
+
+    /**
+     *    Static methods only service class for code generation of
+     *    mock objects.
+        *    @package SimpleTest
+        *    @subpackage MockObjects
+     */
+    class Mock {
+
+        /**
+         *    Factory for mock object classes.
+         *    @access public
+         */
+        function Mock() {
+            trigger_error('Mock factory methods are class only.');
+        }
+
+        /**
+         *    Clones a class' interface and creates a mock version
+         *    that can have return values and expectations set.
+         *    @param string $class         Class to clone.
+         *    @param string $mock_class    New class name. Default is
+         *                                 the old name with "Mock"
+         *                                 prepended.
+         *    @param array $methods        Additional methods to add beyond
+         *                                 those in th cloned class. Use this
+         *                                 to emulate the dynamic addition of
+         *                                 methods in the cloned class or when
+         *                                 the class hasn't been written yet.
+         *    @static
+         *    @access public
+         */
+        function generate($class, $mock_class = false, $methods = false) {
+            $generator = new MockGenerator($class, $mock_class);
+            return $generator->generate($methods);
+        }
+
+        /**
+         *    Generates a version of a class with selected
+         *    methods mocked only. Inherits the old class
+         *    and chains the mock methods of an aggregated
+         *    mock object.
+         *    @param string $class            Class to clone.
+         *    @param string $mock_class       New class name.
+         *    @param array $methods           Methods to be overridden
+         *                                    with mock versions.
+         *    @static
+         *    @access public
+         */
+        function generatePartial($class, $mock_class, $methods) {
+            $generator = new MockGenerator($class, $mock_class);
+            return $generator->generatePartial($methods);
+        }
+
+        /**
+         *    Uses a stack trace to find the line of an assertion.
+         *    @param array $stack      Stack frames top most first. Only
+         *                             needed if not using the PHP
+         *                             backtrace function.
+         *    @return string           Location of first expect*
+         *                             method embedded in format string.
+         *    @access public
+         *    @static
+         */
+        function getExpectationLine($stack = false) {
+            if ($stack === false) {
+                $stack = SimpleTestCompatibility::getStackTrace();
+            }
+            return SimpleDumper::getFormattedAssertionLine($stack);
+        }
+    }
+
+    /**
+     *    @deprecated
+     */
+    class Stub extends Mock {
+    }
+
+    /**
+     *    Service class for code generation of mock objects.
+        *    @package SimpleTest
+        *    @subpackage MockObjects
+     */
+    class MockGenerator {
+        var $_class;
+        var $_mock_class;
+        var $_mock_base;
+        var $_reflection;
+
+        function MockGenerator($class, $mock_class) {
+            $this->_class = $class;
+            $this->_mock_class = $mock_class;
+            $this->_mock_base = SimpleTest::getMockBaseClass();
+            $this->_reflection = new SimpleReflection($this->_class);
+        }
+
+        /**
+         *    Clones a class' interface and creates a mock version
+         *    that can have return values and expectations set.
+         *    @param array $methods        Additional methods to add beyond
+         *                                 those in th cloned class. Use this
+         *                                 to emulate the dynamic addition of
+         *                                 methods in the cloned class or when
+         *                                 the class hasn't been written yet.
+         *    @access public
+         */
+        function generate($methods) {
+            if (! $this->_reflection->classOrInterfaceExists()) {
+                return false;
+            }
+            if (! $this->_mock_class) {
+                $this->_mock_class = 'Mock' . $this->_class;
+            }
+            $mock_reflection = new SimpleReflection($this->_mock_class);
+            if ($mock_reflection->classExistsSansAutoload()) {
+                return false;
+            }
+            return eval(
+                    $this->_createClassCode($methods ? $methods : array()) .
+                    " return true;");
+        }
+
+        /**
+         *    Generates a version of a class with selected
+         *    methods mocked only. Inherits the old class
+         *    and chains the mock methods of an aggregated
+         *    mock object.
+         *    @param array $methods           Methods to be overridden
+         *                                    with mock versions.
+         *    @access public
+         */
+        function generatePartial($methods) {
+            if (! $this->_reflection->classExists($this->_class)) {
+                return false;
+            }
+            $mock_reflection = new SimpleReflection($this->_mock_class);
+            if ($mock_reflection->classExistsSansAutoload()) {
+                trigger_error("Partial mock class [$mock_class] already exists");
+                return false;
+            }
+            return eval($this->_extendClassCode($methods));
+        }
+
+        /**
+         *    The new mock class code as a string.
+         *    @param array $methods          Additional methods.
+         *    @return string                 Code for new mock class.
+         *    @access private
+         */
+        function _createClassCode($methods) {
+            $implements = '';
+            $interfaces = $this->_reflection->getInterfaces();
+            if (function_exists('spl_classes')) {
+               $interfaces = array_diff($interfaces, array('Traversable'));
+            }
+            if (count($interfaces) > 0) {
+               $implements = 'implements ' . implode(', ', $interfaces);
+            }
+            $code = "class " . $this->_mock_class . " extends " . $this->_mock_base . " $implements {\n";
+            $code .= "    function " . $this->_mock_class . "() {\n";
+            $code .= "        \$this->" . $this->_mock_base . "();\n";
+            $code .= "    }\n";
+            $code .= $this->_createHandlerCode($methods);
+            $code .= "}\n";
+            return $code;
+        }
+
+        /**
+         *    The extension class code as a string. The class
+         *    composites a mock object and chains mocked methods
+         *    to it.
+         *    @param array  $methods       Mocked methods.
+         *    @return string               Code for a new class.
+         *    @access private
+         */
+        function _extendClassCode($methods) {
+            $code  = "class " . $this->_mock_class . " extends " . $this->_class . " {\n";
+            $code .= "    var \$_mock;\n";
+            $code .= $this->_addMethodList($methods);
+            $code .= "\n";
+            $code .= "    function " . $this->_mock_class . "() {\n";
+            $code .= "        \$this->_mock = &new " . $this->_mock_base . "();\n";
+            $code .= "        \$this->_mock->disableExpectationNameChecks();\n";
+            $code .= "    }\n";
+            $code .= $this->_chainMockReturns();
+            $code .= $this->_chainMockExpectations();
+            $code .= $this->_overrideMethods($methods);
+            $code .= "}\n";
+            return $code;
+        }
+
+        /**
+         *    Creates code within a class to generate replaced
+         *    methods. All methods call the _invoke() handler
+         *    with the method name and the arguments in an
+         *    array.
+         *    @param array $methods    Additional methods.
+         *    @access private
+         */
+        function _createHandlerCode($methods) {
+               $code = '';
+            $methods = array_merge($methods, $this->_reflection->getMethods());
+            foreach ($methods as $method) {
+                if ($this->_isConstructor($method)) {
+                    continue;
+                }
+                $mock_reflection = new SimpleReflection($this->_mock_base);
+                if (in_array($method, $mock_reflection->getMethods())) {
+                    continue;
+                }
+                $code .= "    " . $this->_reflection->getSignature($method) . " {\n";
+                $code .= "        \$args = func_get_args();\n";
+                $code .= "        \$result = &\$this->_invoke(\"$method\", \$args);\n";
+                $code .= "        return \$result;\n";
+                $code .= "    }\n";
+            }
+            return $code;
+        }
+
+        /**
+         *    Tests to see if a special PHP method is about to
+         *    be stubbed by mistake.
+         *    @param string $method    Method name.
+         *    @return boolean          True if special.
+         *    @access private
+         */
+        function _isConstructor($method) {
+            return in_array(
+                    strtolower($method),
+                    array('__construct', '__destruct', '__clone'));
+        }
+
+        /**
+         *    Creates a list of mocked methods for error checking.
+         *    @param array $methods       Mocked methods.
+         *    @return string              Code for a method list.
+         *    @access private
+         */
+        function _addMethodList($methods) {
+            return "    var \$_mocked_methods = array('" . implode("', '", $methods) . "');\n";
+        }
+
+        /**
+         *    Creates code to abandon the expectation if not mocked.
+         *    @param string $alias       Parameter name of method name.
+         *    @return string             Code for bail out.
+         *    @access private
+         */
+        function _bailOutIfNotMocked($alias) {
+            $code  = "        if (! in_array($alias, \$this->_mocked_methods)) {\n";
+            $code .= "            trigger_error(\"Method [$alias] is not mocked\");\n";
+            $code .= "            \$null = null;\n";
+            $code .= "            return \$null;\n";
+            $code .= "        }\n";
+            return $code;
+        }
+
+        /**
+         *    Creates source code for chaining to the composited
+         *    mock object.
+         *    @return string           Code for mock set up.
+         *    @access private
+         */
+        function _chainMockReturns() {
+            $code  = "    function setReturnValue(\$method, \$value, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->setReturnValue(\$method, \$value, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function setReturnValueAt(\$timing, \$method, \$value, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->setReturnValueAt(\$timing, \$method, \$value, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function setReturnReference(\$method, &\$ref, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->setReturnReference(\$method, \$ref, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function setReturnReferenceAt(\$timing, \$method, &\$ref, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->setReturnReferenceAt(\$timing, \$method, \$ref, \$args);\n";
+            $code .= "    }\n";
+            return $code;
+        }
+
+        /**
+         *    Creates source code for chaining to an aggregated
+         *    mock object.
+         *    @return string                 Code for expectations.
+         *    @access private
+         */
+        function _chainMockExpectations() {
+            $code  = "    function expect(\$method, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expect(\$method, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function expectArguments(\$method, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectArguments(\$method, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function expectAt(\$timing, \$method, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectArgumentsAt(\$timing, \$method, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function expectArgumentsAt(\$timing, \$method, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectArgumentsAt(\$timing, \$method, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function expectCallCount(\$method, \$count) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectCallCount(\$method, \$count);\n";
+            $code .= "    }\n";
+            $code .= "    function expectMaximumCallCount(\$method, \$count) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectMaximumCallCount(\$method, \$count);\n";
+            $code .= "    }\n";
+            $code .= "    function expectMinimumCallCount(\$method, \$count) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectMinimumCallCount(\$method, \$count);\n";
+            $code .= "    }\n";
+            $code .= "    function expectNever(\$method) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectNever(\$method);\n";
+            $code .= "    }\n";
+            $code .= "    function expectOnce(\$method, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectOnce(\$method, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function expectAtLeastOnce(\$method, \$args = false) {\n";
+            $code .= $this->_bailOutIfNotMocked("\$method");
+            $code .= "        \$this->_mock->expectAtLeastOnce(\$method, \$args);\n";
+            $code .= "    }\n";
+            $code .= "    function tally() {\n";
+            $code .= "        \$this->_mock->tally();\n";
+            $code .= "    }\n";
+            return $code;
+        }
+
+        /**
+         *    Creates source code to override a list of methods
+         *    with mock versions.
+         *    @param array $methods    Methods to be overridden
+         *                             with mock versions.
+         *    @return string           Code for overridden chains.
+         *    @access private
+         */
+        function _overrideMethods($methods) {
+            $code = "";
+            foreach ($methods as $method) {
+                $code .= "    " . $this->_reflection->getSignature($method) . " {\n";
+                $code .= "        \$args = func_get_args();\n";
+                $code .= "        \$result = &\$this->_mock->_invoke(\"$method\", \$args);\n";
+                $code .= "        return \$result;\n";
+                $code .= "    }\n";
+            }
+            return $code;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/page.php b/lib/simpletestlib/page.php
new file mode 100644 (file)
index 0000000..ee3cd05
--- /dev/null
@@ -0,0 +1,975 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/http.php');
+    require_once(dirname(__FILE__) . '/parser.php');
+    require_once(dirname(__FILE__) . '/tag.php');
+    require_once(dirname(__FILE__) . '/form.php');
+    require_once(dirname(__FILE__) . '/selector.php');
+    /**#@-*/
+
+    /**
+     *    Creates tags and widgets given HTML tag
+     *    attributes.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleTagBuilder {
+
+        /**
+         *    Factory for the tag objects. Creates the
+         *    appropriate tag object for the incoming tag name
+         *    and attributes.
+         *    @param string $name        HTML tag name.
+         *    @param hash $attributes    Element attributes.
+         *    @return SimpleTag          Tag object.
+         *    @access public
+         */
+        function createTag($name, $attributes) {
+            static $map = array(
+                    'a' => 'SimpleAnchorTag',
+                    'title' => 'SimpleTitleTag',
+                    'button' => 'SimpleButtonTag',
+                    'textarea' => 'SimpleTextAreaTag',
+                    'option' => 'SimpleOptionTag',
+                    'label' => 'SimpleLabelTag',
+                    'form' => 'SimpleFormTag',
+                    'frame' => 'SimpleFrameTag');
+            $attributes = $this->_keysToLowerCase($attributes);
+            if (array_key_exists($name, $map)) {
+                $tag_class = $map[$name];
+                return new $tag_class($attributes);
+            } elseif ($name == 'select') {
+                return $this->_createSelectionTag($attributes);
+            } elseif ($name == 'input') {
+                return $this->_createInputTag($attributes);
+            }
+            return new SimpleTag($name, $attributes);
+        }
+
+        /**
+         *    Factory for selection fields.
+         *    @param hash $attributes    Element attributes.
+         *    @return SimpleTag          Tag object.
+         *    @access protected
+         */
+        function _createSelectionTag($attributes) {
+            if (isset($attributes['multiple'])) {
+                return new MultipleSelectionTag($attributes);
+            }
+            return new SimpleSelectionTag($attributes);
+        }
+
+        /**
+         *    Factory for input tags.
+         *    @param hash $attributes    Element attributes.
+         *    @return SimpleTag          Tag object.
+         *    @access protected
+         */
+        function _createInputTag($attributes) {
+            if (! isset($attributes['type'])) {
+                return new SimpleTextTag($attributes);
+            }
+            $type = strtolower(trim($attributes['type']));
+            $map = array(
+                    'submit' => 'SimpleSubmitTag',
+                    'image' => 'SimpleImageSubmitTag',
+                    'checkbox' => 'SimpleCheckboxTag',
+                    'radio' => 'SimpleRadioButtonTag',
+                    'text' => 'SimpleTextTag',
+                    'hidden' => 'SimpleTextTag',
+                    'password' => 'SimpleTextTag',
+                    'file' => 'SimpleUploadTag');
+            if (array_key_exists($type, $map)) {
+                $tag_class = $map[$type];
+                return new $tag_class($attributes);
+            }
+            return false;
+        }
+
+        /**
+         *    Make the keys lower case for case insensitive look-ups.
+         *    @param hash $map   Hash to convert.
+         *    @return hash       Unchanged values, but keys lower case.
+         *    @access private
+         */
+        function _keysToLowerCase($map) {
+            $lower = array();
+            foreach ($map as $key => $value) {
+                $lower[strtolower($key)] = $value;
+            }
+            return $lower;
+        }
+    }
+
+    /**
+     *    SAX event handler. Maintains a list of
+     *    open tags and dispatches them as they close.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimplePageBuilder extends SimpleSaxListener {
+        var $_tags;
+        var $_page;
+        var $_private_content_tag;
+
+        /**
+         *    Sets the builder up empty.
+         *    @access public
+         */
+        function SimplePageBuilder() {
+            $this->SimpleSaxListener();
+        }
+        
+        /**
+         *    Frees up any references so as to allow the PHP garbage
+         *    collection from unset() to work.
+         *    @access public
+         */
+        function free() {
+            unset($this->_tags);
+            unset($this->_page);
+            unset($this->_private_content_tags);
+        }
+
+        /**
+         *    Reads the raw content and send events
+         *    into the page to be built.
+         *    @param $response SimpleHttpResponse  Fetched response.
+         *    @return SimplePage                   Newly parsed page.
+         *    @access public
+         */
+        function &parse($response) {
+            $this->_tags = array();
+            $this->_page = &$this->_createPage($response);
+            $parser = &$this->_createParser($this);
+            $parser->parse($response->getContent());
+            $this->_page->acceptPageEnd();
+            return $this->_page;
+        }
+
+        /**
+         *    Creates an empty page.
+         *    @return SimplePage        New unparsed page.
+         *    @access protected
+         */
+        function &_createPage($response) {
+            $page = &new SimplePage($response);
+            return $page;
+        }
+
+        /**
+         *    Creates the parser used with the builder.
+         *    @param $listener SimpleSaxListener   Target of parser.
+         *    @return SimpleSaxParser              Parser to generate
+         *                                         events for the builder.
+         *    @access protected
+         */
+        function &_createParser(&$listener) {
+            $parser = &new SimpleHtmlSaxParser($listener);
+            return $parser;
+        }
+        
+        /**
+         *    Start of element event. Opens a new tag.
+         *    @param string $name         Element name.
+         *    @param hash $attributes     Attributes without content
+         *                                are marked as true.
+         *    @return boolean             False on parse error.
+         *    @access public
+         */
+        function startElement($name, $attributes) {
+            $factory = &new SimpleTagBuilder();
+            $tag = $factory->createTag($name, $attributes);
+            if (! $tag) {
+                return true;
+            }
+            if ($tag->getTagName() == 'label') {
+                $this->_page->acceptLabelStart($tag);
+                $this->_openTag($tag);
+                return true;
+            }
+            if ($tag->getTagName() == 'form') {
+                $this->_page->acceptFormStart($tag);
+                return true;
+            }
+            if ($tag->getTagName() == 'frameset') {
+                $this->_page->acceptFramesetStart($tag);
+                return true;
+            }
+            if ($tag->getTagName() == 'frame') {
+                $this->_page->acceptFrame($tag);
+                return true;
+            }
+            if ($tag->isPrivateContent() && ! isset($this->_private_content_tag)) {
+                $this->_private_content_tag = &$tag;
+            }
+            if ($tag->expectEndTag()) {
+                $this->_openTag($tag);
+                return true;
+            }
+            $this->_page->acceptTag($tag);
+            return true;
+        }
+
+        /**
+         *    End of element event.
+         *    @param string $name        Element name.
+         *    @return boolean            False on parse error.
+         *    @access public
+         */
+        function endElement($name) {
+            if ($name == 'label') {
+                $this->_page->acceptLabelEnd();
+                return true;
+            }
+            if ($name == 'form') {
+                $this->_page->acceptFormEnd();
+                return true;
+            }
+            if ($name == 'frameset') {
+                $this->_page->acceptFramesetEnd();
+                return true;
+            }
+            if ($this->_hasNamedTagOnOpenTagStack($name)) {
+                $tag = array_pop($this->_tags[$name]);
+                if ($tag->isPrivateContent() && $this->_private_content_tag->getTagName() == $name) {
+                    unset($this->_private_content_tag);
+                }
+                $this->_addContentTagToOpenTags($tag);
+                $this->_page->acceptTag($tag);
+                return true;
+            }
+            return true;
+        }
+
+        /**
+         *    Test to see if there are any open tags awaiting
+         *    closure that match the tag name.
+         *    @param string $name        Element name.
+         *    @return boolean            True if any are still open.
+         *    @access private
+         */
+        function _hasNamedTagOnOpenTagStack($name) {
+            return isset($this->_tags[$name]) && (count($this->_tags[$name]) > 0);
+        }
+
+        /**
+         *    Unparsed, but relevant data. The data is added
+         *    to every open tag.
+         *    @param string $text        May include unparsed tags.
+         *    @return boolean            False on parse error.
+         *    @access public
+         */
+        function addContent($text) {
+            if (isset($this->_private_content_tag)) {
+                $this->_private_content_tag->addContent($text);
+            } else {
+                $this->_addContentToAllOpenTags($text);
+            }
+            return true;
+        }
+
+        /**
+         *    Any content fills all currently open tags unless it
+         *    is part of an option tag.
+         *    @param string $text        May include unparsed tags.
+         *    @access private
+         */
+        function _addContentToAllOpenTags($text) {
+            foreach (array_keys($this->_tags) as $name) {
+                for ($i = 0, $count = count($this->_tags[$name]); $i < $count; $i++) {
+                    $this->_tags[$name][$i]->addContent($text);
+                }
+            }
+        }
+
+        /**
+         *    Parsed data in tag form. The parsed tag is added
+         *    to every open tag. Used for adding options to select
+         *    fields only.
+         *    @param SimpleTag $tag        Option tags only.
+         *    @access private
+         */
+        function _addContentTagToOpenTags(&$tag) {
+            if ($tag->getTagName() != 'option') {
+                return;
+            }
+            foreach (array_keys($this->_tags) as $name) {
+                for ($i = 0, $count = count($this->_tags[$name]); $i < $count; $i++) {
+                    $this->_tags[$name][$i]->addTag($tag);
+                }
+            }
+        }
+
+        /**
+         *    Opens a tag for receiving content. Multiple tags
+         *    will be receiving input at the same time.
+         *    @param SimpleTag $tag        New content tag.
+         *    @access private
+         */
+        function _openTag(&$tag) {
+            $name = $tag->getTagName();
+            if (! in_array($name, array_keys($this->_tags))) {
+                $this->_tags[$name] = array();
+            }
+            $this->_tags[$name][] = &$tag;
+        }
+    }
+
+    /**
+     *    A wrapper for a web page.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimplePage {
+        var $_links;
+        var $_title;
+        var $_last_widget;
+        var $_label;
+        var $_left_over_labels;
+        var $_open_forms;
+        var $_complete_forms;
+        var $_frameset;
+        var $_frames;
+        var $_frameset_nesting_level;
+        var $_transport_error;
+        var $_raw;
+        var $_text;
+        var $_sent;
+        var $_headers;
+        var $_method;
+        var $_url;
+        var $_request_data;
+
+        /**
+         *    Parses a page ready to access it's contents.
+         *    @param SimpleHttpResponse $response     Result of HTTP fetch.
+         *    @access public
+         */
+        function SimplePage($response = false) {
+            $this->_links = array();
+            $this->_title = false;
+            $this->_left_over_labels = array();
+            $this->_open_forms = array();
+            $this->_complete_forms = array();
+            $this->_frameset = false;
+            $this->_frames = array();
+            $this->_frameset_nesting_level = 0;
+            $this->_text = false;
+            if ($response) {
+                $this->_extractResponse($response);
+            } else {
+                $this->_noResponse();
+            }
+        }
+
+        /**
+         *    Extracts all of the response information.
+         *    @param SimpleHttpResponse $response    Response being parsed.
+         *    @access private
+         */
+        function _extractResponse($response) {
+            $this->_transport_error = $response->getError();
+            $this->_raw = $response->getContent();
+            $this->_sent = $response->getSent();
+            $this->_headers = $response->getHeaders();
+            $this->_method = $response->getMethod();
+            $this->_url = $response->getUrl();
+            $this->_request_data = $response->getRequestData();
+        }
+
+        /**
+         *    Sets up a missing response.
+         *    @access private
+         */
+        function _noResponse() {
+            $this->_transport_error = 'No page fetched yet';
+            $this->_raw = false;
+            $this->_sent = false;
+            $this->_headers = false;
+            $this->_method = 'GET';
+            $this->_url = false;
+            $this->_request_data = false;
+        }
+
+        /**
+         *    Original request as bytes sent down the wire.
+         *    @return mixed              Sent content.
+         *    @access public
+         */
+        function getRequest() {
+            return $this->_sent;
+        }
+
+        /**
+         *    Accessor for raw text of page.
+         *    @return string        Raw unparsed content.
+         *    @access public
+         */
+        function getRaw() {
+            return $this->_raw;
+        }
+
+        /**
+         *    Accessor for plain text of page as a text browser
+         *    would see it.
+         *    @return string        Plain text of page.
+         *    @access public
+         */
+        function getText() {
+            if (! $this->_text) {
+                $this->_text = SimpleHtmlSaxParser::normalise($this->_raw);
+            }
+            return $this->_text;
+        }
+
+        /**
+         *    Accessor for raw headers of page.
+         *    @return string       Header block as text.
+         *    @access public
+         */
+        function getHeaders() {
+            if ($this->_headers) {
+                return $this->_headers->getRaw();
+            }
+            return false;
+        }
+
+        /**
+         *    Original request method.
+         *    @return string        GET, POST or HEAD.
+         *    @access public
+         */
+        function getMethod() {
+            return $this->_method;
+        }
+
+        /**
+         *    Original resource name.
+         *    @return SimpleUrl        Current url.
+         *    @access public
+         */
+        function getUrl() {
+            return $this->_url;
+        }
+
+        /**
+         *    Original request data.
+         *    @return mixed              Sent content.
+         *    @access public
+         */
+        function getRequestData() {
+            return $this->_request_data;
+        }
+
+        /**
+         *    Accessor for last error.
+         *    @return string        Error from last response.
+         *    @access public
+         */
+        function getTransportError() {
+            return $this->_transport_error;
+        }
+
+        /**
+         *    Accessor for current MIME type.
+         *    @return string    MIME type as string; e.g. 'text/html'
+         *    @access public
+         */
+        function getMimeType() {
+            if ($this->_headers) {
+                return $this->_headers->getMimeType();
+            }
+            return false;
+        }
+
+        /**
+         *    Accessor for HTTP response code.
+         *    @return integer    HTTP response code received.
+         *    @access public
+         */
+        function getResponseCode() {
+            if ($this->_headers) {
+                return $this->_headers->getResponseCode();
+            }
+            return false;
+        }
+
+        /**
+         *    Accessor for last Authentication type. Only valid
+         *    straight after a challenge (401).
+         *    @return string    Description of challenge type.
+         *    @access public
+         */
+        function getAuthentication() {
+            if ($this->_headers) {
+                return $this->_headers->getAuthentication();
+            }
+            return false;
+        }
+
+        /**
+         *    Accessor for last Authentication realm. Only valid
+         *    straight after a challenge (401).
+         *    @return string    Name of security realm.
+         *    @access public
+         */
+        function getRealm() {
+            if ($this->_headers) {
+                return $this->_headers->getRealm();
+            }
+            return false;
+        }
+
+        /**
+         *    Accessor for current frame focus. Will be
+         *    false as no frames.
+         *    @return array    Always empty.
+         *    @access public
+         */
+        function getFrameFocus() {
+            return array();
+        }
+
+        /**
+         *    Sets the focus by index. The integer index starts from 1.
+         *    @param integer $choice    Chosen frame.
+         *    @return boolean           Always false.
+         *    @access public
+         */
+        function setFrameFocusByIndex($choice) {
+            return false;
+        }
+
+        /**
+         *    Sets the focus by name. Always fails for a leaf page.
+         *    @param string $name    Chosen frame.
+         *    @return boolean        False as no frames.
+         *    @access public
+         */
+        function setFrameFocus($name) {
+            return false;
+        }
+
+        /**
+         *    Clears the frame focus. Does nothing for a leaf page.
+         *    @access public
+         */
+        function clearFrameFocus() {
+        }
+
+        /**
+         *    Adds a tag to the page.
+         *    @param SimpleTag $tag        Tag to accept.
+         *    @access public
+         */
+        function acceptTag(&$tag) {
+            if ($tag->getTagName() == "a") {
+                $this->_addLink($tag);
+            } elseif ($tag->getTagName() == "title") {
+                $this->_setTitle($tag);
+            } elseif ($this->_isFormElement($tag->getTagName())) {
+                for ($i = 0; $i < count($this->_open_forms); $i++) {
+                    $this->_open_forms[$i]->addWidget($tag);
+                }
+                $this->_last_widget = &$tag;
+            }
+        }
+
+        /**
+         *    Opens a label for a described widget.
+         *    @param SimpleFormTag $tag      Tag to accept.
+         *    @access public
+         */
+        function acceptLabelStart(&$tag) {
+            $this->_label = &$tag;
+            unset($this->_last_widget);
+        }
+
+        /**
+         *    Closes the most recently opened label.
+         *    @access public
+         */
+        function acceptLabelEnd() {
+            if (isset($this->_label)) {
+                if (isset($this->_last_widget)) {
+                    $this->_last_widget->setLabel($this->_label->getText());
+                    unset($this->_last_widget);
+                } else {
+                    $this->_left_over_labels[] = SimpleTestCompatibility::copy($this->_label);
+                }
+                unset($this->_label);
+            }
+        }
+
+        /**
+         *    Tests to see if a tag is a possible form
+         *    element.
+         *    @param string $name     HTML element name.
+         *    @return boolean         True if form element.
+         *    @access private
+         */
+        function _isFormElement($name) {
+            return in_array($name, array('input', 'button', 'textarea', 'select'));
+        }
+
+        /**
+         *    Opens a form. New widgets go here.
+         *    @param SimpleFormTag $tag      Tag to accept.
+         *    @access public
+         */
+        function acceptFormStart(&$tag) {
+            $this->_open_forms[] = &new SimpleForm($tag, $this->getUrl());
+        }
+
+        /**
+         *    Closes the most recently opened form.
+         *    @access public
+         */
+        function acceptFormEnd() {
+            if (count($this->_open_forms)) {
+                $this->_complete_forms[] = array_pop($this->_open_forms);
+            }
+        }
+
+        /**
+         *    Opens a frameset. A frameset may contain nested
+         *    frameset tags.
+         *    @param SimpleFramesetTag $tag      Tag to accept.
+         *    @access public
+         */
+        function acceptFramesetStart(&$tag) {
+            if (! $this->_isLoadingFrames()) {
+                $this->_frameset = &$tag;
+            }
+            $this->_frameset_nesting_level++;
+        }
+
+        /**
+         *    Closes the most recently opened frameset.
+         *    @access public
+         */
+        function acceptFramesetEnd() {
+            if ($this->_isLoadingFrames()) {
+                $this->_frameset_nesting_level--;
+            }
+        }
+
+        /**
+         *    Takes a single frame tag and stashes it in
+         *    the current frame set.
+         *    @param SimpleFrameTag $tag      Tag to accept.
+         *    @access public
+         */
+        function acceptFrame(&$tag) {
+            if ($this->_isLoadingFrames()) {
+                if ($tag->getAttribute('src')) {
+                    $this->_frames[] = &$tag;
+                }
+            }
+        }
+
+        /**
+         *    Test to see if in the middle of reading
+         *    a frameset.
+         *    @return boolean        True if inframeset.
+         *    @access private
+         */
+        function _isLoadingFrames() {
+            if (! $this->_frameset) {
+                return false;
+            }
+            return ($this->_frameset_nesting_level > 0);
+        }
+
+        /**
+         *    Test to see if link is an absolute one.
+         *    @param string $url     Url to test.
+         *    @return boolean        True if absolute.
+         *    @access protected
+         */
+        function _linkIsAbsolute($url) {
+            $parsed = new SimpleUrl($url);
+            return (boolean)($parsed->getScheme() && $parsed->getHost());
+        }
+
+        /**
+         *    Adds a link to the page.
+         *    @param SimpleAnchorTag $tag      Link to accept.
+         *    @access protected
+         */
+        function _addLink($tag) {
+            $this->_links[] = $tag;
+        }
+
+        /**
+         *    Marker for end of complete page. Any work in
+         *    progress can now be closed.
+         *    @access public
+         */
+        function acceptPageEnd() {
+            while (count($this->_open_forms)) {
+                $this->_complete_forms[] = array_pop($this->_open_forms);
+            }
+            foreach ($this->_left_over_labels as $label) {
+                for ($i = 0, $count = count($this->_complete_forms); $i < $count; $i++) {
+                    $this->_complete_forms[$i]->attachLabelBySelector(
+                            new SimpleById($label->getFor()),
+                            $label->getText());
+                }
+            }
+        }
+
+        /**
+         *    Test for the presence of a frameset.
+         *    @return boolean        True if frameset.
+         *    @access public
+         */
+        function hasFrames() {
+            return (boolean)$this->_frameset;
+        }
+
+        /**
+         *    Accessor for frame name and source URL for every frame that
+         *    will need to be loaded. Immediate children only.
+         *    @return boolean/array     False if no frameset or
+         *                              otherwise a hash of frame URLs.
+         *                              The key is either a numerical
+         *                              base one index or the name attribute.
+         *    @access public
+         */
+        function getFrameset() {
+            if (! $this->_frameset) {
+                return false;
+            }
+            $urls = array();
+            for ($i = 0; $i < count($this->_frames); $i++) {
+                $name = $this->_frames[$i]->getAttribute('name');
+                $url = new SimpleUrl($this->_frames[$i]->getAttribute('src'));
+                $urls[$name ? $name : $i + 1] = $url->makeAbsolute($this->getUrl());
+            }
+            return $urls;
+        }
+
+        /**
+         *    Fetches a list of loaded frames.
+         *    @return array/string    Just the URL for a single page.
+         *    @access public
+         */
+        function getFrames() {
+            $url = $this->getUrl();
+            return $url->asString();
+        }
+
+        /**
+         *    Accessor for a list of all fixed links.
+         *    @return array   List of urls with scheme of
+         *                    http or https and hostname.
+         *    @access public
+         */
+        function getAbsoluteUrls() {
+            $all = array();
+            foreach ($this->_links as $link) {
+                if ($this->_linkIsAbsolute($link->getHref())) {
+                    $all[] = $link->getHref();
+                }
+            }
+            return $all;
+        }
+
+        /**
+         *    Accessor for a list of all relative links.
+         *    @return array      List of urls without hostname.
+         *    @access public
+         */
+        function getRelativeUrls() {
+            $all = array();
+            foreach ($this->_links as $link) {
+                if (! $this->_linkIsAbsolute($link->getHref())) {
+                    $all[] = $link->getHref();
+                }
+            }
+            return $all;
+        }
+
+        /**
+         *    Accessor for URLs by the link label. Label will match
+         *    regardess of whitespace issues and case.
+         *    @param string $label    Text of link.
+         *    @return array           List of links with that label.
+         *    @access public
+         */
+        function getUrlsByLabel($label) {
+            $matches = array();
+            foreach ($this->_links as $link) {
+                if ($link->getText() == $label) {
+                    $matches[] = $this->_getUrlFromLink($link);
+                }
+            }
+            return $matches;
+        }
+
+        /**
+         *    Accessor for a URL by the id attribute.
+         *    @param string $id       Id attribute of link.
+         *    @return SimpleUrl       URL with that id of false if none.
+         *    @access public
+         */
+        function getUrlById($id) {
+            foreach ($this->_links as $link) {
+                if ($link->getAttribute('id') === (string)$id) {
+                    return $this->_getUrlFromLink($link);
+                }
+            }
+            return false;
+        }
+
+        /**
+         *    Converts a link into a target URL.
+         *    @param SimpleAnchor $link    Parsed link.
+         *    @return SimpleUrl            URL with frame target if any.
+         *    @access private
+         */
+        function _getUrlFromLink($link) {
+            $url = $this->_makeAbsolute($link->getHref());
+            if ($link->getAttribute('target')) {
+                $url->setTarget($link->getAttribute('target'));
+            }
+            return $url;
+        }
+
+        /**
+         *    Expands expandomatic URLs into fully qualified
+         *    URLs.
+         *    @param SimpleUrl $url        Relative URL.
+         *    @return SimpleUrl            Absolute URL.
+         *    @access protected
+         */
+        function _makeAbsolute($url) {
+            if (! is_object($url)) {
+                $url = new SimpleUrl($url);
+            }
+            return $url->makeAbsolute($this->getUrl());
+        }
+
+        /**
+         *    Sets the title tag contents.
+         *    @param SimpleTitleTag $tag    Title of page.
+         *    @access protected
+         */
+        function _setTitle(&$tag) {
+            $this->_title = &$tag;
+        }
+
+        /**
+         *    Accessor for parsed title.
+         *    @return string     Title or false if no title is present.
+         *    @access public
+         */
+        function getTitle() {
+            if ($this->_title) {
+                return $this->_title->getText();
+            }
+            return false;
+        }
+
+        /**
+         *    Finds a held form by button label. Will only
+         *    search correctly built forms.
+         *    @param SimpleSelector $selector       Button finder.
+         *    @return SimpleForm                    Form object containing
+         *                                          the button.
+         *    @access public
+         */
+        function &getFormBySubmit($selector) {
+            for ($i = 0; $i < count($this->_complete_forms); $i++) {
+                if ($this->_complete_forms[$i]->hasSubmit($selector)) {
+                    return $this->_complete_forms[$i];
+                }
+            }
+            $null = null;
+            return $null;
+        }
+
+        /**
+         *    Finds a held form by image using a selector.
+         *    Will only search correctly built forms.
+         *    @param SimpleSelector $selector  Image finder.
+         *    @return SimpleForm               Form object containing
+         *                                     the image.
+         *    @access public
+         */
+        function &getFormByImage($selector) {
+            for ($i = 0; $i < count($this->_complete_forms); $i++) {
+                if ($this->_complete_forms[$i]->hasImage($selector)) {
+                    return $this->_complete_forms[$i];
+                }
+            }
+            $null = null;
+            return $null;
+        }
+
+        /**
+         *    Finds a held form by the form ID. A way of
+         *    identifying a specific form when we have control
+         *    of the HTML code.
+         *    @param string $id     Form label.
+         *    @return SimpleForm    Form object containing the matching ID.
+         *    @access public
+         */
+        function &getFormById($id) {
+            for ($i = 0; $i < count($this->_complete_forms); $i++) {
+                if ($this->_complete_forms[$i]->getId() == $id) {
+                    return $this->_complete_forms[$i];
+                }
+            }
+            $null = null;
+            return $null;
+        }
+
+        /**
+         *    Sets a field on each form in which the field is
+         *    available.
+         *    @param SimpleSelector $selector    Field finder.
+         *    @param string $value               Value to set field to.
+         *    @return boolean                    True if value is valid.
+         *    @access public
+         */
+        function setField($selector, $value) {
+            $is_set = false;
+            for ($i = 0; $i < count($this->_complete_forms); $i++) {
+                if ($this->_complete_forms[$i]->setField($selector, $value)) {
+                    $is_set = true;
+                }
+            }
+            return $is_set;
+        }
+
+        /**
+         *    Accessor for a form element value within a page.
+         *    @param SimpleSelector $selector    Field finder.
+         *    @return string/boolean             A string if the field is
+         *                                       present, false if unchecked
+         *                                       and null if missing.
+         *    @access public
+         */
+        function getField($selector) {
+            for ($i = 0; $i < count($this->_complete_forms); $i++) {
+                $value = $this->_complete_forms[$i]->getValue($selector);
+                if (isset($value)) {
+                    return $value;
+                }
+            }
+            return null;
+        }
+    }
+?>
diff --git a/lib/simpletestlib/parser.php b/lib/simpletestlib/parser.php
new file mode 100644 (file)
index 0000000..97db5ca
--- /dev/null
@@ -0,0 +1,773 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     MockObjects
+     * @version        $Id$
+     */
+
+    /**#@+
+     * Lexer mode stack constants
+     */
+    if (! defined('LEXER_ENTER')) {
+        define('LEXER_ENTER', 1);
+    }
+    if (! defined('LEXER_MATCHED')) {
+        define('LEXER_MATCHED', 2);
+    }
+    if (! defined('LEXER_UNMATCHED')) {
+        define('LEXER_UNMATCHED', 3);
+    }
+    if (! defined('LEXER_EXIT')) {
+        define('LEXER_EXIT', 4);
+    }
+    if (! defined('LEXER_SPECIAL')) {
+        define('LEXER_SPECIAL', 5);
+    }
+    /**#@-*/
+    
+    /**
+     *    Compounded regular expression. Any of
+     *    the contained patterns could match and
+     *    when one does, it's label is returned.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class ParallelRegex {
+        var $_patterns;
+        var $_labels;
+        var $_regex;
+        var $_case;
+        
+        /**
+         *    Constructor. Starts with no patterns.
+         *    @param boolean $case    True for case sensitive, false
+         *                            for insensitive.
+         *    @access public
+         */
+        function ParallelRegex($case) {
+            $this->_case = $case;
+            $this->_patterns = array();
+            $this->_labels = array();
+            $this->_regex = null;
+        }
+        
+        /**
+         *    Adds a pattern with an optional label.
+         *    @param string $pattern      Perl style regex, but ( and )
+         *                                lose the usual meaning.
+         *    @param string $label        Label of regex to be returned
+         *                                on a match.
+         *    @access public
+         */
+        function addPattern($pattern, $label = true) {
+            $count = count($this->_patterns);
+            $this->_patterns[$count] = $pattern;
+            $this->_labels[$count] = $label;
+            $this->_regex = null;
+        }
+        
+        /**
+         *    Attempts to match all patterns at once against
+         *    a string.
+         *    @param string $subject      String to match against.
+         *    @param string $match        First matched portion of
+         *                                subject.
+         *    @return boolean             True on success.
+         *    @access public
+         */
+        function match($subject, &$match) {
+            if (count($this->_patterns) == 0) {
+                return false;
+            }
+            if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) {
+                $match = '';
+                return false;
+            }
+            $match = $matches[0];
+            for ($i = 1; $i < count($matches); $i++) {
+                if ($matches[$i]) {
+                    return $this->_labels[$i - 1];
+                }
+            }
+            return true;
+        }
+        
+        /**
+         *    Compounds the patterns into a single
+         *    regular expression separated with the
+         *    "or" operator. Caches the regex.
+         *    Will automatically escape (, ) and / tokens.
+         *    @param array $patterns    List of patterns in order.
+         *    @access private
+         */
+        function _getCompoundedRegex() {
+            if ($this->_regex == null) {
+                for ($i = 0, $count = count($this->_patterns); $i < $count; $i++) {
+                    $this->_patterns[$i] = '(' . str_replace(
+                            array('/', '(', ')'),
+                            array('\/', '\(', '\)'),
+                            $this->_patterns[$i]) . ')';
+                }
+                $this->_regex = "/" . implode("|", $this->_patterns) . "/" . $this->_getPerlMatchingFlags();
+            }
+            return $this->_regex;
+        }
+        
+        /**
+         *    Accessor for perl regex mode flags to use.
+         *    @return string       Perl regex flags.
+         *    @access private
+         */
+        function _getPerlMatchingFlags() {
+            return ($this->_case ? "msS" : "msSi");
+        }
+    }
+    
+    /**
+     *    States for a stack machine.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleStateStack {
+        var $_stack;
+        
+        /**
+         *    Constructor. Starts in named state.
+         *    @param string $start        Starting state name.
+         *    @access public
+         */
+        function SimpleStateStack($start) {
+            $this->_stack = array($start);
+        }
+        
+        /**
+         *    Accessor for current state.
+         *    @return string       State.
+         *    @access public
+         */
+        function getCurrent() {
+            return $this->_stack[count($this->_stack) - 1];
+        }
+        
+        /**
+         *    Adds a state to the stack and sets it
+         *    to be the current state.
+         *    @param string $state        New state.
+         *    @access public
+         */
+        function enter($state) {
+            array_push($this->_stack, $state);
+        }
+        
+        /**
+         *    Leaves the current state and reverts
+         *    to the previous one.
+         *    @return boolean    False if we drop off
+         *                       the bottom of the list.
+         *    @access public
+         */
+        function leave() {
+            if (count($this->_stack) == 1) {
+                return false;
+            }
+            array_pop($this->_stack);
+            return true;
+        }
+    }
+    
+    /**
+     *    Accepts text and breaks it into tokens.
+     *    Some optimisation to make the sure the
+     *    content is only scanned by the PHP regex
+     *    parser once. Lexer modes must not start
+     *    with leading underscores.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleLexer {
+        var $_regexes;
+        var $_parser;
+        var $_mode;
+        var $_mode_handlers;
+        var $_case;
+        
+        /**
+         *    Sets up the lexer in case insensitive matching
+         *    by default.
+         *    @param SimpleSaxParser $parser  Handling strategy by
+         *                                    reference.
+         *    @param string $start            Starting handler.
+         *    @param boolean $case            True for case sensitive.
+         *    @access public
+         */
+        function SimpleLexer(&$parser, $start = "accept", $case = false) {
+            $this->_case = $case;
+            $this->_regexes = array();
+            $this->_parser = &$parser;
+            $this->_mode = &new SimpleStateStack($start);
+            $this->_mode_handlers = array($start => $start);
+        }
+        
+        /**
+         *    Adds a token search pattern for a particular
+         *    parsing mode. The pattern does not change the
+         *    current mode.
+         *    @param string $pattern      Perl style regex, but ( and )
+         *                                lose the usual meaning.
+         *    @param string $mode         Should only apply this
+         *                                pattern when dealing with
+         *                                this type of input.
+         *    @access public
+         */
+        function addPattern($pattern, $mode = "accept") {
+            if (! isset($this->_regexes[$mode])) {
+                $this->_regexes[$mode] = new ParallelRegex($this->_case);
+            }
+            $this->_regexes[$mode]->addPattern($pattern);
+            if (! isset($this->_mode_handlers[$mode])) {
+                $this->_mode_handlers[$mode] = $mode;
+            }
+        }
+        
+        /**
+         *    Adds a pattern that will enter a new parsing
+         *    mode. Useful for entering parenthesis, strings,
+         *    tags, etc.
+         *    @param string $pattern      Perl style regex, but ( and )
+         *                                lose the usual meaning.
+         *    @param string $mode         Should only apply this
+         *                                pattern when dealing with
+         *                                this type of input.
+         *    @param string $new_mode     Change parsing to this new
+         *                                nested mode.
+         *    @access public
+         */
+        function addEntryPattern($pattern, $mode, $new_mode) {
+            if (! isset($this->_regexes[$mode])) {
+                $this->_regexes[$mode] = new ParallelRegex($this->_case);
+            }
+            $this->_regexes[$mode]->addPattern($pattern, $new_mode);
+            if (! isset($this->_mode_handlers[$new_mode])) {
+                $this->_mode_handlers[$new_mode] = $new_mode;
+            }
+        }
+        
+        /**
+         *    Adds a pattern that will exit the current mode
+         *    and re-enter the previous one.
+         *    @param string $pattern      Perl style regex, but ( and )
+         *                                lose the usual meaning.
+         *    @param string $mode         Mode to leave.
+         *    @access public
+         */
+        function addExitPattern($pattern, $mode) {
+            if (! isset($this->_regexes[$mode])) {
+                $this->_regexes[$mode] = new ParallelRegex($this->_case);
+            }
+            $this->_regexes[$mode]->addPattern($pattern, "__exit");
+            if (! isset($this->_mode_handlers[$mode])) {
+                $this->_mode_handlers[$mode] = $mode;
+            }
+        }
+        
+        /**
+         *    Adds a pattern that has a special mode. Acts as an entry
+         *    and exit pattern in one go, effectively calling a special
+         *    parser handler for this token only.
+         *    @param string $pattern      Perl style regex, but ( and )
+         *                                lose the usual meaning.
+         *    @param string $mode         Should only apply this
+         *                                pattern when dealing with
+         *                                this type of input.
+         *    @param string $special      Use this mode for this one token.
+         *    @access public
+         */
+        function addSpecialPattern($pattern, $mode, $special) {
+            if (! isset($this->_regexes[$mode])) {
+                $this->_regexes[$mode] = new ParallelRegex($this->_case);
+            }
+            $this->_regexes[$mode]->addPattern($pattern, "_$special");
+            if (! isset($this->_mode_handlers[$special])) {
+                $this->_mode_handlers[$special] = $special;
+            }
+        }
+        
+        /**
+         *    Adds a mapping from a mode to another handler.
+         *    @param string $mode        Mode to be remapped.
+         *    @param string $handler     New target handler.
+         *    @access public
+         */
+        function mapHandler($mode, $handler) {
+            $this->_mode_handlers[$mode] = $handler;
+        }
+        
+        /**
+         *    Splits the page text into tokens. Will fail
+         *    if the handlers report an error or if no
+         *    content is consumed. If successful then each
+         *    unparsed and parsed token invokes a call to the
+         *    held listener.
+         *    @param string $raw        Raw HTML text.
+         *    @return boolean           True on success, else false.
+         *    @access public
+         */
+        function parse($raw) {
+            if (! isset($this->_parser)) {
+                return false;
+            }
+            $length = strlen($raw);
+            while (is_array($parsed = $this->_reduce($raw))) {
+                list($raw, $unmatched, $matched, $mode) = $parsed;
+                if (! $this->_dispatchTokens($unmatched, $matched, $mode)) {
+                    return false;
+                }
+                if ($raw === '') {
+                    return true;
+                }
+                if (strlen($raw) == $length) {
+                    return false;
+                }
+                $length = strlen($raw);
+            }
+            if (! $parsed) {
+                return false;
+            }
+            return $this->_invokeParser($raw, LEXER_UNMATCHED);
+        }
+        
+        /**
+         *    Sends the matched token and any leading unmatched
+         *    text to the parser changing the lexer to a new
+         *    mode if one is listed.
+         *    @param string $unmatched    Unmatched leading portion.
+         *    @param string $matched      Actual token match.
+         *    @param string $mode         Mode after match. A boolean
+         *                                false mode causes no change.
+         *    @return boolean             False if there was any error
+         *                                from the parser.
+         *    @access private
+         */
+        function _dispatchTokens($unmatched, $matched, $mode = false) {
+            if (! $this->_invokeParser($unmatched, LEXER_UNMATCHED)) {
+                return false;
+            }
+            if (is_bool($mode)) {
+                return $this->_invokeParser($matched, LEXER_MATCHED);
+            }
+            if ($this->_isModeEnd($mode)) {
+                if (! $this->_invokeParser($matched, LEXER_EXIT)) {
+                    return false;
+                }
+                return $this->_mode->leave();
+            }
+            if ($this->_isSpecialMode($mode)) {
+                $this->_mode->enter($this->_decodeSpecial($mode));
+                if (! $this->_invokeParser($matched, LEXER_SPECIAL)) {
+                    return false;
+                }
+                return $this->_mode->leave();
+            }
+            $this->_mode->enter($mode);
+            return $this->_invokeParser($matched, LEXER_ENTER);
+        }
+        
+        /**
+         *    Tests to see if the new mode is actually to leave
+         *    the current mode and pop an item from the matching
+         *    mode stack.
+         *    @param string $mode    Mode to test.
+         *    @return boolean        True if this is the exit mode.
+         *    @access private
+         */
+        function _isModeEnd($mode) {
+            return ($mode === "__exit");
+        }
+        
+        /**
+         *    Test to see if the mode is one where this mode
+         *    is entered for this token only and automatically
+         *    leaves immediately afterwoods.
+         *    @param string $mode    Mode to test.
+         *    @return boolean        True if this is the exit mode.
+         *    @access private
+         */
+        function _isSpecialMode($mode) {
+            return (strncmp($mode, "_", 1) == 0);
+        }
+        
+        /**
+         *    Strips the magic underscore marking single token
+         *    modes.
+         *    @param string $mode    Mode to decode.
+         *    @return string         Underlying mode name.
+         *    @access private
+         */
+        function _decodeSpecial($mode) {
+            return substr($mode, 1);
+        }
+        
+        /**
+         *    Calls the parser method named after the current
+         *    mode. Empty content will be ignored. The lexer
+         *    has a parser handler for each mode in the lexer.
+         *    @param string $content        Text parsed.
+         *    @param boolean $is_match      Token is recognised rather
+         *                                  than unparsed data.
+         *    @access private
+         */
+        function _invokeParser($content, $is_match) {
+            if (($content === '') || ($content === false)) {
+                return true;
+            }
+            $handler = $this->_mode_handlers[$this->_mode->getCurrent()];
+            return $this->_parser->$handler($content, $is_match);
+        }
+        
+        /**
+         *    Tries to match a chunk of text and if successful
+         *    removes the recognised chunk and any leading
+         *    unparsed data. Empty strings will not be matched.
+         *    @param string $raw         The subject to parse. This is the
+         *                               content that will be eaten.
+         *    @return array/boolean      Three item list of unparsed
+         *                               content followed by the
+         *                               recognised token and finally the
+         *                               action the parser is to take.
+         *                               True if no match, false if there
+         *                               is a parsing error.
+         *    @access private
+         */
+        function _reduce($raw) {
+            if ($action = $this->_regexes[$this->_mode->getCurrent()]->match($raw, $match)) {
+                $unparsed_character_count = strpos($raw, $match);
+                $unparsed = substr($raw, 0, $unparsed_character_count);
+                $raw = substr($raw, $unparsed_character_count + strlen($match));
+                return array($raw, $unparsed, $match, $action);
+            }
+            return true;
+        }
+    }
+    
+    /**
+     *    Breas HTML into SAX events.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleHtmlLexer extends SimpleLexer {
+        
+        /**
+         *    Sets up the lexer with case insensitive matching
+         *    and adds the HTML handlers.
+         *    @param SimpleSaxParser $parser  Handling strategy by
+         *                                    reference.
+         *    @access public
+         */
+        function SimpleHtmlLexer(&$parser) {
+            $this->SimpleLexer($parser, 'text');
+            $this->mapHandler('text', 'acceptTextToken');
+            $this->_addSkipping();
+            foreach ($this->_getParsedTags() as $tag) {
+                $this->_addTag($tag);
+            }
+            $this->_addInTagTokens();
+        }
+        
+        /**
+         *    List of parsed tags. Others are ignored.
+         *    @return array        List of searched for tags.
+         *    @access private
+         */
+        function _getParsedTags() {
+            return array('a', 'title', 'form', 'input', 'button', 'textarea', 'select',
+                    'option', 'frameset', 'frame', 'label');
+        }
+        
+        /**
+         *    The lexer has to skip certain sections such
+         *    as server code, client code and styles.
+         *    @access private
+         */
+        function _addSkipping() {
+            $this->mapHandler('css', 'ignore');
+            $this->addEntryPattern('<style', 'text', 'css');
+            $this->addExitPattern('</style>', 'css');
+            $this->mapHandler('js', 'ignore');
+            $this->addEntryPattern('<script', 'text', 'js');
+            $this->addExitPattern('</script>', 'js');
+            $this->mapHandler('comment', 'ignore');
+            $this->addEntryPattern('<!--', 'text', 'comment');
+            $this->addExitPattern('-->', 'comment');
+        }
+        
+        /**
+         *    Pattern matches to start and end a tag.
+         *    @param string $tag          Name of tag to scan for.
+         *    @access private
+         */
+        function _addTag($tag) {
+            $this->addSpecialPattern("</$tag>", 'text', 'acceptEndToken');
+            $this->addEntryPattern("<$tag", 'text', 'tag');
+        }
+        
+        /**
+         *    Pattern matches to parse the inside of a tag
+         *    including the attributes and their quoting.
+         *    @access private
+         */
+        function _addInTagTokens() {
+            $this->mapHandler('tag', 'acceptStartToken');
+            $this->addSpecialPattern('\s+', 'tag', 'ignore');
+            $this->_addAttributeTokens();
+            $this->addExitPattern('/>', 'tag');
+            $this->addExitPattern('>', 'tag');
+        }
+        
+        /**
+         *    Matches attributes that are either single quoted,
+         *    double quoted or unquoted.
+         *    @access private
+         */
+        function _addAttributeTokens() {
+            $this->mapHandler('dq_attribute', 'acceptAttributeToken');
+            $this->addEntryPattern('=\s*"', 'tag', 'dq_attribute');
+            $this->addPattern("\\\\\"", 'dq_attribute');
+            $this->addExitPattern('"', 'dq_attribute');
+            $this->mapHandler('sq_attribute', 'acceptAttributeToken');
+            $this->addEntryPattern("=\s*'", 'tag', 'sq_attribute');
+            $this->addPattern("\\\\'", 'sq_attribute');
+            $this->addExitPattern("'", 'sq_attribute');
+            $this->mapHandler('uq_attribute', 'acceptAttributeToken');
+            $this->addSpecialPattern('=\s*[^>\s]*', 'tag', 'uq_attribute');
+        }
+    }
+    
+    /**
+     *    Converts HTML tokens into selected SAX events.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleHtmlSaxParser {
+        var $_lexer;
+        var $_listener;
+        var $_tag;
+        var $_attributes;
+        var $_current_attribute;
+        
+        /**
+         *    Sets the listener.
+         *    @param SimpleSaxListener $listener    SAX event handler.
+         *    @access public
+         */
+        function SimpleHtmlSaxParser(&$listener) {
+            $this->_listener = &$listener;
+            $this->_lexer = &$this->createLexer($this);
+            $this->_tag = '';
+            $this->_attributes = array();
+            $this->_current_attribute = '';
+        }
+        
+        /**
+         *    Runs the content through the lexer which
+         *    should call back to the acceptors.
+         *    @param string $raw      Page text to parse.
+         *    @return boolean         False if parse error.
+         *    @access public
+         */
+        function parse($raw) {
+            return $this->_lexer->parse($raw);
+        }
+        
+        /**
+         *    Sets up the matching lexer. Starts in 'text' mode.
+         *    @param SimpleSaxParser $parser    Event generator, usually $self.
+         *    @return SimpleLexer               Lexer suitable for this parser.
+         *    @access public
+         *    @static
+         */
+        function &createLexer(&$parser) {
+            $lexer = &new SimpleHtmlLexer($parser);
+            return $lexer;
+        }
+        
+        /**
+         *    Accepts a token from the tag mode. If the
+         *    starting element completes then the element
+         *    is dispatched and the current attributes
+         *    set back to empty. The element or attribute
+         *    name is converted to lower case.
+         *    @param string $token     Incoming characters.
+         *    @param integer $event    Lexer event type.
+         *    @return boolean          False if parse error.
+         *    @access public
+         */
+        function acceptStartToken($token, $event) {
+            if ($event == LEXER_ENTER) {
+                $this->_tag = strtolower(substr($token, 1));
+                return true;
+            }
+            if ($event == LEXER_EXIT) {
+                $success = $this->_listener->startElement(
+                        $this->_tag,
+                        $this->_attributes);
+                $this->_tag = '';
+                $this->_attributes = array();
+                return $success;
+            }
+            if ($token != '=') {
+                $this->_current_attribute = strtolower(SimpleHtmlSaxParser::decodeHtml($token));
+                $this->_attributes[$this->_current_attribute] = '';
+            }
+            return true;
+        }
+        
+        /**
+         *    Accepts a token from the end tag mode.
+         *    The element name is converted to lower case.
+         *    @param string $token     Incoming characters.
+         *    @param integer $event    Lexer event type.
+         *    @return boolean          False if parse error.
+         *    @access public
+         */
+        function acceptEndToken($token, $event) {
+            if (! preg_match('/<\/(.*)>/', $token, $matches)) {
+                return false;
+            }
+            return $this->_listener->endElement(strtolower($matches[1]));
+        }
+        
+        /**
+         *    Part of the tag data.
+         *    @param string $token     Incoming characters.
+         *    @param integer $event    Lexer event type.
+         *    @return boolean          False if parse error.
+         *    @access public
+         */
+        function acceptAttributeToken($token, $event) {
+            if ($event == LEXER_UNMATCHED) {
+                $this->_attributes[$this->_current_attribute] .=
+                        SimpleHtmlSaxParser::decodeHtml($token);
+            }
+            if ($event == LEXER_SPECIAL) {
+                $this->_attributes[$this->_current_attribute] .=
+                        preg_replace('/^=\s*/' , '', SimpleHtmlSaxParser::decodeHtml($token));
+            }
+            return true;
+        }
+        
+        /**
+         *    A character entity.
+         *    @param string $token    Incoming characters.
+         *    @param integer $event   Lexer event type.
+         *    @return boolean         False if parse error.
+         *    @access public
+         */
+        function acceptEntityToken($token, $event) {
+        }
+        
+        /**
+         *    Character data between tags regarded as
+         *    important.
+         *    @param string $token     Incoming characters.
+         *    @param integer $event    Lexer event type.
+         *    @return boolean          False if parse error.
+         *    @access public
+         */
+        function acceptTextToken($token, $event) {
+            return $this->_listener->addContent($token);
+        }
+        
+        /**
+         *    Incoming data to be ignored.
+         *    @param string $token     Incoming characters.
+         *    @param integer $event    Lexer event type.
+         *    @return boolean          False if parse error.
+         *    @access public
+         */
+        function ignore($token, $event) {
+            return true;
+        }
+        
+        /**
+         *    Decodes any HTML entities.
+         *    @param string $html    Incoming HTML.
+         *    @return string         Outgoing plain text.
+         *    @access public
+         *    @static
+         */
+        function decodeHtml($html) {
+            static $translations;
+            if (! isset($translations)) {
+                $translations = array_flip(get_html_translation_table(HTML_ENTITIES));
+            }
+            return strtr($html, $translations);
+        }
+        
+        /**
+         *    Turns HTML into text browser visible text. Images
+         *    are converted to their alt text and tags are supressed.
+         *    Entities are converted to their visible representation.
+         *    @param string $html        HTML to convert.
+         *    @return string             Plain text.
+         *    @access public
+         *    @static
+         */
+        function normalise($html) {
+            $text = preg_replace('|<!--.*?-->|', '', $html);
+            $text = preg_replace('|<img.*?alt\s*=\s*"(.*?)".*?>|', ' \1 ', $text);
+            $text = preg_replace('|<img.*?alt\s*=\s*\'(.*?)\'.*?>|', ' \1 ', $text);
+            $text = preg_replace('|<img.*?alt\s*=\s*([a-zA-Z_]+).*?>|', ' \1 ', $text);
+            $text = preg_replace('|<.*?>|', '', $text);
+            $text = SimpleHtmlSaxParser::decodeHtml($text);
+            $text = preg_replace('|\s+|', ' ', $text);
+            return trim($text);
+        }
+    }
+    
+    /**
+     *    SAX event handler.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     *    @abstract
+     */
+    class SimpleSaxListener {
+        
+        /**
+         *    Sets the document to write to.
+         *    @access public
+         */
+        function SimpleSaxListener() {
+        }
+        
+        /**
+         *    Start of element event.
+         *    @param string $name        Element name.
+         *    @param hash $attributes    Name value pairs.
+         *                               Attributes without content
+         *                               are marked as true.
+         *    @return boolean            False on parse error.
+         *    @access public
+         */
+        function startElement($name, $attributes) {
+        }
+        
+        /**
+         *    End of element event.
+         *    @param string $name        Element name.
+         *    @return boolean            False on parse error.
+         *    @access public
+         */
+        function endElement($name) {
+        }
+        
+        /**
+         *    Unparsed, but relevant data.
+         *    @param string $text        May include unparsed tags.
+         *    @return boolean            False on parse error.
+         *    @access public
+         */
+        function addContent($text) {
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/reflection_php4.php b/lib/simpletestlib/reflection_php4.php
new file mode 100644 (file)
index 0000000..a624106
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**
+     *    Version specific reflection API.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class SimpleReflection {
+        var $_interface;
+
+        /**
+         *    Stashes the class/interface.
+         *    @param string $interface    Class or interface
+         *                                to inspect.
+         */
+        function SimpleReflection($interface) {
+            $this->_interface = $interface;
+        }
+
+        /**
+         *    Checks that a class has been declared.
+         *    @return boolean        True if defined.
+         *    @access public
+         */
+        function classExists() {
+            return class_exists($this->_interface);
+        }
+
+        /**
+         *    Needed to kill the autoload feature in PHP5
+         *    for classes created dynamically.
+         *    @return boolean        True if defined.
+         *    @access public
+         */
+        function classExistsSansAutoload() {
+            return class_exists($this->_interface);
+        }
+
+        /**
+         *    Checks that a class or interface has been
+         *    declared.
+         *    @return boolean        True if defined.
+         *    @access public
+         */
+        function classOrInterfaceExists() {
+            return class_exists($this->_interface);
+        }
+
+        /**
+         *    Needed to kill the autoload feature in PHP5
+         *    for classes created dynamically.
+         *    @return boolean        True if defined.
+         *    @access public
+         */
+        function classOrInterfaceExistsSansAutoload() {
+            return class_exists($this->_interface);
+        }
+
+        /**
+         *    Gets the list of methods on a class or
+         *    interface.
+         *    @returns array          List of method names.
+         *    @access public
+         */
+        function getMethods() {
+            return get_class_methods($this->_interface);
+        }
+
+        /**
+         *    Gets the list of interfaces from a class. If the
+         *       class name is actually an interface then just that
+         *       interface is returned.
+         *    @returns array          List of interfaces.
+         *    @access public
+         */
+        function getInterfaces() {
+            return array();
+        }
+
+        /**
+         *    Finds the parent class name.
+         *    @returns string      Parent class name.
+         *    @access public
+         */
+        function getParent() {
+            return strtolower(get_parent_class($this->_interface));
+        }
+
+        /**
+         *    Determines if the class is abstract, which for PHP 4
+         *    will never be the case.
+         *    @returns boolean      True if abstract.
+         *    @access public
+         */
+        function isAbstract() {
+            return false;
+        }
+
+        /**
+         *       Gets the source code matching the declaration
+         *       of a method.
+         *       @param string $method           Method name.
+         *    @access public
+         */
+        function getSignature($method) {
+               return "function &$method()";
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/reflection_php5.php b/lib/simpletestlib/reflection_php5.php
new file mode 100644 (file)
index 0000000..b816ef3
--- /dev/null
@@ -0,0 +1,275 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**
+     *    Version specific reflection API.
+        *    @package SimpleTest
+        *    @subpackage UnitTester
+     */
+    class SimpleReflection {
+        var $_interface;
+
+        /**
+         *    Stashes the class/interface.
+         *    @param string $interface    Class or interface
+         *                                to inspect.
+         */
+        function SimpleReflection($interface) {
+            $this->_interface = $interface;
+        }
+
+        /**
+         *    Checks that a class has been declared. Versions
+         *    before PHP5.0.2 need a check that it's not really
+         *    an interface.
+         *    @return boolean            True if defined.
+         *    @access public
+         */
+        function classExists() {
+            if (! class_exists($this->_interface)) {
+                return false;
+            }
+            $reflection = new ReflectionClass($this->_interface);
+            return ! $reflection->isInterface();
+        }
+
+        /**
+         *    Needed to kill the autoload feature in PHP5
+         *    for classes created dynamically.
+         *    @return boolean        True if defined.
+         *    @access public
+         */
+        function classExistsSansAutoload() {
+            return class_exists($this->_interface, false);
+        }
+
+        /**
+         *    Checks that a class or interface has been
+         *    declared.
+         *    @return boolean            True if defined.
+         *    @access public
+         */
+        function classOrInterfaceExists() {
+            return $this->_classOrInterfaceExistsWithAutoload($this->_interface, true);
+        }
+
+        /**
+         *    Needed to kill the autoload feature in PHP5
+         *    for classes created dynamically.
+         *    @return boolean        True if defined.
+         *    @access public
+         */
+        function classOrInterfaceExistsSansAutoload() {
+            return $this->_classOrInterfaceExistsWithAutoload($this->_interface, false);
+        }
+
+        /**
+         *    Needed to select the autoload feature in PHP5
+         *    for classes created dynamically.
+         *    @param string $interface       Class or interface name.
+         *    @param boolean $autoload       True totriggerautoload.
+         *    @return boolean                True if interface defined.
+         *    @access private
+         */
+        function _classOrInterfaceExistsWithAutoload($interface, $autoload) {
+            if (function_exists('interface_exists')) {
+                if (interface_exists($this->_interface, $autoload)) {
+                    return true;
+                }
+            }
+            return class_exists($this->_interface, $autoload);
+        }
+
+        /**
+         *    Gets the list of methods on a class or
+         *    interface. Needs to recursively look at all of
+         *    the interfaces included.
+         *    @returns array              List of method names.
+         *    @access public
+         */
+        function getMethods() {
+            return array_unique(get_class_methods($this->_interface));
+        }
+
+        /**
+         *    Gets the list of interfaces from a class. If the
+         *    class name is actually an interface then just that
+         *    interface is returned.
+         *    @returns array          List of interfaces.
+         *    @access public
+         */
+        function getInterfaces() {
+            $reflection = new ReflectionClass($this->_interface);
+            if ($reflection->isInterface()) {
+               return array($this->_interface);
+            }
+            return $this->_onlyParents($reflection->getInterfaces());
+        }
+
+        /**
+         *    Gets the list of methods for the implemented
+         *    interfaces only.
+         *    @returns array      List of enforced method signatures.
+         *    @access public
+         */
+        function getInterfaceMethods() {
+            $methods = array();
+            foreach ($this->getInterfaces() as $interface) {
+                $methods = array_merge($methods, get_class_methods($interface));
+            }
+            return array_unique($methods);
+        }
+        
+        /**
+         *    Checks to see if the method signature has to be tightly
+         *    specified.
+         *    @param string $method        Method name.
+         *    @returns boolean             True if enforced.
+         *    @access private
+         */
+        function _isInterfaceMethod($method) {
+            return in_array($method, $this->getInterfaceMethods());
+        }
+
+        /**
+         *    Finds the parent class name.
+         *    @returns string      Parent class name.
+         *    @access public
+         */
+        function getParent() {
+            $reflection = new ReflectionClass($this->_interface);
+            $parent = $reflection->getParentClass();
+            if ($parent) {
+                return $parent->getName();
+            }
+            return false;
+        }
+
+        /**
+         *    Determines if the class is abstract.
+         *    @returns boolean      True if abstract.
+         *    @access public
+         */
+        function isAbstract() {
+            $reflection = new ReflectionClass($this->_interface);
+            return $reflection->isAbstract();
+        }
+
+        /**
+         *    Wittles a list of interfaces down to only the top
+         *    level parents.
+         *    @param array $interfaces     Reflection API interfaces
+         *                                 to reduce.
+         *    @returns array               List of parent interface names.
+         *    @access private
+         */
+        function _onlyParents($interfaces) {
+            $parents = array();
+            foreach ($interfaces as $interface) {
+                foreach($interfaces as $possible_parent) {
+                    if ($interface->getName() == $possible_parent->getName()) {
+                        continue;
+                    }
+                    if ($interface->isSubClassOf($possible_parent)) {
+                        break;
+                    }
+                }
+                $parents[] = $interface->getName();
+            }
+            return $parents;
+        }
+
+        /**
+         *    Gets the source code matching the declaration
+         *    of a method.
+         *    @param string $name    Method name.
+         *    @return string         Method signature up to last
+         *                           bracket.
+         *    @access public
+         */
+        function getSignature($name) {
+               if ($name == '__get') {
+                       return 'function __get($key)';
+               }
+               if ($name == '__set') {
+                       return 'function __set($key, $value)';
+               }
+               if (! is_callable(array($this->_interface, $name))) {
+                       return "function $name()";
+               }
+               if ($this->_isInterfaceMethod($name)) {
+                   return $this->_getFullSignature($name);
+               }
+               return "function $name()";
+        }
+        
+        /**
+         *    For a signature specified in an interface, full
+         *    details must be replicated to be a valid implementation.
+         *    @param string $name    Method name.
+         *    @return string         Method signature up to last
+         *                           bracket.
+         *    @access private
+         */
+        function _getFullSignature($name) {
+               $interface = new ReflectionClass($this->_interface);
+               $method = $interface->getMethod($name);
+               $reference = $method->returnsReference() ? '&' : '';
+               return "function $reference$name(" .
+                       implode(', ', $this->_getParameterSignatures($method)) .
+                       ")";
+        }
+
+        /**
+         *    Gets the source code for each parameter.
+         *    @param ReflectionMethod $method   Method object from
+         *                                                                             reflection API
+         *    @return array                     List of strings, each
+         *                                      a snippet of code.
+         *    @access private
+         */
+        function _getParameterSignatures($method) {
+               $signatures = array();
+            foreach ($method->getParameters() as $parameter) {
+                $type = $parameter->getClass();
+               $signatures[] =
+                                       (! is_null($type) ? $type->getName() . ' ' : '') .
+                               ($parameter->isPassedByReference() ? '&' : '') .
+                               '$' . $this->_suppressSpurious($parameter->getName()) .
+                               ($this->_isOptional($parameter) ? ' = null' : '');
+            }
+            return $signatures;
+        }
+
+        /**
+         *    The SPL library has problems with the
+         *    Reflection library. In particular, you can
+         *    get extra characters in parameter names :(.
+         *    @param string $name    Parameter name.
+         *    @return string         Cleaner name.
+         *    @access private
+         */
+        function _suppressSpurious($name) {
+            return str_replace(array('[', ']', ' '), '', $name);
+        }
+
+        /**
+         *    Test of a reflection parameter being optional
+         *    that works with early versions of PHP5.
+         *    @param reflectionParameter $parameter    Is this optional.
+         *    @return boolean                          True if optional.
+         *    @access private
+         */
+        function _isOptional($parameter) {
+            if (method_exists($parameter, 'isOptional')) {
+                return $parameter->isOptional();
+            }
+            return false;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/remote.php b/lib/simpletestlib/remote.php
new file mode 100644 (file)
index 0000000..cb6b9f2
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/browser.php');
+    require_once(dirname(__FILE__) . '/xml.php');
+    require_once(dirname(__FILE__) . '/test_case.php');
+    /**#@-*/
+
+    /**
+     *    Runs an XML formated test on a remote server.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class RemoteTestCase {
+        var $_url;
+        var $_dry_url;
+        var $_size;
+        
+        /**
+         *    Sets the location of the remote test.
+         *    @param string $url       Test location.
+         *    @param string $dry_url   Location for dry run.
+         *    @access public
+         */
+        function RemoteTestCase($url, $dry_url = false) {
+            $this->_url = $url;
+            $this->_dry_url = $dry_url ? $dry_url : $url;
+            $this->_size = false;
+        }
+        
+        /**
+         *    Accessor for the test name for subclasses.
+         *    @return string           Name of the test.
+         *    @access public
+         */
+        function getLabel() {
+            return $this->_url;
+        }
+
+        /**
+         *    Runs the top level test for this class. Currently
+         *    reads the data as a single chunk. I'll fix this
+         *    once I have added iteration to the browser.
+         *    @param SimpleReporter $reporter    Target of test results.
+         *    @returns boolean                   True if no failures.
+         *    @access public
+         */
+        function run(&$reporter) {
+            $browser = &$this->_createBrowser();
+            $xml = $browser->get($this->_url);
+            if (! $xml) {
+                trigger_error('Cannot read remote test URL [' . $this->_url . ']');
+                return false;
+            }
+            $parser = &$this->_createParser($reporter);
+            if (! $parser->parse($xml)) {
+                trigger_error('Cannot parse incoming XML from [' . $this->_url . ']');
+                return false;
+            }
+            return true;
+        }
+        
+        /**
+         *    Creates a new web browser object for fetching
+         *    the XML report.
+         *    @return SimpleBrowser           New browser.
+         *    @access protected
+         */
+        function &_createBrowser() {
+            return new SimpleBrowser();
+        }
+        
+        /**
+         *    Creates the XML parser.
+         *    @param SimpleReporter $reporter    Target of test results.
+         *    @return SimpleTestXmlListener      XML reader.
+         *    @access protected
+         */
+        function &_createParser(&$reporter) {
+            return new SimpleTestXmlParser($reporter);
+        }
+        
+        /**
+         *    Accessor for the number of subtests.
+         *    @return integer           Number of test cases.
+         *    @access public
+         */
+        function getSize() {
+            if ($this->_size === false) {
+                $browser = &$this->_createBrowser();
+                $xml = $browser->get($this->_dry_url);
+                if (! $xml) {
+                    trigger_error('Cannot read remote test URL [' . $this->_dry_url . ']');
+                    return false;
+                }
+                $reporter = &new SimpleReporter();
+                $parser = &$this->_createParser($reporter);
+                if (! $parser->parse($xml)) {
+                    trigger_error('Cannot parse incoming XML from [' . $this->_dry_url . ']');
+                    return false;
+                }
+                $this->_size = $reporter->getTestCaseCount();
+            }
+            return $this->_size;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/reporter.php b/lib/simpletestlib/reporter.php
new file mode 100644 (file)
index 0000000..3be2a15
--- /dev/null
@@ -0,0 +1,367 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/scorer.php');
+    /**#@-*/
+
+    /**
+     *    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 "<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 { color: red; } pre { background-color: lightgray; }";
+        }
+
+        /**
+         *    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 or exception.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         *    @abstract
+         */
+        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 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 is ignored.
+         *    @access public
+         *    @abstract
+         */
+        function paintError($message) {
+            parent::paintError($message);
+            print "Exception " . $this->getExceptionCount() . "!\n$message\n";
+        }
+
+        /**
+         *    Paints formatted text such as dumped variables.
+         *    @param string $message        Text to show.
+         *    @access public
+         */
+        function paintFormattedMessage($message) {
+            print "$message\n";
+            flush();
+        }
+    }
+
+    /**
+     *    Runs just a single test group, a single case or
+     *    even a single test within that case.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class SelectiveReporter extends SimpleReporterDecorator {
+        var $_just_this_case =false;
+        var $_just_this_test = false;
+        var $_within_test_case = true;
+
+        /**
+         *    Selects the test case or group to be run,
+         *    and optionally a specific test.
+         *    @param SimpleScorer $reporter    Reporter to receive events.
+         *    @param string $just_this_case    Only this case or group will run.
+         *    @param string $just_this_test    Only this test method will run.
+         */
+        function SelectiveReporter(&$reporter, $just_this_case = false, $just_this_test = false) {
+            if (isset($just_this_case) && $just_this_case) {
+                $this->_just_this_case = strtolower($just_this_case);
+                $this->_within_test_case = false;
+            }
+            if (isset($just_this_test) && $just_this_test) {
+                $this->_just_this_test = strtolower($just_this_test);
+            }
+            $this->SimpleReporterDecorator($reporter);
+        }
+
+        /**
+         *    Compares criteria to actual the case/group name.
+         *    @param string $test_case    The incoming test.
+         *    @return boolean             True if matched.
+         *    @access protected
+         */
+        function _isCaseMatch($test_case) {
+            if ($this->_just_this_case) {
+                return $this->_just_this_case == strtolower($test_case);
+            }
+            return false;
+        }
+
+        /**
+         *    Compares criteria to actual the test name.
+         *    @param string $method       The incoming test method.
+         *    @return boolean             True if matched.
+         *    @access protected
+         */
+        function _isTestMatch($method) {
+            if ($this->_just_this_test) {
+                return $this->_just_this_test == strtolower($method);
+            }
+            return true;
+        }
+
+        /**
+         *    Veto everything that doesn't match the method wanted.
+         *    @param string $test_case       Name of test case.
+         *    @param string $method          Name of test method.
+         *    @return boolean                True if test should be run.
+         *    @access public
+         */
+        function shouldInvoke($test_case, $method) {
+            if ($this->_within_test_case && $this->_isTestMatch($method)) {
+                return $this->_reporter->shouldInvoke($test_case, $method);
+            }
+            return false;
+        }
+
+        /**
+         *    Paints the start of a group test.
+         *    @param string $test_case     Name of test or other label.
+         *    @param integer $size         Number of test cases starting.
+         *    @access public
+         */
+        function paintGroupStart($test_case, $size) {
+            if ($this->_isCaseMatch($test_case)) {
+                $this->_within_test_case = true;
+            }
+            if ($this->_within_test_case) {
+                $this->_reporter->paintGroupStart($test_case, $size);
+            }
+        }
+
+        /**
+         *    Paints the end of a group test.
+         *    @param string $test_case     Name of test or other label.
+         *    @access public
+         */
+        function paintGroupEnd($test_case) {
+            if ($this->_within_test_case) {
+                $this->_reporter->paintGroupEnd($test_case);
+            }
+            if ($this->_isCaseMatch($test_case)) {
+                $this->_within_test_case = false;
+            }
+        }
+
+        /**
+         *    Paints the start of a test case.
+         *    @param string $test_case     Name of test or other label.
+         *    @access public
+         */
+        function paintCaseStart($test_case) {
+            if ($this->_isCaseMatch($test_case)) {
+                $this->_within_test_case = true;
+            }
+            if ($this->_within_test_case) {
+                $this->_reporter->paintCaseStart($test_case);
+            }
+        }
+
+        /**
+         *    Paints the end of a test case.
+         *    @param string $test_case     Name of test or other label.
+         *    @access public
+         */
+        function paintCaseEnd($test_case) {
+            if ($this->_within_test_case) {
+                $this->_reporter->paintCaseEnd($test_case);
+            }
+            if ($this->_isCaseMatch($test_case)) {
+                $this->_within_test_case = false;
+            }
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/scorer.php b/lib/simpletestlib/scorer.php
new file mode 100644 (file)
index 0000000..62d232d
--- /dev/null
@@ -0,0 +1,777 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+*/
+    require_once(dirname(__FILE__) . '/invoker.php');
+    /**#@-*/
+
+    /**
+     *    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 or PHP 5
+         *    throwing an exception.
+         *    @param string $message    Text of error formatted by
+         *                              the test case.
+         *    @access public
+         */
+        function paintError($message) {
+            $this->_exceptions++;
+        }
+
+        /**
+         *    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;
+        }
+
+        /**
+         *    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 teh reporter and the test case.
+         *    @param SimpleScorer $reporter       Reporter to receive events.
+         */
+        function SimpleReporterDecorator(&$reporter) {
+            $this->_reporter = &$reporter;
+        }
+
+        /**
+         *    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->_reporter->makeDry($is_dry);
+        }
+
+        /**
+         *    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() {
+            return $this->_reporter->getStatus();
+        }
+
+        /**
+         *    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.
+         *    @return boolean                True if test should be run.
+         *    @access public
+         */
+        function shouldInvoke($test_case_name, $method) {
+            return $this->_reporter->shouldInvoke($test_case_name, $method);
+        }
+
+        /**
+         *    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 $this->_reporter->createInvoker($invoker);
+        }
+
+        /**
+         *    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) {
+            $this->_reporter->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) {
+            $this->_reporter->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) {
+            $this->_reporter->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) {
+            $this->_reporter->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) {
+            $this->_reporter->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) {
+            $this->_reporter->paintMethodEnd($test_name);
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         */
+        function paintPass($message) {
+            $this->_reporter->paintPass($message);
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         */
+        function paintFail($message) {
+            $this->_reporter->paintFail($message);
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message    Text of error formatted by
+         *                              the test case.
+         *    @access public
+         */
+        function paintError($message) {
+            $this->_reporter->paintError($message);
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Text to display.
+         *    @access public
+         */
+        function paintMessage($message) {
+            $this->_reporter->paintMessage($message);
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Text to display.
+         *    @access public
+         */
+        function paintFormattedMessage($message) {
+            $this->_reporter->paintFormattedMessage($message);
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $type        Event type as text.
+         *    @param mixed $payload      Message or object.
+         *    @return boolean            Should return false if this
+         *                               type of signal should fail the
+         *                               test suite.
+         *    @access public
+         */
+        function paintSignal($type, &$payload) {
+            $this->_reporter->paintSignal($type, $payload);
+        }
+    }
+
+    /**
+     *    For sending messages to multiple reporters at
+     *    the same time.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class MultipleReporter {
+        var $_reporters = array();
+
+        /**
+         *    Adds a reporter to the subscriber list.
+         *    @param SimpleScorer $reporter     Reporter to receive events.
+         *    @access public
+         */
+        function attachReporter(&$reporter) {
+            $this->_reporters[] = &$reporter;
+        }
+
+        /**
+         *    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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->makeDry($is_dry);
+            }
+        }
+
+        /**
+         *    Accessor for current status. Will be false
+         *    if there have been any failures or exceptions.
+         *    If any reporter reports a failure, the whole
+         *    suite fails.
+         *    @return boolean        True if no failures.
+         *    @access public
+         */
+        function getStatus() {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                if (! $this->_reporters[$i]->getStatus()) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         *    The reporter has a veto on what should be run.
+         *    It requires all reporters to want to run the method.
+         *    @param string $test_case_name  name of test case.
+         *    @param string $method          Name of test method.
+         *    @access public
+         */
+        function shouldInvoke($test_case_name, $method) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                if (! $this->_reporters[$i]->shouldInvoke($test_case_name, $method)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         *    Every reporter gets a chance to wrap the invoker.
+         *    @param SimpleInvoker $invoker   Individual test runner.
+         *    @return SimpleInvoker           Wrapped test runner.
+         *    @access public
+         */
+        function &createInvoker(&$invoker) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $invoker = &$this->_reporters[$i]->createInvoker($invoker);
+            }
+            return $invoker;
+        }
+
+        /**
+         *    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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->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) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintMethodEnd($test_name);
+            }
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         */
+        function paintPass($message) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintPass($message);
+            }
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         */
+        function paintFail($message) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintFail($message);
+            }
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message    Text of error formatted by
+         *                              the test case.
+         *    @access public
+         */
+        function paintError($message) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintError($message);
+            }
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Text to display.
+         *    @access public
+         */
+        function paintMessage($message) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintMessage($message);
+            }
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $message        Text to display.
+         *    @access public
+         */
+        function paintFormattedMessage($message) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintFormattedMessage($message);
+            }
+        }
+
+        /**
+         *    Chains to the wrapped reporter.
+         *    @param string $type        Event type as text.
+         *    @param mixed $payload      Message or object.
+         *    @return boolean            Should return false if this
+         *                               type of signal should fail the
+         *                               test suite.
+         *    @access public
+         */
+        function paintSignal($type, &$payload) {
+            for ($i = 0; $i < count($this->_reporters); $i++) {
+                $this->_reporters[$i]->paintSignal($type, $payload);
+            }
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/selector.php b/lib/simpletestlib/selector.php
new file mode 100644 (file)
index 0000000..ec8f354
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+    /**
+     * Base include file for SimpleTest.
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/tag.php');
+    require_once(dirname(__FILE__) . '/encoding.php');
+    /**#@-*/
+
+    /**
+     *    Used to extract form elements for testing against.
+     *    Searches by name attribute.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleByName {
+        var $_name;
+
+        /**
+         *    Stashes the name for later comparison.
+         *    @param string $name     Name attribute to match.
+         */
+        function SimpleByName($name) {
+            $this->_name = $name;
+        }
+
+        /**
+         *    Compares with name attribute of widget.
+         *    @param SimpleWidget $widget    Control to compare.
+         *    @access public
+         */
+        function isMatch($widget) {
+            return ($widget->getName() == $this->_name);
+        }
+    }
+
+    /**
+     *    Used to extract form elements for testing against.
+     *    Searches by visible label or alt text.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleByLabel {
+        var $_label;
+
+        /**
+         *    Stashes the name for later comparison.
+         *    @param string $label     Visible text to match.
+         */
+        function SimpleByLabel($label) {
+            $this->_label = $label;
+        }
+
+        /**
+         *    Comparison. Compares visible text of widget or
+         *    related label.
+         *    @param SimpleWidget $widget    Control to compare.
+         *    @access public
+         */
+        function isMatch($widget) {
+            if (! method_exists($widget, 'isLabel')) {
+                return false;
+            }
+            return $widget->isLabel($this->_label);
+        }
+    }
+
+    /**
+     *    Used to extract form elements for testing against.
+     *    Searches dy id attribute.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleById {
+        var $_id;
+
+        /**
+         *    Stashes the name for later comparison.
+         *    @param string $id     ID atribute to match.
+         */
+        function SimpleById($id) {
+            $this->_id = $id;
+        }
+
+        /**
+         *    Comparison. Compares id attribute of widget.
+         *    @param SimpleWidget $widget    Control to compare.
+         *    @access public
+         */
+        function isMatch($widget) {
+            return $widget->isId($this->_id);
+        }
+    }
+
+    /**
+     *    Used to extract form elements for testing against.
+     *    Searches by visible label, name or alt text.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleByLabelOrName {
+        var $_label;
+
+        /**
+         *    Stashes the name/label for later comparison.
+         *    @param string $label     Visible text to match.
+         */
+        function SimpleByLabelOrName($label) {
+            $this->_label = $label;
+        }
+
+        /**
+         *    Comparison. Compares visible text of widget or
+         *    related label or name.
+         *    @param SimpleWidget $widget    Control to compare.
+         *    @access public
+         */
+        function isMatch($widget) {
+            if (method_exists($widget, 'isLabel')) {
+                if ($widget->isLabel($this->_label)) {
+                    return true;
+                }
+            }
+            return ($widget->getName() == $this->_label);
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/shell_tester.php b/lib/simpletestlib/shell_tester.php
new file mode 100644 (file)
index 0000000..1e513ef
--- /dev/null
@@ -0,0 +1,306 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/test_case.php');
+    /**#@-*/
+
+    /**
+     *    Wrapper for exec() functionality.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class SimpleShell {
+        var $_output;
+
+        /**
+         *    Executes the shell comand and stashes the output.
+         *    @access public
+         */
+        function SimpleShell() {
+            $this->_output = false;
+        }
+
+        /**
+         *    Actually runs the command. Does not trap the
+         *    error stream output as this need PHP 4.3+.
+         *    @param string $command    The actual command line
+         *                              to run.
+         *    @return integer           Exit code.
+         *    @access public
+         */
+        function execute($command) {
+            $this->_output = false;
+            exec($command, $this->_output, $ret);
+            return $ret;
+        }
+
+        /**
+         *    Accessor for the last output.
+         *    @return string        Output as text.
+         *    @access public
+         */
+        function getOutput() {
+            return implode("\n", $this->_output);
+        }
+
+        /**
+         *    Accessor for the last output.
+         *    @return array         Output as array of lines.
+         *    @access public
+         */
+               function getOutputAsList() {
+                       return $this->_output;
+               }
+    }
+
+    /**
+     *    Test case for testing of command line scripts and
+     *    utilities. Usually scripts taht are external to the
+     *    PHP code, but support it in some way.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class ShellTestCase extends SimpleTestCase {
+        var $_current_shell;
+        var $_last_status;
+        var $_last_command;
+
+        /**
+         *    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 ShellTestCase($label = false) {
+            $this->SimpleTestCase($label);
+            $this->_current_shell = &$this->_createShell();
+            $this->_last_status = false;
+            $this->_last_command = '';
+        }
+
+        /**
+         *    Executes a command and buffers the results.
+         *    @param string $command     Command to run.
+         *    @return boolean            True if zero exit code.
+         *    @access public
+         */
+        function execute($command) {
+            $shell = &$this->_getShell();
+            $this->_last_status = $shell->execute($command);
+            $this->_last_command = $command;
+            return ($this->_last_status === 0);
+        }
+
+        /**
+         *    Dumps the output of the last command.
+         *    @access public
+         */
+        function dumpOutput() {
+            $this->dump($this->getOutput());
+        }
+
+        /**
+         *    Accessor for the last output.
+         *    @return string        Output as text.
+         *    @access public
+         */
+               function getOutput() {
+            $shell = &$this->_getShell();
+            return $shell->getOutput();
+               }
+
+        /**
+         *    Accessor for the last output.
+         *    @return array         Output as array of lines.
+         *    @access public
+         */
+               function getOutputAsList() {
+            $shell = &$this->_getShell();
+            return $shell->getOutputAsList();
+               }
+        
+        /**
+         *    Will trigger a pass if the two parameters have
+         *    the same value only. Otherwise a fail. This
+         *    is for testing hand extracted text, etc.
+         *    @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") {
+            return $this->assert(
+                    new EqualExpectation($first),
+                    $second,
+                    $message);
+        }
+        
+        /**
+         *    Will trigger a pass if the two parameters have
+         *    a different value. Otherwise a fail. This
+         *    is for testing hand extracted text, etc.
+         *    @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") {
+            return $this->assert(
+                    new NotEqualExpectation($first),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    Tests the last status code from the shell.
+         *    @param integer $status   Expected status of last
+         *                             command.
+         *    @param string $message   Message to display.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertExitCode($status, $message = "%s") {
+            $message = sprintf($message, "Expected status code of [$status] from [" .
+                    $this->_last_command . "], but got [" .
+                    $this->_last_status . "]");
+            return $this->assertTrue($status === $this->_last_status, $message);
+        }
+
+        /**
+         *    Attempt to exactly match the combined STDERR and
+         *    STDOUT output.
+         *    @param string $expected  Expected output.
+         *    @param string $message   Message to display.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertOutput($expected, $message = "%s") {
+            $shell = &$this->_getShell();
+            return $this->assert(
+                    new EqualExpectation($expected),
+                    $shell->getOutput(),
+                    $message);
+        }
+
+        /**
+         *    Scans the output for a Perl regex. If found
+         *    anywhere it passes, else it fails.
+         *    @param string $pattern    Regex to search for.
+         *    @param string $message    Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertOutputPattern($pattern, $message = "%s") {
+            $shell = &$this->_getShell();
+            return $this->assert(
+                    new PatternExpectation($pattern),
+                    $shell->getOutput(),
+                    $message);
+        }
+
+        /**
+         *    If a Perl regex is found anywhere in the current
+         *    output then a failure is generated, else a pass.
+         *    @param string $pattern    Regex to search for.
+         *    @param $message           Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertNoOutputPattern($pattern, $message = "%s") {
+            $shell = &$this->_getShell();
+            return $this->assert(
+                    new NoPatternExpectation($pattern),
+                    $shell->getOutput(),
+                    $message);
+        }
+
+        /**
+         *    File existence check.
+         *    @param string $path      Full filename and path.
+         *    @param string $message   Message to display.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertFileExists($path, $message = "%s") {
+            $message = sprintf($message, "File [$path] should exist");
+            return $this->assertTrue(file_exists($path), $message);
+        }
+
+        /**
+         *    File non-existence check.
+         *    @param string $path      Full filename and path.
+         *    @param string $message   Message to display.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertFileNotExists($path, $message = "%s") {
+            $message = sprintf($message, "File [$path] should not exist");
+            return $this->assertFalse(file_exists($path), $message);
+        }
+
+        /**
+         *    Scans a file for a Perl regex. If found
+         *    anywhere it passes, else it fails.
+         *    @param string $pattern    Regex to search for.
+         *    @param string $path       Full filename and path.
+         *    @param string $message    Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertFilePattern($pattern, $path, $message = "%s") {
+            $shell = &$this->_getShell();
+            return $this->assert(
+                    new PatternExpectation($pattern),
+                    implode('', file($path)),
+                    $message);
+        }
+
+        /**
+         *    If a Perl regex is found anywhere in the named
+         *    file then a failure is generated, else a pass.
+         *    @param string $pattern    Regex to search for.
+         *    @param string $path       Full filename and path.
+         *    @param string $message    Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertNoFilePattern($pattern, $path, $message = "%s") {
+            $shell = &$this->_getShell();
+            return $this->assert(
+                    new NoPatternExpectation($pattern),
+                    implode('', file($path)),
+                    $message);
+        }
+
+        /**
+         *    Accessor for current shell. Used for testing the
+         *    the tester itself.
+         *    @return Shell        Current shell.
+         *    @access protected
+         */
+        function &_getShell() {
+            return $this->_current_shell;
+        }
+
+        /**
+         *    Factory for the shell to run the command on.
+         *    @return Shell        New shell object.
+         *    @access protected
+         */
+        function &_createShell() {
+            $shell = &new SimpleShell();
+            return $shell;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/simpletest.php b/lib/simpletestlib/simpletest.php
new file mode 100644 (file)
index 0000000..0a1693a
--- /dev/null
@@ -0,0 +1,282 @@
+<?php
+    /**
+     * Global state for SimpleTest and kicker script in future versions.
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include SimpleTest files
+     */
+    if (version_compare(phpversion(), '5') >= 0) {
+        require_once(dirname(__FILE__) . '/reflection_php5.php');
+    } else {
+        require_once(dirname(__FILE__) . '/reflection_php4.php');
+    }
+    /**#@-*/
+
+    /**
+     *    Static global directives and options. I hate this
+     *    class. It's a mixture of reference hacks, configuration
+     *    and previous design screw-ups that I have to maintain
+     *    to keep backward compatibility.
+     *   @package      SimpleTest
+     */
+    class SimpleTest {
+
+        /**
+         *    Reads the SimpleTest version from the release file.
+         *    @return string        Version string.
+         *    @static
+         *    @access public
+         */
+        function getVersion() {
+            $content = file(dirname(__FILE__) . '/VERSION');
+            return trim($content[0]);
+        }
+
+        /**
+         *    Sets the name of a test case to ignore, usually
+         *    because the class is an abstract case that should
+         *    not be run. Once PHP4 is dropped this will disappear
+         *    as a public method and "abstract" will rule.
+         *    @param string $class        Add a class to ignore.
+         *    @static
+         *    @access public
+         */
+        function ignore($class) {
+            $registry = &SimpleTest::_getRegistry();
+            $registry['IgnoreList'][strtolower($class)] = true;
+        }
+
+        /**
+         *    Scans the now complete ignore list, and adds
+         *    all parent classes to the list. If a class
+         *    is not a runnable test case, then it's parents
+         *    wouldn't be either. This is syntactic sugar
+         *    to cut down on ommissions of ignore()'s or
+         *    missing abstract declarations. This cannot
+         *    be done whilst loading classes wiithout forcing
+         *    a particular order on the class declarations and
+         *    the ignore() calls. It's nice to havethe ignore()
+         *    calls at the top of teh file.
+         *    @param array $classes     Class names of interest.
+         *    @static
+         *    @access public
+         */
+        function ignoreParentsIfIgnored($classes) {
+            $registry = &SimpleTest::_getRegistry();
+            foreach ($classes as $class) {
+                if (SimpleTest::isIgnored($class)) {
+                    $reflection = new SimpleReflection($class);
+                    if ($parent = $reflection->getParent()) {
+                        SimpleTest::ignore($parent);
+                    }
+                }
+            }
+        }
+
+        /**
+         *    Test to see if a test case is in the ignore
+         *    list. Quite obviously the ignore list should
+         *    be a separate object and will be one day.
+         *    This method is internal to SimpleTest. Don't
+         *    use it.
+         *    @param string $class        Class name to test.
+         *    @return boolean             True if should not be run.
+         *    @access public
+         *    @static
+         */
+        function isIgnored($class) {
+            $registry = &SimpleTest::_getRegistry();
+            return isset($registry['IgnoreList'][strtolower($class)]);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function setMockBaseClass($mock_base) {
+            $registry = &SimpleTest::_getRegistry();
+            $registry['MockBaseClass'] = $mock_base;
+        }
+
+        /**
+         *    @deprecated
+         */
+        function getMockBaseClass() {
+            $registry = &SimpleTest::_getRegistry();
+            return $registry['MockBaseClass'];
+        }
+
+        /**
+         *    Sets proxy to use on all requests for when
+         *    testing from behind a firewall. Set host
+         *    to false to disable. This will take effect
+         *    if there are no other proxy settings.
+         *    @param string $proxy     Proxy host as URL.
+         *    @param string $username  Proxy username for authentication.
+         *    @param string $password  Proxy password for authentication.
+         *    @access public
+         */
+        function useProxy($proxy, $username = false, $password = false) {
+            $registry = &SimpleTest::_getRegistry();
+            $registry['DefaultProxy'] = $proxy;
+            $registry['DefaultProxyUsername'] = $username;
+            $registry['DefaultProxyPassword'] = $password;
+        }
+
+        /**
+         *    Accessor for default proxy host.
+         *    @return string       Proxy URL.
+         *    @access public
+         */
+        function getDefaultProxy() {
+            $registry = &SimpleTest::_getRegistry();
+            return $registry['DefaultProxy'];
+        }
+
+        /**
+         *    Accessor for default proxy username.
+         *    @return string    Proxy username for authentication.
+         *    @access public
+         */
+        function getDefaultProxyUsername() {
+            $registry = &SimpleTest::_getRegistry();
+            return $registry['DefaultProxyUsername'];
+        }
+
+        /**
+         *    Accessor for default proxy password.
+         *    @return string    Proxy password for authentication.
+         *    @access public
+         */
+        function getDefaultProxyPassword() {
+            $registry = &SimpleTest::_getRegistry();
+            return $registry['DefaultProxyPassword'];
+        }
+
+        /**
+         *    Sets the current test case instance. This
+         *    global instance can be used by the mock objects
+         *    to send message to the test cases.
+         *    @param SimpleTestCase $test        Test case to register.
+         *    @access public
+         *    @static
+         */
+        function setCurrent(&$test) {
+            $registry = &SimpleTest::_getRegistry();
+            $registry['CurrentTestCase'] = &$test;
+        }
+
+        /**
+         *    Accessor for current test instance.
+         *    @return SimpleTEstCase        Currently running test.
+         *    @access public
+         *    @static
+         */
+        function &getCurrent() {
+            $registry = &SimpleTest::_getRegistry();
+            return $registry['CurrentTestCase'];
+        }
+
+        /**
+         *    Accessor for global registry of options.
+         *    @return hash           All stored values.
+         *    @access private
+         *    @static
+         */
+        function &_getRegistry() {
+            static $registry = false;
+            if (! $registry) {
+                $registry = SimpleTest::_getDefaults();
+            }
+            return $registry;
+        }
+
+        /**
+         *    Constant default values.
+         *    @return hash       All registry defaults.
+         *    @access private
+         *    @static
+         */
+        function _getDefaults() {
+            return array(
+                    'StubBaseClass' => 'SimpleStub',
+                    'MockBaseClass' => 'SimpleMock',
+                    'IgnoreList' => array(),
+                    'DefaultProxy' => false,
+                    'DefaultProxyUsername' => false,
+                    'DefaultProxyPassword' => false);
+        }
+    }
+
+    /**
+     *    @deprecated
+     */
+    class SimpleTestOptions extends SimpleTest {
+
+        /**
+         *    @deprecated
+         */
+        function getVersion() {
+            return Simpletest::getVersion();
+        }
+
+        /**
+         *    @deprecated
+         */
+        function ignore($class) {
+            return Simpletest::ignore($class);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function isIgnored($class) {
+            return Simpletest::isIgnored($class);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function setMockBaseClass($mock_base) {
+            return Simpletest::setMockBaseClass($mock_base);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function getMockBaseClass() {
+            return Simpletest::getMockBaseClass();
+        }
+
+        /**
+         *    @deprecated
+         */
+        function useProxy($proxy, $username = false, $password = false) {
+            return Simpletest::useProxy($proxy, $username, $password);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function getDefaultProxy() {
+            return Simpletest::getDefaultProxy();
+        }
+
+        /**
+         *    @deprecated
+         */
+        function getDefaultProxyUsername() {
+            return Simpletest::getDefaultProxyUsername();
+        }
+
+        /**
+         *    @deprecated
+         */
+        function getDefaultProxyPassword() {
+            return Simpletest::getDefaultProxyPassword();
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/socket.php b/lib/simpletestlib/socket.php
new file mode 100644 (file)
index 0000000..4205482
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     MockObjects
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/compatibility.php');
+    /**#@-*/
+
+    /**
+     *    Stashes an error for later. Useful for constructors
+     *    until PHP gets exceptions.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleStickyError {
+        var $_error = 'Constructor not chained';
+
+        /**
+         *    Sets the error to empty.
+         *    @access public
+         */
+        function SimpleStickyError() {
+            $this->_clearError();
+        }
+
+        /**
+         *    Test for an outstanding error.
+         *    @return boolean           True if there is an error.
+         *    @access public
+         */
+        function isError() {
+            return ($this->_error != '');
+        }
+
+        /**
+         *    Accessor for an outstanding error.
+         *    @return string     Empty string if no error otherwise
+         *                       the error message.
+         *    @access public
+         */
+        function getError() {
+            return $this->_error;
+        }
+
+        /**
+         *    Sets the internal error.
+         *    @param string       Error message to stash.
+         *    @access protected
+         */
+        function _setError($error) {
+            $this->_error = $error;
+        }
+
+        /**
+         *    Resets the error state to no error.
+         *    @access protected
+         */
+        function _clearError() {
+            $this->_setError('');
+        }
+    }
+
+    /**
+     *    Wrapper for TCP/IP socket.
+     *    @package SimpleTest
+     *    @subpackage WebTester
+     */
+    class SimpleSocket extends SimpleStickyError {
+        var $_handle;
+        var $_is_open = false;
+        var $_sent = '';
+        var $lock_size;
+
+        /**
+         *    Opens a socket for reading and writing.
+         *    @param string $host          Hostname to send request to.
+         *    @param integer $port         Port on remote machine to open.
+         *    @param integer $timeout      Connection timeout in seconds.
+         *    @param integer $block_size   Size of chunk to read.
+         *    @access public
+         */
+        function SimpleSocket($host, $port, $timeout, $block_size = 255) {
+            $this->SimpleStickyError();
+            if (! ($this->_handle = $this->_openSocket($host, $port, $error_number, $error, $timeout))) {
+                $this->_setError("Cannot open [$host:$port] with [$error] within [$timeout] seconds");
+                return;
+            }
+            $this->_is_open = true;
+            $this->_block_size = $block_size;
+            SimpleTestCompatibility::setTimeout($this->_handle, $timeout);
+        }
+
+        /**
+         *    Writes some data to the socket and saves alocal copy.
+         *    @param string $message       String to send to socket.
+         *    @return boolean              True if successful.
+         *    @access public
+         */
+        function write($message) {
+            if ($this->isError() || ! $this->isOpen()) {
+                return false;
+            }
+            $count = fwrite($this->_handle, $message);
+            if (! $count) {
+                if ($count === false) {
+                    $this->_setError('Cannot write to socket');
+                    $this->close();
+                }
+                return false;
+            }
+            fflush($this->_handle);
+            $this->_sent .= $message;
+            return true;
+        }
+
+        /**
+         *    Reads data from the socket. The error suppresion
+         *    is a workaround for PHP4 always throwing a warning
+         *    with a secure socket.
+         *    @return integer/boolean           Incoming bytes. False
+         *                                     on error.
+         *    @access public
+         */
+        function read() {
+            if ($this->isError() || ! $this->isOpen()) {
+                return false;
+            }
+            $raw = @fread($this->_handle, $this->_block_size);
+            if ($raw === false) {
+                $this->_setError('Cannot read from socket');
+                $this->close();
+            }
+            return $raw;
+        }
+
+        /**
+         *    Accessor for socket open state.
+         *    @return boolean           True if open.
+         *    @access public
+         */
+        function isOpen() {
+            return $this->_is_open;
+        }
+
+        /**
+         *    Closes the socket preventing further reads.
+         *    Cannot be reopened once closed.
+         *    @return boolean           True if successful.
+         *    @access public
+         */
+        function close() {
+            $this->_is_open = false;
+            return fclose($this->_handle);
+        }
+
+        /**
+         *    Accessor for content so far.
+         *    @return string        Bytes sent only.
+         *    @access public
+         */
+        function getSent() {
+            return $this->_sent;
+        }
+
+        /**
+         *    Actually opens the low level socket.
+         *    @param string $host          Host to connect to.
+         *    @param integer $port         Port on host.
+         *    @param integer $error_number Recipient of error code.
+         *    @param string $error         Recipoent of error message.
+         *    @param integer $timeout      Maximum time to wait for connection.
+         *    @access protected
+         */
+        function _openSocket($host, $port, &$error_number, &$error, $timeout) {
+            return @fsockopen($host, $port, $error_number, $error, $timeout);
+        }
+    }
+
+    /**
+     *    Wrapper for TCP/IP socket over TLS.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleSecureSocket extends SimpleSocket {
+
+        /**
+         *    Opens a secure socket for reading and writing.
+         *    @param string $host      Hostname to send request to.
+         *    @param integer $port     Port on remote machine to open.
+         *    @param integer $timeout  Connection timeout in seconds.
+         *    @access public
+         */
+        function SimpleSecureSocket($host, $port, $timeout) {
+            $this->SimpleSocket($host, $port, $timeout);
+        }
+
+        /**
+         *    Actually opens the low level socket.
+         *    @param string $host          Host to connect to.
+         *    @param integer $port         Port on host.
+         *    @param integer $error_number Recipient of error code.
+         *    @param string $error         Recipient of error message.
+         *    @param integer $timeout      Maximum time to wait for connection.
+         *    @access protected
+         */
+        function _openSocket($host, $port, &$error_number, &$error, $timeout) {
+            return parent::_openSocket("tls://$host", $port, $error_number, $error, $timeout);
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/tag.php b/lib/simpletestlib/tag.php
new file mode 100644 (file)
index 0000000..c713d0a
--- /dev/null
@@ -0,0 +1,1392 @@
+<?php
+    /**
+     * Base include file for SimpleTest.
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+     
+    /**#@+
+     * include SimpleTest files
+     */
+    require_once(dirname(__FILE__) . '/parser.php');
+    require_once(dirname(__FILE__) . '/encoding.php');
+    /**#@-*/
+   
+    /**
+     *    HTML or XML tag.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleTag {
+        var $_name;
+        var $_attributes;
+        var $_content;
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param string $name        Tag name.
+         *    @param hash $attributes    Attribute names and
+         *                               string values. Note that
+         *                               the keys must have been
+         *                               converted to lower case.
+         */
+        function SimpleTag($name, $attributes) {
+            $this->_name = strtolower(trim($name));
+            $this->_attributes = $attributes;
+            $this->_content = '';
+        }
+        
+        /**
+         *    Check to see if the tag can have both start and
+         *    end tags with content in between.
+         *    @return boolean        True if content allowed.
+         *    @access public
+         */
+        function expectEndTag() {
+            return true;
+        }
+        
+        /**
+         *    The current tag should not swallow all content for
+         *    itself as it's searchable page content. Private
+         *    content tags are usually widgets that contain default
+         *    values.
+         *    @return boolean        False as content is available
+         *                           to other tags by default.
+         *    @access public
+         */
+        function isPrivateContent() {
+            return false;
+        }
+
+        /**
+         *    Appends string content to the current content.
+         *    @param string $content        Additional text.
+         *    @access public
+         */
+        function addContent($content) {
+            $this->_content .= (string)$content;
+        }
+        
+        /**
+         *    Adds an enclosed tag to the content.
+         *    @param SimpleTag $tag    New tag.
+         *    @access public
+         */
+        function addTag(&$tag) {
+        }
+        
+        /**
+         *    Accessor for tag name.
+         *    @return string       Name of tag.
+         *    @access public
+         */
+        function getTagName() {
+            return $this->_name;
+        }
+        
+        /**
+         *    List of legal child elements.
+         *    @return array        List of element names.
+         *    @access public
+         */
+        function getChildElements() {
+            return array();
+        }
+        
+        /**
+         *    Accessor for an attribute.
+         *    @param string $label    Attribute name.
+         *    @return string          Attribute value.
+         *    @access public
+         */
+        function getAttribute($label) {
+            $label = strtolower($label);
+            if (! isset($this->_attributes[$label])) {
+                return false;
+            }
+            return (string)$this->_attributes[$label];
+        }
+        
+        /**
+         *    Sets an attribute.
+         *    @param string $label    Attribute name.
+         *    @return string $value   New attribute value.
+         *    @access protected
+         */
+        function _setAttribute($label, $value) {
+            $this->_attributes[strtolower($label)] = $value;
+        }
+        
+        /**
+         *    Accessor for the whole content so far.
+         *    @return string       Content as big raw string.
+         *    @access public
+         */
+        function getContent() {
+            return $this->_content;
+        }
+        
+        /**
+         *    Accessor for content reduced to visible text. Acts
+         *    like a text mode browser, normalising space and
+         *    reducing images to their alt text.
+         *    @return string       Content as plain text.
+         *    @access public
+         */
+        function getText() {
+            return SimpleHtmlSaxParser::normalise($this->_content);
+        }
+        
+        /**
+         *    Test to see if id attribute matches.
+         *    @param string $id        ID to test against.
+         *    @return boolean          True on match.
+         *    @access public
+         */
+        function isId($id) {
+            return ($this->getAttribute('id') == $id);
+        }
+    }
+    
+    /**
+     *    Page title.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleTitleTag extends SimpleTag {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleTitleTag($attributes) {
+            $this->SimpleTag('title', $attributes);
+        }
+    }
+    
+    /**
+     *    Link.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleAnchorTag extends SimpleTag {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleAnchorTag($attributes) {
+            $this->SimpleTag('a', $attributes);
+        }
+        
+        /**
+         *    Accessor for URL as string.
+         *    @return string    Coerced as string.
+         *    @access public
+         */
+        function getHref() {
+            $url = $this->getAttribute('href');
+            if (is_bool($url)) {
+                $url = '';
+            }
+            return $url;
+        }
+    }
+    
+    /**
+     *    Form element.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleWidget extends SimpleTag {
+        var $_value;
+        var $_label;
+        var $_is_set;
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param string $name        Tag name.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleWidget($name, $attributes) {
+            $this->SimpleTag($name, $attributes);
+            $this->_value = false;
+            $this->_label = false;
+            $this->_is_set = false;
+        }
+        
+        /**
+         *    Accessor for name submitted as the key in
+         *    GET/POST variables hash.
+         *    @return string        Parsed value.
+         *    @access public
+         */
+        function getName() {
+            return $this->getAttribute('name');
+        }
+        
+        /**
+         *    Accessor for default value parsed with the tag.
+         *    @return string        Parsed value.
+         *    @access public
+         */
+        function getDefault() {
+            return $this->getAttribute('value');
+        }
+        
+        /**
+         *    Accessor for currently set value or default if
+         *    none.
+         *    @return string      Value set by form or default
+         *                        if none.
+         *    @access public
+         */
+        function getValue() {
+            if (! $this->_is_set) {
+                return $this->getDefault();
+            }
+            return $this->_value;
+        }
+        
+        /**
+         *    Sets the current form element value.
+         *    @param string $value       New value.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            $this->_value = $value;
+            $this->_is_set = true;
+            return true;
+        }
+        
+        /**
+         *    Resets the form element value back to the
+         *    default.
+         *    @access public
+         */
+        function resetValue() {
+            $this->_is_set = false;
+        }
+        
+        /**
+         *    Allows setting of a label externally, say by a
+         *    label tag.
+         *    @param string $label    Label to attach.
+         *    @access public
+         */
+        function setLabel($label) {
+            $this->_label = trim($label);
+        }
+        
+        /**
+         *    Reads external or internal label.
+         *    @param string $label    Label to test.
+         *    @return boolean         True is match.
+         *    @access public
+         */
+        function isLabel($label) {
+            return $this->_label == trim($label);
+        }
+        
+        /**
+         *    Dispatches the value into the form encoded packet.
+         *    @param SimpleEncoding $encoding    Form packet.
+         *    @access public
+         */
+        function write(&$encoding) {
+            if ($this->getName()) {
+                $encoding->add($this->getName(), $this->getValue());
+            }
+        }
+    }
+    
+    /**
+     *    Text, password and hidden field.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleTextTag extends SimpleWidget {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleTextTag($attributes) {
+            $this->SimpleWidget('input', $attributes);
+            if ($this->getAttribute('value') === false) {
+                $this->_setAttribute('value', '');
+            }
+        }
+        
+        /**
+         *    Tag contains no content.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+        
+        /**
+         *    Sets the current form element value. Cannot
+         *    change the value of a hidden field.
+         *    @param string $value       New value.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            if ($this->getAttribute('type') == 'hidden') {
+                return false;
+            }
+            return parent::setValue($value);
+        }
+    }
+    
+    /**
+     *    Submit button as input tag.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleSubmitTag extends SimpleWidget {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleSubmitTag($attributes) {
+            $this->SimpleWidget('input', $attributes);
+            if ($this->getAttribute('value') === false) {
+                $this->_setAttribute('value', 'Submit');
+            }
+        }
+        
+        /**
+         *    Tag contains no end element.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+        
+        /**
+         *    Disables the setting of the button value.
+         *    @param string $value       Ignored.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            return false;
+        }
+        
+        /**
+         *    Value of browser visible text.
+         *    @return string        Visible label.
+         *    @access public
+         */
+        function getLabel() {
+            return $this->getValue();
+        }
+        
+        /**
+         *    Test for a label match when searching.
+         *    @param string $label     Label to test.
+         *    @return boolean          True on match.
+         *    @access public
+         */
+        function isLabel($label) {
+            return trim($label) == trim($this->getLabel());
+        }
+    }
+      
+    /**
+     *    Image button as input tag.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleImageSubmitTag extends SimpleWidget {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleImageSubmitTag($attributes) {
+            $this->SimpleWidget('input', $attributes);
+        }
+        
+        /**
+         *    Tag contains no end element.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+        
+        /**
+         *    Disables the setting of the button value.
+         *    @param string $value       Ignored.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            return false;
+        }
+        
+        /**
+         *    Value of browser visible text.
+         *    @return string        Visible label.
+         *    @access public
+         */
+        function getLabel() {
+            if ($this->getAttribute('title')) {
+                return $this->getAttribute('title');
+            }
+            return $this->getAttribute('alt');
+        }
+        
+        /**
+         *    Test for a label match when searching.
+         *    @param string $label     Label to test.
+         *    @return boolean          True on match.
+         *    @access public
+         */
+        function isLabel($label) {
+            return trim($label) == trim($this->getLabel());
+        }
+        
+        /**
+         *    Dispatches the value into the form encoded packet.
+         *    @param SimpleEncoding $encoding    Form packet.
+         *    @param integer $x                  X coordinate of click.
+         *    @param integer $y                  Y coordinate of click.
+         *    @access public
+         */
+        function write(&$encoding, $x, $y) {
+            if ($this->getName()) {
+                $encoding->add($this->getName() . '.x', $x);
+                $encoding->add($this->getName() . '.y', $y);
+            } else {
+                $encoding->add('x', $x);
+                $encoding->add('y', $y);
+            }
+        }
+    }
+      
+    /**
+     *    Submit button as button tag.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleButtonTag extends SimpleWidget {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    Defaults are very browser dependent.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleButtonTag($attributes) {
+            $this->SimpleWidget('button', $attributes);
+        }
+        
+        /**
+         *    Check to see if the tag can have both start and
+         *    end tags with content in between.
+         *    @return boolean        True if content allowed.
+         *    @access public
+         */
+        function expectEndTag() {
+            return true;
+        }
+        
+        /**
+         *    Disables the setting of the button value.
+         *    @param string $value       Ignored.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            return false;
+        }
+        
+        /**
+         *    Value of browser visible text.
+         *    @return string        Visible label.
+         *    @access public
+         */
+        function getLabel() {
+            return $this->getContent();
+        }
+        
+        /**
+         *    Test for a label match when searching.
+         *    @param string $label     Label to test.
+         *    @return boolean          True on match.
+         *    @access public
+         */
+        function isLabel($label) {
+            return trim($label) == trim($this->getLabel());
+        }
+    }
+  
+    /**
+     *    Content tag for text area.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleTextAreaTag extends SimpleWidget {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleTextAreaTag($attributes) {
+            $this->SimpleWidget('textarea', $attributes);
+        }
+        
+        /**
+         *    Accessor for starting value.
+         *    @return string        Parsed value.
+         *    @access public
+         */
+        function getDefault() {
+            return $this->_wrap(SimpleHtmlSaxParser::decodeHtml($this->getContent()));
+        }
+        
+        /**
+         *    Applies word wrapping if needed.
+         *    @param string $value      New value.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            return parent::setValue($this->_wrap($value));
+        }
+        
+        /**
+         *    Test to see if text should be wrapped.
+         *    @return boolean        True if wrapping on.
+         *    @access private
+         */
+        function _wrapIsEnabled() {
+            if ($this->getAttribute('cols')) {
+                $wrap = $this->getAttribute('wrap');
+                if (($wrap == 'physical') || ($wrap == 'hard')) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Performs the formatting that is peculiar to
+         *    this tag. There is strange behaviour in this
+         *    one, including stripping a leading new line.
+         *    Go figure. I am using Firefox as a guide.
+         *    @param string $text    Text to wrap.
+         *    @return string         Text wrapped with carriage
+         *                           returns and line feeds
+         *    @access private
+         */
+        function _wrap($text) {
+            $text = str_replace("\r\r\n", "\r\n", str_replace("\n", "\r\n", $text));
+            $text = str_replace("\r\n\n", "\r\n", str_replace("\r", "\r\n", $text));
+            if (strncmp($text, "\r\n", strlen("\r\n")) == 0) {
+                $text = substr($text, strlen("\r\n"));
+            }
+            if ($this->_wrapIsEnabled()) {
+                return wordwrap(
+                        $text,
+                        (integer)$this->getAttribute('cols'),
+                        "\r\n");
+            }
+            return $text;
+        }
+        
+        /**
+         *    The content of textarea is not part of the page.
+         *    @return boolean        True.
+         *    @access public
+         */
+        function isPrivateContent() {
+            return true;
+        }
+    }
+    
+    /**
+     *    File upload widget.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleUploadTag extends SimpleWidget {
+        
+        /**
+         *    Starts with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleUploadTag($attributes) {
+            $this->SimpleWidget('input', $attributes);
+        }
+        
+        /**
+         *    Tag contains no content.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+        
+        /**
+         *    Dispatches the value into the form encoded packet.
+         *    @param SimpleEncoding $encoding    Form packet.
+         *    @access public
+         */
+        function write(&$encoding) {
+            if (! file_exists($this->getValue())) {
+                return;
+            }
+            $encoding->attach(
+                    $this->getName(),
+                    implode('', file($this->getValue())),
+                    basename($this->getValue()));
+        }
+    }
+    
+    /**
+     *    Drop down widget.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleSelectionTag extends SimpleWidget {
+        var $_options;
+        var $_choice;
+        
+        /**
+         *    Starts with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleSelectionTag($attributes) {
+            $this->SimpleWidget('select', $attributes);
+            $this->_options = array();
+            $this->_choice = false;
+        }
+        
+        /**
+         *    Adds an option tag to a selection field.
+         *    @param SimpleOptionTag $tag     New option.
+         *    @access public
+         */
+        function addTag(&$tag) {
+            if ($tag->getTagName() == 'option') {
+                $this->_options[] = &$tag;
+            }
+        }
+        
+        /**
+         *    Text within the selection element is ignored.
+         *    @param string $content        Ignored.
+         *    @access public
+         */
+        function addContent($content) {
+        }
+        
+        /**
+         *    Scans options for defaults. If none, then
+         *    the first option is selected.
+         *    @return string        Selected field.
+         *    @access public
+         */
+        function getDefault() {
+            for ($i = 0, $count = count($this->_options); $i < $count; $i++) {
+                if ($this->_options[$i]->getAttribute('selected') !== false) {
+                    return $this->_options[$i]->getDefault();
+                }
+            }
+            if ($count > 0) {
+                return $this->_options[0]->getDefault();
+            }
+            return '';
+        }
+        
+        /**
+         *    Can only set allowed values.
+         *    @param string $value       New choice.
+         *    @return boolean            True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            for ($i = 0, $count = count($this->_options); $i < $count; $i++) {
+                if ($this->_options[$i]->isValue($value)) {
+                    $this->_choice = $i;
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Accessor for current selection value.
+         *    @return string      Value attribute or
+         *                        content of opton.
+         *    @access public
+         */
+        function getValue() {
+            if ($this->_choice === false) {
+                return $this->getDefault();
+            }
+            return $this->_options[$this->_choice]->getValue();
+        }
+    }
+    
+    /**
+     *    Drop down widget.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class MultipleSelectionTag extends SimpleWidget {
+        var $_options;
+        var $_values;
+        
+        /**
+         *    Starts with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function MultipleSelectionTag($attributes) {
+            $this->SimpleWidget('select', $attributes);
+            $this->_options = array();
+            $this->_values = false;
+        }
+        
+        /**
+         *    Adds an option tag to a selection field.
+         *    @param SimpleOptionTag $tag     New option.
+         *    @access public
+         */
+        function addTag(&$tag) {
+            if ($tag->getTagName() == 'option') {
+                $this->_options[] = &$tag;
+            }
+        }
+        
+        /**
+         *    Text within the selection element is ignored.
+         *    @param string $content        Ignored.
+         *    @access public
+         */
+        function addContent($content) {
+        }
+        
+        /**
+         *    Scans options for defaults to populate the
+         *    value array().
+         *    @return array        Selected fields.
+         *    @access public
+         */
+        function getDefault() {
+            $default = array();
+            for ($i = 0, $count = count($this->_options); $i < $count; $i++) {
+                if ($this->_options[$i]->getAttribute('selected') !== false) {
+                    $default[] = $this->_options[$i]->getDefault();
+                }
+            }
+            return $default;
+        }
+        
+        /**
+         *    Can only set allowed values. Any illegal value
+         *    will result in a failure, but all correct values
+         *    will be set.
+         *    @param array $desired      New choices.
+         *    @return boolean            True if all allowed.
+         *    @access public
+         */
+        function setValue($desired) {
+            $achieved = array();
+            foreach ($desired as $value) {
+                $success = false;
+                for ($i = 0, $count = count($this->_options); $i < $count; $i++) {
+                    if ($this->_options[$i]->isValue($value)) {
+                        $achieved[] = $this->_options[$i]->getValue();
+                        $success = true;
+                        break;
+                    }
+                }
+                if (! $success) {
+                    return false;
+                }
+            }
+            $this->_values = $achieved;
+            return true;
+        }
+        
+        /**
+         *    Accessor for current selection value.
+         *    @return array      List of currently set options.
+         *    @access public
+         */
+        function getValue() {
+            if ($this->_values === false) {
+                return $this->getDefault();
+            }
+            return $this->_values;
+        }
+    }
+    
+    /**
+     *    Option for selection field.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleOptionTag extends SimpleWidget {
+        
+        /**
+         *    Stashes the attributes.
+         */
+        function SimpleOptionTag($attributes) {
+            $this->SimpleWidget('option', $attributes);
+        }
+        
+        /**
+         *    Does nothing.
+         *    @param string $value      Ignored.
+         *    @return boolean           Not allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            return false;
+        }
+        
+        /**
+         *    Test to see if a value matches the option.
+         *    @param string $compare    Value to compare with.
+         *    @return boolean           True if possible match.
+         *    @access public
+         */
+        function isValue($compare) {
+            $compare = trim($compare);
+            if (trim($this->getValue()) == $compare) {
+                return true;
+            }
+            return trim($this->getContent()) == $compare;
+        }
+        
+        /**
+         *    Accessor for starting value. Will be set to
+         *    the option label if no value exists.
+         *    @return string        Parsed value.
+         *    @access public
+         */
+        function getDefault() {
+            if ($this->getAttribute('value') === false) {
+                return $this->getContent();
+            }
+            return $this->getAttribute('value');
+        }
+        
+        /**
+         *    The content of options is not part of the page.
+         *    @return boolean        True.
+         *    @access public
+         */
+        function isPrivateContent() {
+            return true;
+        }
+    }
+    
+    /**
+     *    Radio button.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleRadioButtonTag extends SimpleWidget {
+        
+        /**
+         *    Stashes the attributes.
+         *    @param array $attributes        Hash of attributes.
+         */
+        function SimpleRadioButtonTag($attributes) {
+            $this->SimpleWidget('input', $attributes);
+            if ($this->getAttribute('value') === false) {
+                $this->_setAttribute('value', 'on');
+            }
+        }
+        
+        /**
+         *    Tag contains no content.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+        
+        /**
+         *    The only allowed value sn the one in the
+         *    "value" attribute.
+         *    @param string $value      New value.
+         *    @return boolean           True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            if ($value === false) {
+                return parent::setValue($value);
+            }
+            if ($value !== $this->getAttribute('value')) {
+                return false;
+            }
+            return parent::setValue($value);
+        }
+        
+        /**
+         *    Accessor for starting value.
+         *    @return string        Parsed value.
+         *    @access public
+         */
+        function getDefault() {
+            if ($this->getAttribute('checked') !== false) {
+                return $this->getAttribute('value');
+            }
+            return false;
+        }
+    }
+    
+    /**
+     *    Checkbox widget.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleCheckboxTag extends SimpleWidget {
+        
+        /**
+         *    Starts with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleCheckboxTag($attributes) {
+            $this->SimpleWidget('input', $attributes);
+            if ($this->getAttribute('value') === false) {
+                $this->_setAttribute('value', 'on');
+            }
+        }
+        
+        /**
+         *    Tag contains no content.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+        
+        /**
+         *    The only allowed value in the one in the
+         *    "value" attribute. The default for this
+         *    attribute is "on". If this widget is set to
+         *    true, then the usual value will be taken.
+         *    @param string $value      New value.
+         *    @return boolean           True if allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            if ($value === false) {
+                return parent::setValue($value);
+            }
+            if ($value === true) {
+                return parent::setValue($this->getAttribute('value'));
+            }
+            if ($value != $this->getAttribute('value')) {
+                return false;
+            }
+            return parent::setValue($value);
+        }
+        
+        /**
+         *    Accessor for starting value. The default
+         *    value is "on".
+         *    @return string        Parsed value.
+         *    @access public
+         */
+        function getDefault() {
+            if ($this->getAttribute('checked') !== false) {
+                return $this->getAttribute('value');
+            }
+            return false;
+        }
+    }
+    
+    /**
+     *    A group of multiple widgets with some shared behaviour.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleTagGroup {
+        var $_widgets = array();
+
+        /**
+         *    Adds a tag to the group.
+         *    @param SimpleWidget $widget
+         *    @access public
+         */
+        function addWidget(&$widget) {
+            $this->_widgets[] = &$widget;
+        }
+        
+        /**
+         *    Accessor to widget set.
+         *    @return array        All widgets.
+         *    @access protected
+         */
+        function &_getWidgets() {
+            return $this->_widgets;
+        }
+
+        /**
+         *    Accessor for an attribute.
+         *    @param string $label    Attribute name.
+         *    @return boolean         Always false.
+         *    @access public
+         */
+        function getAttribute($label) {
+            return false;
+        }
+        
+        /**
+         *    Fetches the name for the widget from the first
+         *    member.
+         *    @return string        Name of widget.
+         *    @access public
+         */
+        function getName() {
+            if (count($this->_widgets) > 0) {
+                return $this->_widgets[0]->getName();
+            }
+        }
+        
+        /**
+         *    Scans the widgets for one with the appropriate
+         *    ID field.
+         *    @param string $id        ID value to try.
+         *    @return boolean          True if matched.
+         *    @access public
+         */
+        function isId($id) {
+            for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) {
+                if ($this->_widgets[$i]->isId($id)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Scans the widgets for one with the appropriate
+         *    attached label.
+         *    @param string $label     Attached label to try.
+         *    @return boolean          True if matched.
+         *    @access public
+         */
+        function isLabel($label) {
+            for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) {
+                if ($this->_widgets[$i]->isLabel($label)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Dispatches the value into the form encoded packet.
+         *    @param SimpleEncoding $encoding    Form packet.
+         *    @access public
+         */
+        function write(&$encoding) {
+            $encoding->add($this->getName(), $this->getValue());
+        }
+    }
+
+    /**
+     *    A group of tags with the same name within a form.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleCheckboxGroup extends SimpleTagGroup {
+        
+        /**
+         *    Accessor for current selected widget or false
+         *    if none.
+         *    @return string/array     Widget values or false if none.
+         *    @access public
+         */
+        function getValue() {
+            $values = array();
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                if ($widgets[$i]->getValue() !== false) {
+                    $values[] = $widgets[$i]->getValue();
+                }
+            }
+            return $this->_coerceValues($values);
+        }
+        
+        /**
+         *    Accessor for starting value that is active.
+         *    @return string/array      Widget values or false if none.
+         *    @access public
+         */
+        function getDefault() {
+            $values = array();
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                if ($widgets[$i]->getDefault() !== false) {
+                    $values[] = $widgets[$i]->getDefault();
+                }
+            }
+            return $this->_coerceValues($values);
+        }
+        
+        /**
+         *    Accessor for current set values.
+         *    @param string/array/boolean $values   Either a single string, a
+         *                                          hash or false for nothing set.
+         *    @return boolean                       True if all values can be set.
+         *    @access public
+         */
+        function setValue($values) {
+            $values = $this->_makeArray($values);
+            if (! $this->_valuesArePossible($values)) {
+                return false;
+            }
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                $possible = $widgets[$i]->getAttribute('value');
+                if (in_array($widgets[$i]->getAttribute('value'), $values)) {
+                    $widgets[$i]->setValue($possible);
+                } else {
+                    $widgets[$i]->setValue(false);
+                }
+            }
+            return true;
+        }
+        
+        /**
+         *    Tests to see if a possible value set is legal.
+         *    @param string/array/boolean $values   Either a single string, a
+         *                                          hash or false for nothing set.
+         *    @return boolean                       False if trying to set a
+         *                                          missing value.
+         *    @access private
+         */
+        function _valuesArePossible($values) {
+            $matches = array();
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                $possible = $widgets[$i]->getAttribute('value');
+                if (in_array($possible, $values)) {
+                    $matches[] = $possible;
+                }
+            }
+            return ($values == $matches);
+        }
+        
+        /**
+         *    Converts the output to an appropriate format. This means
+         *    that no values is false, a single value is just that
+         *    value and only two or more are contained in an array.
+         *    @param array $values           List of values of widgets.
+         *    @return string/array/boolean   Expected format for a tag.
+         *    @access private
+         */
+        function _coerceValues($values) {
+            if (count($values) == 0) {
+                return false;
+            } elseif (count($values) == 1) {
+                return $values[0];
+            } else {
+                return $values;
+            }
+        }
+        
+        /**
+         *    Converts false or string into array. The opposite of
+         *    the coercian method.
+         *    @param string/array/boolean $value  A single item is converted
+         *                                        to a one item list. False
+         *                                        gives an empty list.
+         *    @return array                       List of values, possibly empty.
+         *    @access private
+         */
+        function _makeArray($value) {
+            if ($value === false) {
+                return array();
+            }
+            if (is_string($value)) {
+                return array($value);
+            }
+            return $value;
+        }
+    }
+
+    /**
+     *    A group of tags with the same name within a form.
+     *    Used for radio buttons.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleRadioGroup extends SimpleTagGroup {
+        
+        /**
+         *    Each tag is tried in turn until one is
+         *    successfully set. The others will be
+         *    unchecked if successful.
+         *    @param string $value      New value.
+         *    @return boolean           True if any allowed.
+         *    @access public
+         */
+        function setValue($value) {
+            if (! $this->_valueIsPossible($value)) {
+                return false;
+            }
+            $index = false;
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                if (! $widgets[$i]->setValue($value)) {
+                    $widgets[$i]->setValue(false);
+                }
+            }
+            return true;
+        }
+        
+        /**
+         *    Tests to see if a value is allowed.
+         *    @param string    Attempted value.
+         *    @return boolean  True if a valid value.
+         *    @access private
+         */
+        function _valueIsPossible($value) {
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                if ($widgets[$i]->getAttribute('value') == $value) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Accessor for current selected widget or false
+         *    if none.
+         *    @return string/boolean   Value attribute or
+         *                             content of opton.
+         *    @access public
+         */
+        function getValue() {
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                if ($widgets[$i]->getValue() !== false) {
+                    return $widgets[$i]->getValue();
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Accessor for starting value that is active.
+         *    @return string/boolean      Value of first checked
+         *                                widget or false if none.
+         *    @access public
+         */
+        function getDefault() {
+            $widgets = &$this->_getWidgets();
+            for ($i = 0, $count = count($widgets); $i < $count; $i++) {
+                if ($widgets[$i]->getDefault() !== false) {
+                    return $widgets[$i]->getDefault();
+                }
+            }
+            return false;
+        }
+    }
+    
+    /**
+     *    Tag to keep track of labels.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleLabelTag extends SimpleTag {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleLabelTag($attributes) {
+            $this->SimpleTag('label', $attributes);
+        }
+        
+        /**
+         *    Access for the ID to attach the label to.
+         *    @return string        For attribute.
+         *    @access public
+         */
+        function getFor() {
+            return $this->getAttribute('for');
+        }
+    }
+    
+    /**
+     *    Tag to aid parsing the form.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleFormTag extends SimpleTag {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleFormTag($attributes) {
+            $this->SimpleTag('form', $attributes);
+        }
+    }
+    
+    /**
+     *    Tag to aid parsing the frames in a page.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleFrameTag extends SimpleTag {
+        
+        /**
+         *    Starts with a named tag with attributes only.
+         *    @param hash $attributes    Attribute names and
+         *                               string values.
+         */
+        function SimpleFrameTag($attributes) {
+            $this->SimpleTag('frame', $attributes);
+        }
+        
+        /**
+         *    Tag contains no content.
+         *    @return boolean        False.
+         *    @access public
+         */
+        function expectEndTag() {
+            return false;
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/test_case.php b/lib/simpletestlib/test_case.php
new file mode 100644 (file)
index 0000000..c2f56f0
--- /dev/null
@@ -0,0 +1,684 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * Includes SimpleTest files and defined the root constant
+     * for dependent libraries.
+     */
+    require_once(dirname(__FILE__) . '/invoker.php');
+    require_once(dirname(__FILE__) . '/errors.php');
+    require_once(dirname(__FILE__) . '/compatibility.php');
+    require_once(dirname(__FILE__) . '/scorer.php');
+    require_once(dirname(__FILE__) . '/expectation.php');
+    require_once(dirname(__FILE__) . '/dumper.php');
+    require_once(dirname(__FILE__) . '/simpletest.php');
+    if (version_compare(phpversion(), '5') >= 0) {
+        require_once(dirname(__FILE__) . '/exceptions.php');
+        require_once(dirname(__FILE__) . '/reflection_php5.php');
+    } else {
+        require_once(dirname(__FILE__) . '/reflection_php4.php');
+    }
+    if (! defined('SIMPLE_TEST')) {
+        /**
+         * @ignore
+         */
+        define('SIMPLE_TEST', dirname(__FILE__) . '/');
+    }
+    /**#@-*/
+
+    /**
+     *    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.
+        *    @package          SimpleTest
+        *    @subpackage       UnitTester
+     */
+    class SimpleTestCase {
+        var $_label = false;
+        var $_reporter;
+        var $_observers;
+
+        /**
+         *    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);
+        }
+
+        /**
+         *    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.
+         *    @access public
+         */
+        function run(&$reporter) {
+            SimpleTest::setCurrent($this);
+            $this->_reporter = &$reporter;
+            $this->_reporter->paintCaseStart($this->getLabel());
+            foreach ($this->getTests() as $method) {
+                if ($this->_reporter->shouldInvoke($this->getLabel(), $method)) {
+                    $invoker = &$this->_reporter->createInvoker($this->createInvoker());
+                    $invoker->before($method);
+                    $invoker->invoke($method);
+                    $invoker->after($method);
+                }
+            }
+            $this->_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 ! SimpleTestCompatibility::isA($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->_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;
+        }
+
+        /**
+         *    Sends a pass event with a message.
+         *    @param string $message        Message to send.
+         *    @access public
+         */
+        function pass($message = "Pass") {
+            if (! isset($this->_reporter)) {
+                trigger_error('Can only make assertions within test methods');
+            }
+            $this->_reporter->paintPass(
+                    $message . $this->getAssertionLine());
+            return true;
+        }
+
+        /**
+         *    Sends a fail event with a message.
+         *    @param string $message        Message to send.
+         *    @access public
+         */
+        function fail($message = "Fail") {
+            if (! isset($this->_reporter)) {
+                trigger_error('Can only make assertions within test methods');
+            }
+            $this->_reporter->paintFail(
+                    $message . $this->getAssertionLine());
+            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->paintError(
+                    'Unexpected exception of type [' . get_class($exception) .
+                    '] with message ['. $exception->getMessage() .
+                    '] in ['. $exception->getFile() .
+                    '] line [' . $exception->getLine() . ']');
+        }
+
+        /**
+         *    Sends a user defined event to the test reporter.
+         *    This is for small scale extension where
+         *    both the test case and either the reporter or
+         *    display are subclassed.
+         *    @param string $type       Type of event.
+         *    @param mixed $payload     Object or message to deliver.
+         *    @access public
+         */
+        function signal($type, &$payload) {
+            if (! isset($this->_reporter)) {
+                trigger_error('Can only make assertions within test methods');
+            }
+            $this->_reporter->paintSignal($type, $payload);
+        }
+
+        /**
+         *    Cancels any outstanding errors.
+         *    @access public
+         */
+        function swallowErrors() {
+            $queue = &SimpleErrorQueue::instance();
+            $queue->clear();
+        }
+
+        /**
+         *    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') {
+            return $this->assertTrue(
+                    $expectation->test($compare),
+                    sprintf($message, $expectation->overlayMessage($compare)));
+        }
+
+        /**
+         *       @deprecated
+         */
+        function assertExpectation(&$expectation, $compare, $message = '%s') {
+               return $this->assert($expectation, $compare, $message);
+        }
+
+        /**
+         *    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) {
+            if (! $message) {
+                $message = 'True assertion got ' . ($result ? 'True' : 'False');
+            }
+            if ($result) {
+                return $this->pass($message);
+            } else {
+                return $this->fail($message);
+            }
+        }
+
+        /**
+         *    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 = false) {
+            if (! $message) {
+                $message = 'False assertion got ' . ($result ? 'True' : 'False');
+            }
+            return $this->assertTrue(! $result, $message);
+        }
+
+        /**
+         *    Uses a stack trace to find the line of an assertion.
+         *    @param string $format    String formatting.
+         *    @param array $stack      Stack frames top most first. Only
+         *                             needed if not using the PHP
+         *                             backtrace function.
+         *    @return string           Line number of first assert*
+         *                             method embedded in format string.
+         *    @access public
+         */
+        function getAssertionLine($stack = false) {
+            if ($stack === false) {
+                $stack = SimpleTestCompatibility::getStackTrace();
+            }
+            return SimpleDumper::getFormattedAssertionLine($stack);
+        }
+
+        /**
+         *    Sends a formatted dump of a variable to the
+         *    test suite for those emergency debugging
+         *    situations.
+         *    @param mixed $variable    Variable to display.
+         *    @param string $message    Message to display.
+         *    @return mixed             The original variable.
+         *    @access public
+         */
+        function dump($variable, $message = false) {
+            $formatted = SimpleDumper::dump($variable);
+            if ($message) {
+                $formatted = $message . "\n" . $formatted;
+            }
+            $this->_reporter->paintFormattedMessage($formatted);
+            return $variable;
+        }
+
+        /**
+         *    Dispatches a text message straight to the
+         *    test suite. Useful for status bar displays.
+         *    @param string $message        Message to show.
+         *    @access public
+         */
+        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;
+        }
+    }
+
+    /**
+     *    This is a composite test class for combining
+     *    test cases and other RunnableTest classes into
+     *    a group test.
+        *    @package          SimpleTest
+        *    @subpackage       UnitTester
+     */
+    class GroupTest {
+        var $_label;
+        var $_test_cases;
+        var $_old_track_errors;
+        var $_xdebug_is_enabled;
+
+        /**
+         *    Sets the name of the test suite.
+         *    @param string $label    Name sent at the start and end
+         *                            of the test.
+         *    @access public
+         */
+        function GroupTest($label = false) {
+            $this->_label = $label ? $label : get_class($this);
+            $this->_test_cases = array();
+            $this->_old_track_errors = ini_get('track_errors');
+            $this->_xdebug_is_enabled = function_exists('xdebug_is_enabled') ?
+                    xdebug_is_enabled() : false;
+        }
+
+        /**
+         *    Accessor for the test name for subclasses.
+         *    @return string           Name of the test.
+         *    @access public
+         */
+        function getLabel() {
+            return $this->_label;
+        }
+
+        /**
+         *    Adds a test into the suite. Can be either a group
+         *    test or some other unit test.
+         *    @param SimpleTestCase $test_case  Suite or individual test
+         *                                      case implementing the
+         *                                      runnable test interface.
+         *    @access public
+         */
+        function addTestCase(&$test_case) {
+            $this->_test_cases[] = &$test_case;
+        }
+
+        /**
+         *    Adds a test into the suite by class name. The class will
+         *    be instantiated as needed.
+         *    @param SimpleTestCase $test_case  Suite or individual test
+         *                                      case implementing the
+         *                                      runnable test interface.
+         *    @access public
+         */
+        function addTestClass($class) {
+            if ($this->_getBaseTestCase($class) == 'grouptest') {
+                $this->_test_cases[] = &new $class();
+            } else {
+                $this->_test_cases[] = $class;
+            }
+        }
+
+        /**
+         *    Builds a group test from a library of test cases.
+         *    The new group is composed into this one.
+         *    @param string $test_file        File name of library with
+         *                                    test case classes.
+         *    @access public
+         */
+        function addTestFile($test_file) {
+            $existing_classes = get_declared_classes();
+            if ($error = $this->_requireWithError($test_file)) {
+                $this->addTestCase(new BadGroupTest($test_file, $error));
+                return;
+            }
+            $classes = $this->_selectRunnableTests($existing_classes, get_declared_classes());
+            if (count($classes) == 0) {
+                $this->addTestCase(new BadGroupTest($test_file, "No runnable test cases in [$test_file]"));
+                return;
+            }
+            $group = &$this->_createGroupFromClasses($test_file, $classes);
+            $this->addTestCase($group);
+        }
+
+        /**
+         *    Requires a source file recording any syntax errors.
+         *    @param string $file        File name to require in.
+         *    @return string/boolean     An error message on failure or false
+         *                               if no errors.
+         *    @access private
+         */
+        function _requireWithError($file) {
+            $this->_enableErrorReporting();
+            include($file);
+            $error = isset($php_errormsg) ? $php_errormsg : false;
+            $this->_disableErrorReporting();
+            $self_inflicted_errors = array(
+                    'Assigning the return value of new by reference is deprecated',
+                    'var: Deprecated. Please use the public/private/protected modifiers');
+            if (in_array($error, $self_inflicted_errors)) {
+                return false;
+            }
+            return $error;
+        }
+
+        /**
+         *    Sets up detection of parse errors. Note that XDebug
+         *    interferes with this and has to be disabled. This is
+         *    to make sure the correct error code is returned
+         *    from unattended scripts.
+         *    @access private
+         */
+        function _enableErrorReporting() {
+            if ($this->_xdebug_is_enabled) {
+                xdebug_disable();
+            }
+            ini_set('track_errors', true);
+        }
+
+        /**
+         *    Resets detection of parse errors to their old values.
+         *    This is to make sure the correct error code is returned
+         *    from unattended scripts.
+         *    @access private
+         */
+        function _disableErrorReporting() {
+            ini_set('track_errors', $this->_old_track_errors);
+            if ($this->_xdebug_is_enabled) {
+                xdebug_enable();
+            }
+        }
+
+        /**
+         *    Calculates the incoming test cases from a before
+         *    and after list of loaded classes. Skips abstract
+         *    classes.
+         *    @param array $existing_classes   Classes before require().
+         *    @param array $new_classes        Classes after require().
+         *    @return array                    New classes which are test
+         *                                     cases that shouldn't be ignored.
+         *    @access private
+         */
+        function _selectRunnableTests($existing_classes, $new_classes) {
+            $classes = array();
+            foreach ($new_classes as $class) {
+                if (in_array($class, $existing_classes)) {
+                    continue;
+                }
+                if ($this->_getBaseTestCase($class)) {
+                    $reflection = new SimpleReflection($class);
+                    if ($reflection->isAbstract()) {
+                        SimpleTest::ignore($class);
+                    }
+                    $classes[] = $class;
+                }
+            }
+            return $classes;
+        }
+
+        /**
+         *    Builds a group test from a class list.
+         *    @param string $title       Title of new group.
+         *    @param array $classes      Test classes.
+         *    @return GroupTest          Group loaded with the new
+         *                               test cases.
+         *    @access private
+         */
+        function &_createGroupFromClasses($title, $classes) {
+            SimpleTest::ignoreParentsIfIgnored($classes);
+            $group = &new GroupTest($title);
+            foreach ($classes as $class) {
+                if (! SimpleTest::isIgnored($class)) {
+                    $group->addTestClass($class);
+                }
+            }
+            return $group;
+        }
+
+        /**
+         *    Test to see if a class is derived from the
+         *    SimpleTestCase class.
+         *    @param string $class     Class name.
+         *    @access private
+         */
+        function _getBaseTestCase($class) {
+            while ($class = get_parent_class($class)) {
+                $class = strtolower($class);
+                if ($class == "simpletestcase" || $class == "grouptest") {
+                    return $class;
+                }
+            }
+            return false;
+        }
+
+        /**
+         *    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);
+                } 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;
+        }
+    }
+
+    /**
+     *    This is a failing group test for when a test suite hasn't
+     *    loaded properly.
+        *    @package          SimpleTest
+        *    @subpackage       UnitTester
+     */
+    class BadGroupTest {
+        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 BadGroupTest($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 GroupTest [' . $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;
+        }
+    }
+?>
diff --git a/lib/simpletestlib/ui/colortext_reporter.php b/lib/simpletestlib/ui/colortext_reporter.php
new file mode 100644 (file)
index 0000000..5f0696a
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+    /**
+     *  Base include file for SimpleTest
+     *  @package        SimpleTest
+     *  @subpackage     UnitTester
+     *  @version        $Id$
+     */
+
+/**
+ * include base reporter
+ */
+require_once(dirname(__FILE__) . '/../reporter.php');
+
+
+/**
+ * Provides an ANSI-colored {@link TextReporter} for viewing test results.
+ *
+ * This code is made available under the same terms as SimpleTest.  It is based
+ * off of code that Jason Sweat originally published on the SimpleTest mailing
+ * list. 
+ *
+ * @author Jason Sweat (original code)
+ * @author Travis Swicegood <development@domain51.com>
+ * @package SimpleTest
+ * @subpackage UnitTester
+ */
+class ColorTextReporter extends TextReporter { 
+    var $_failColor = 41;
+    var $_passColor = 42;
+    
+    /**
+     * Handle initialization
+     *
+     * @param {@link TextReporter}
+     */
+    function ColorTextReporter() {
+        parent::TextReporter();
+    }
+    
+    /**
+     * Capture the attempt to display the final test results and insert the 
+     * ANSI-color codes in place.
+     *
+     * @param string
+     * @see TextReporter
+     * @access public
+     */
+    function paintFooter($test_name) {
+        ob_start();
+        parent::paintFooter($test_name);
+        $output = trim(ob_get_clean());
+        if ($output) {
+            if (($this->getFailCount() + $this->getExceptionCount()) == 0) {
+                $color = $this->_passColor;
+            } else {
+                $color = $this->_failColor;
+            }
+            
+            $this->_setColor($color);
+            echo $output;
+            $this->_resetColor();
+        }
+    }
+    
+    
+    /**
+     * Sets the terminal to an ANSI-standard $color
+     *
+     * @param int
+     * @access protected
+     */
+    function _setColor($color) {
+        printf("%s[%sm\n", chr(27), $color);
+    }
+    
+    
+    /**
+     * Resets the color back to normal.
+     *
+     * @access protected
+     */
+    function _resetColor() {
+        $this->_setColor(0);
+    }
+}
+
diff --git a/lib/simpletestlib/ui/css/webunit.css b/lib/simpletestlib/ui/css/webunit.css
new file mode 100644 (file)
index 0000000..49da468
--- /dev/null
@@ -0,0 +1,78 @@
+html {
+       font-face: Verdana, Arial, Helvetica, sans-serif;
+       font-weight: bold;
+       color: black;
+       }       
+
+.pass { color: green; } 
+.fail { color: red; } 
+.activetab {
+       position: relative;
+       background: white;
+       border-color: black;
+       border-style: solid;
+       border-weight: 1;
+       border-top-color: white;
+       border-style: solid;
+       border-weight: 1;
+}
+.inactivetab{
+       position: relative;
+       background: silver;
+       border-color: black;
+       border-style: solid;
+       border-weight: 1;
+}
+span.inactivetab a:link ,
+span.activetab a:link {
+       text-decoration: none;
+       color: black;
+}
+pre { background-color: lightgray; }
+#wait {
+       background: #B7B8DD;
+       position: absolute;
+       visibility: hidden;
+       text-align: center;
+       border-color: blue;
+       border-style: solid;
+       border-weight: 2;
+       }
+#webunit {
+       position: absolute;
+       visibility: hidden;
+       background: silver;
+       border-color: black;
+       border-style: solid;
+       border-weight: 2;
+       }
+#visible_tab {
+       position: relative;
+       visibility: hidden;
+       overflow: auto;
+       background: white;
+       width: 100%;
+       height: 100%;
+       border-color: black;
+       border-style: solid;
+       border-weight: 1;
+       }
+#visible_tab a:link {
+       text-decoration: none;
+       color: inherit;
+}
+#msg {
+       position: absolute;
+       visibility: hidden;
+       overflow: auto;
+       background: white;
+       border-color: black;
+       border-style: solid;
+       border-weight: 1;
+       }
+#fail,
+#tree {
+       position: absolute;
+       visibility: hidden;
+}
+
diff --git a/lib/simpletestlib/ui/img/wait.gif b/lib/simpletestlib/ui/img/wait.gif
new file mode 100644 (file)
index 0000000..a31781c
Binary files /dev/null and b/lib/simpletestlib/ui/img/wait.gif differ
diff --git a/lib/simpletestlib/ui/js/webunit.js b/lib/simpletestlib/ui/js/webunit.js
new file mode 100644 (file)
index 0000000..9612d0b
--- /dev/null
@@ -0,0 +1,194 @@
+// jsreporter.js
+// Script to support JsReporter class
+// Relies heavily on the X library in x.js
+//     X v3.14.1, Cross-Browser DHTML Library from Cross-Browser.com
+// Copyright (c) 2004 Jason E. Sweat (jsweat_php@yahoo.com)
+// 
+// SimpleTest - http://simpletest.sf.net/
+// Copyright (c) 2003,2004 Marcus Baker (marcus@lastcraft.com)
+// $Id$
+
+
+// Variables:
+min_x=500;
+min_y=400;
+groups = new Array();
+cases = new Array();
+methods = new Array();
+current_group=0;
+current_case=0;
+current_method=0;
+
+Hash = {
+       Set : function(foo,bar) {this[foo] = bar;},
+       Get : function(foo) {return this[foo];}
+}
+
+// Functions:
+function wait_start() {
+  var wait_x;
+  var wait_y;
+
+  wait_x = xWidth('wait');
+  wait_y = xHeight('wait');
+  xMoveTo('wait', (xClientWidth()-wait_x)/2, (xClientHeight()-wait_y)/2);
+  xShow('wait');
+}
+
+function layout() {
+       xResizeTo('webunit', max(xClientWidth()-30,min_x), max(xClientHeight()-20,min_y));
+       xMoveTo('webunit', 5, 5);
+       xResizeTo('tabs', xWidth('webunit')-10, xHeight('webunit')/3);
+       xLeft('tabs', 5);
+       xShow('webunit');
+       xShow('tabs');
+       activate_tab('fail');
+       xShow('visible_tab');
+       xZIndex('visible_tab', 2)
+       xResizeTo('msg', xWidth('webunit')-17, xHeight('webunit')/3-20);
+       xLeft('msg', 2);
+       xTop('msg',2*xHeight('webunit')/3);
+       xShow('msg');
+}
+
+function set_div_content(div, content) {
+       xGetElementById(div).innerHTML = content;
+}
+
+function copy_div_content(divsrc, divtrgt) {
+       xGetElementById(divtrgt).innerHTML = xGetElementById(divsrc).innerHTML;
+}
+
+function activate_tab(tab) {
+       if (tab == 'fail') {
+               copy_div_content('fail', 'visible_tab');
+               xGetElementById('failtab').className = 'activetab';
+               xZIndex('failtab', 3)
+               xGetElementById('treetab').className = 'inactivetab';
+               xZIndex('treetab', 1)
+       }
+       if (tab == 'tree') {
+               copy_div_content('tree', 'visible_tab');
+               xGetElementById('failtab').className = 'inactivetab';
+               xZIndex('failtab', 1)
+               xGetElementById('treetab').className = 'activetab';
+               xZIndex('treetab', 3)
+       }
+}
+
+function add_group(group_name) {
+  var add;
+  
+  add = {
+               Set : function(foo,bar) {this[foo] = bar;},
+               Get : function(foo) {return this[foo];}
+  }
+  add.Set('desc', group_name);
+  add.Set('pass', true);
+  groups[groups.length] = add;
+  current_group = groups.length - 1;
+  cases[current_group] = new Array();
+  methods[current_group] = new Array();
+}
+
+function add_case(case_name) {
+  var curgroup;
+  var add;
+  
+  add = {
+               Set : function(foo,bar) {this[foo] = bar;},
+               Get : function(foo) {return this[foo];}
+  }
+  add.Set('desc', case_name);
+  add.Set('pass', true);
+  curgroup = cases[current_group];
+  cases[current_group][curgroup.length] = add;
+  current_case = curgroup.length - 1;
+  methods[current_group][current_case] = new Array();
+}
+
+function add_method(method_name) {
+       var curcase;
+  var add;
+  
+  add = {
+               Set : function(foo,bar) {this[foo] = bar;},
+               Get : function(foo) {return this[foo];}
+  }
+  add.Set('desc', method_name);
+  add.Set('pass', true);
+  add.Set('msg','');
+       curcase = methods[current_group][current_case];
+       methods[current_group][current_case][curcase.length] = add;
+       current_method = curcase.length - 1;
+}
+
+function add_fail(msg) {
+  var oldmsg;
+  add_log(msg);
+  groups[current_group].Set('pass', false);
+  cases[current_group][current_case].Set('pass', false);
+  methods[current_group][current_case][current_method].Set('pass', false);
+  oldmsg = methods[current_group][current_case][current_method].Get('msg');
+  methods[current_group][current_case][current_method].Set('msg', oldmsg+msg);
+}
+
+function add_log(msg) {
+  var faildiv;
+  faildiv = xGetElementById('fail');
+  faildiv.innerHTML = faildiv.innerHTML + msg;
+}
+
+function set_msg(gid, cid, mid) {
+       var passfail;
+       var msg=methods[gid][cid][mid].Get('msg');
+       if ('' == msg) {
+               passfail = (methods[gid][cid][mid].Get('pass')) ? 'pass' : 'fail';
+         msg = 'No output for <span class="' + passfail + '">'
+               + groups[gid].Get('desc') + '-&gt;'
+               + cases[gid][cid].Get('desc') + '-&gt;'
+               + methods[gid][cid][mid].Get('desc') + '</span><br />';
+       }
+  xGetElementById('msg').innerHTML = msg;
+}
+
+function make_tree() {
+       var content;
+       var passfail;
+       content = '<ul>';
+       for (x in groups) {
+         passfail = (groups[x].Get('pass')) ? 'pass' : 'fail'; 
+               content += '<li class="'+passfail+'">'+groups[x].Get('desc')+'<ul>';
+               for (y in cases[x]) {
+           passfail = (cases[x][y].Get('pass')) ? 'pass' : 'fail';     
+                       content += '<li class="'+passfail+'">'+cases[x][y].Get('desc')+'<ul>';
+                       for (z in methods[x][y]) {
+             passfail = (methods[x][y][z].Get('pass')) ? 'pass' : 'fail';      
+                         content += '<li class="'+passfail+'"><a href="javascript:set_msg('+x+','+y+','+z+')">'+methods[x][y][z].Get('desc')+'</a></li>';
+                       }
+                       content += '</ul></li>';
+               }
+               content += '</ul></li>';
+       }
+       content += '</ul>';
+       xGetElementById('tree').innerHTML = content;
+       if (xGetElementById('treetab').className == 'activetab') { 
+         activate_tab('tree'); 
+       } else {
+         activate_tab('fail'); 
+       }
+}
+
+function make_output(data) { 
+}
+
+function make_fail_msg(id, msg) {
+}
+
+function max(n1, n2) {
+  if (n1 > n2) {
+       return n1;
+  } else {
+       return n2;
+  }
+}
diff --git a/lib/simpletestlib/ui/js/x.js b/lib/simpletestlib/ui/js/x.js
new file mode 100644 (file)
index 0000000..78edb02
--- /dev/null
@@ -0,0 +1,423 @@
+// x.js
+// X v3.14.1, Cross-Browser DHTML Library from Cross-Browser.com
+// Copyright (c) 2002,2003 Michael Foster (mike@cross-browser.com)
+// This library is distributed under the terms of the LGPL (gnu.org)
+
+// Variables:
+var xVersion='3.14.1',xOp7=false,xOp5or6=false,xIE4Up=false,xNN4=false,xUA=navigator.userAgent.toLowerCase();
+if(window.opera){
+  xOp7=(xUA.indexOf('opera 7')!=-1 || xUA.indexOf('opera/7')!=-1);
+  if (!xOp7) xOp5or6=(xUA.indexOf('opera 5')!=-1 || xUA.indexOf('opera/5')!=-1 || xUA.indexOf('opera 6')!=-1 || xUA.indexOf('opera/6')!=-1);
+}
+else if(document.layers) {xNN4=true;}
+else {xIE4Up=document.all && xUA.indexOf('msie')!=-1 && parseInt(navigator.appVersion)>=4;}
+// Object:
+function xGetElementById(e) {
+  if(typeof(e)!='string') return e;
+  if(document.getElementById) e=document.getElementById(e);
+  else if(document.all) e=document.all[e];
+  else if(document.layers) e=xLayer(e);
+  else e=null;
+  return e;
+}
+function xParent(e,bNode){
+  if (!(e=xGetElementById(e))) return null;
+  var p=null;
+  if (!bNode && xDef(e.offsetParent)) p=e.offsetParent;
+  else if (xDef(e.parentNode)) p=e.parentNode;
+  else if (xDef(e.parentElement)) p=e.parentElement;
+  else if (xDef(e.parentLayer)){if (e.parentLayer!=window) p=e.parentLayer;}
+  return p;
+}
+function xDef() {
+  for(var i=0; i<arguments.length; ++i){if(typeof(arguments[i])=='undefined') return false;}
+  return true;
+}
+function xStr(s) {
+  return typeof(s)=='string';
+}
+function xNum(n) {
+  return typeof(n)=='number';
+}
+// Appearance:
+function xShow(e) {
+  if(!(e=xGetElementById(e))) return;
+  if(e.style && xDef(e.style.visibility)) e.style.visibility='visible';
+  else if(xDef(e.visibility)) e.visibility='show';
+}
+function xHide(e) {
+  if(!(e=xGetElementById(e))) return;
+  if(e.style && xDef(e.style.visibility)) e.style.visibility='hidden';
+  else if(xDef(e.visibility)) e.visibility='hide';
+}
+function xZIndex(e,uZ) {
+  if(!(e=xGetElementById(e))) return 0;
+  if(e.style && xDef(e.style.zIndex)) {
+    if(xNum(uZ)) e.style.zIndex=uZ;
+    uZ=parseInt(e.style.zIndex);
+  }
+  else if(xDef(e.zIndex)) {
+    if(xNum(uZ)) e.zIndex=uZ;
+    uZ=e.zIndex;
+  }
+  return uZ;
+}
+function xColor(e,sColor) {
+  if(!(e=xGetElementById(e))) return '';
+  var c='';
+  if(e.style && xDef(e.style.color)) {
+    if(xStr(sColor)) e.style.color=sColor;
+    c=e.style.color;
+  }
+  return c;
+}
+function xBackground(e,sColor,sImage) {
+  if(!(e=xGetElementById(e))) return '';
+  var bg='';
+  if(e.style) {
+    if(xStr(sColor)) {
+      if(!xOp5or6) e.style.backgroundColor=sColor;
+      else e.style.background=sColor;
+    }
+    if(xStr(sImage)) e.style.backgroundImage=(sImage!='')? 'url('+sImage+')' : null;
+    if(!xOp5or6) bg=e.style.backgroundColor;
+    else bg=e.style.background;
+  }
+  else if(xDef(e.bgColor)) {
+    if(xStr(sColor)) e.bgColor=sColor;
+    bg=e.bgColor;
+    if(xStr(sImage)) e.background.src=sImage;
+  }
+  return bg;
+}
+// Position:
+function xMoveTo(e,iX,iY) {
+  xLeft(e,iX);
+  xTop(e,iY);
+}
+function xLeft(e,iX) {
+  if(!(e=xGetElementById(e))) return 0;
+  var css=xDef(e.style);
+  if (css && xStr(e.style.left)) {
+    if(xNum(iX)) e.style.left=iX+'px';
+    else {
+      iX=parseInt(e.style.left);
+      if(isNaN(iX)) iX=0;
+    }
+  }
+  else if(css && xDef(e.style.pixelLeft)) {
+    if(xNum(iX)) e.style.pixelLeft=iX;
+    else iX=e.style.pixelLeft;
+  }
+  else if(xDef(e.left)) {
+    if(xNum(iX)) e.left=iX;
+    else iX=e.left;
+  }
+  return iX;
+}
+function xTop(e,iY) {
+  if(!(e=xGetElementById(e))) return 0;
+  var css=xDef(e.style);
+  if(css && xStr(e.style.top)) {
+    if(xNum(iY)) e.style.top=iY+'px';
+    else {
+      iY=parseInt(e.style.top);
+      if(isNaN(iY)) iY=0;
+    }
+  }
+  else if(css && xDef(e.style.pixelTop)) {
+    if(xNum(iY)) e.style.pixelTop=iY;
+    else iY=e.style.pixelTop;
+  }
+  else if(xDef(e.top)) {
+    if(xNum(iY)) e.top=iY;
+    else iY=e.top;
+  }
+  return iY;
+}
+function xPageX(e) {
+  if (!(e=xGetElementById(e))) return 0;
+  if (xDef(e.pageX)) return e.pageX;
+  var x = 0;
+  while (e) {
+    if (xDef(e.offsetLeft)) x += e.offsetLeft;
+    e = xDef(e.offsetParent) ? e.offsetParent : null;
+  }
+  return x;
+}
+function xPageY(e) {
+  if (!(e=xGetElementById(e))) return 0;
+  if (xDef(e.pageY)) return e.pageY;
+  var y = 0;
+  while (e) {
+    if (xDef(e.offsetTop)) y += e.offsetTop;
+    e = xDef(e.offsetParent) ? e.offsetParent : null;
+  }
+//  if (xOp7) return y - document.body.offsetTop; // v3.14, temporary hack for opera bug 130324
+  return y;
+}
+function xOffsetLeft(e) { // v3.14, see x_nn4.js
+  if (!(e=xGetElementById(e))) return 0;
+  if (xDef(e.offsetLeft)) return e.offsetLeft;
+  else return 0;
+}
+function xOffsetTop(e) {
+  if (!(e=xGetElementById(e))) return 0;
+  if (xDef(e.offsetTop)) return e.offsetTop;
+  else return 0;
+}
+function xScrollLeft(e) {
+  var offset=0;
+  if (!(e=xGetElementById(e))) {
+    if(xDef(window.pageXOffset)) offset=window.pageXOffset;
+    else if(document.documentElement && document.documentElement.scrollLeft) offset=document.documentElement.scrollLeft;
+    else if(document.body && xDef(document.body.scrollLeft)) offset=document.body.scrollLeft;
+  }
+  else { if (xNum(e.scrollLeft)) offset = e.scrollLeft; }
+  return offset;
+}
+function xScrollTop(e) {
+  var offset=0;
+  if (!(e=xGetElementById(e))) {
+    if(xDef(window.pageYOffset)) offset=window.pageYOffset;
+    else if(document.documentElement && document.documentElement.scrollTop) offset=document.documentElement.scrollTop;
+    else if(document.body && xDef(document.body.scrollTop)) offset=document.body.scrollTop;
+  }
+  else { if (xNum(e.scrollTop)) offset = e.scrollTop; }
+  return offset;
+}
+// Size:
+function xResizeTo(e,uW,uH) {
+  xWidth(e,uW);
+  xHeight(e,uH);
+}
+function xWidth(e,uW) {
+  if(!(e=xGetElementById(e))) return 0;
+  if (xNum(uW)) { // v3.13.1
+    if (uW<0) uW = 0;
+    else uW=Math.round(uW);
+  }
+  else uW=0;
+  var css=xDef(e.style);
+  if(css && xDef(e.offsetWidth) && xStr(e.style.width)) {
+    if(uW) xSetCW(e, uW);
+    uW=e.offsetWidth;
+  }
+  else if(css && xDef(e.style.pixelWidth)) {
+    if(uW) e.style.pixelWidth=uW;
+    uW=e.style.pixelWidth;
+  }
+  else if(xDef(e.clip) && xDef(e.clip.right)) {
+    if(uW) e.clip.right=uW;
+    uW=e.clip.right;
+  }
+  return uW;
+}
+function xHeight(e,uH) {
+  if(!(e=xGetElementById(e))) return 0;
+  if (xNum(uH)) { // v3.13.1
+    if (uH<0) uH = 0;
+    else uH=Math.round(uH);
+  }
+  else uH=0;
+  var css=xDef(e.style);
+  if(css && xDef(e.offsetHeight) && xStr(e.style.height)) {
+    if(uH) xSetCH(e, uH);
+    uH=e.offsetHeight;
+  }
+  else if(css && xDef(e.style.pixelHeight)) {
+    if(uH) e.style.pixelHeight=uH;
+    uH=e.style.pixelHeight;
+  }
+  else if(xDef(e.clip) && xDef(e.clip.bottom)) {
+    if(uH) e.clip.bottom=uH;
+    uH=e.clip.bottom;
+  }
+  return uH;
+}
+function xGetCS(ele,sP){return parseInt(document.defaultView.getComputedStyle(ele,'').getPropertyValue(sP));}
+function xSetCW(ele,uW){
+  var pl=0,pr=0,bl=0,br=0;
+  if(xDef(document.defaultView) && xDef(document.defaultView.getComputedStyle)){
+    pl=xGetCS(ele,'padding-left');
+    pr=xGetCS(ele,'padding-right');
+    bl=xGetCS(ele,'border-left-width');
+    br=xGetCS(ele,'border-right-width');
+  }
+  else if(xDef(ele.currentStyle,document.compatMode)){
+    if(document.compatMode=='CSS1Compat'){
+      pl=parseInt(ele.currentStyle.paddingLeft);
+      pr=parseInt(ele.currentStyle.paddingRight);
+      bl=parseInt(ele.currentStyle.borderLeftWidth);
+      br=parseInt(ele.currentStyle.borderRightWidth);
+    }
+  }
+  else if(xDef(ele.offsetWidth,ele.style.width)){ // ?
+    ele.style.width=uW+'px';
+    pl=ele.offsetWidth-uW;
+  }
+  if(isNaN(pl)) pl=0; if(isNaN(pr)) pr=0; if(isNaN(bl)) bl=0; if(isNaN(br)) br=0;
+  var cssW=uW-(pl+pr+bl+br);
+  if(isNaN(cssW)||cssW<0) return;
+  else ele.style.width=cssW+'px';
+}
+function xSetCH(ele,uH){
+  var pt=0,pb=0,bt=0,bb=0;
+  if(xDef(document.defaultView) && xDef(document.defaultView.getComputedStyle)){
+    pt=xGetCS(ele,'padding-top');
+    pb=xGetCS(ele,'padding-bottom');
+    bt=xGetCS(ele,'border-top-width');
+    bb=xGetCS(ele,'border-bottom-width');
+  }
+  else if(xDef(ele.currentStyle,document.compatMode)){
+    if(document.compatMode=='CSS1Compat'){
+      pt=parseInt(ele.currentStyle.paddingTop);
+      pb=parseInt(ele.currentStyle.paddingBottom);
+      bt=parseInt(ele.currentStyle.borderTopWidth);
+      bb=parseInt(ele.currentStyle.borderBottomWidth);
+    }
+  }
+  else if(xDef(ele.offsetHeight,ele.style.height)){ // ?
+    ele.style.height=uH+'px';
+    pt=ele.offsetHeight-uH;
+  }
+  if(isNaN(pt)) pt=0; if(isNaN(pb)) pb=0; if(isNaN(bt)) bt=0; if(isNaN(bb)) bb=0;
+  var cssH=uH-(pt+pb+bt+bb);
+  if(isNaN(cssH)||cssH<0) return;
+  else ele.style.height=cssH+'px';
+}
+function xClip(e,iTop,iRight,iBottom,iLeft) {
+  if(!(e=xGetElementById(e))) return;
+  if(e.style) {
+    if (xNum(iLeft)) e.style.clip='rect('+iTop+'px '+iRight+'px '+iBottom+'px '+iLeft+'px)';
+    else e.style.clip='rect(0 '+parseInt(e.style.width)+'px '+parseInt(e.style.height)+'px 0)';
+  }
+  else if(e.clip) {
+    if (xNum(iLeft)) { e.clip.top=iTop; e.clip.right=iRight; e.clip.bottom=iBottom; e.clip.left=iLeft; }
+    else { e.clip.top=0; e.clip.right=xWidth(e); e.clip.bottom=xHeight(e); e.clip.left=0; }
+  }
+}
+// Window:
+function xClientWidth() {
+  var w=0;
+  if(xOp5or6) w=window.innerWidth;
+  else if(!window.opera && document.documentElement && document.documentElement.clientWidth) // v3.12
+    w=document.documentElement.clientWidth;
+  else if(document.body && document.body.clientWidth)
+    w=document.body.clientWidth;
+  else if(xDef(window.innerWidth,window.innerHeight,document.height)) {
+    w=window.innerWidth;
+    if(document.height>window.innerHeight) w-=16;
+  }
+  return w;
+}
+function xClientHeight() {
+  var h=0;
+  if(xOp5or6) h=window.innerHeight;
+  else if(!window.opera && document.documentElement && document.documentElement.clientHeight) // v3.12
+    h=document.documentElement.clientHeight;
+  else if(document.body && document.body.clientHeight)
+    h=document.body.clientHeight;
+  else if(xDef(window.innerWidth,window.innerHeight,document.width)) {
+    h=window.innerHeight;
+    if(document.width>window.innerWidth) h-=16;
+  }
+  return h;
+}
+// Animation:
+function xSlideTo(e,x,y,uTime) {
+  if (!(e=xGetElementById(e))) return;
+  if (!e.timeout) e.timeout = 25;
+  e.xTarget = x; e.yTarget = y; e.slideTime = uTime; e.stop = false;
+  e.yA = e.yTarget - xTop(e); e.xA = e.xTarget - xLeft(e); // A = distance
+  e.B = Math.PI / (2 * e.slideTime); // B = period
+  e.yD = xTop(e); e.xD = xLeft(e); // D = initial position
+  var d = new Date(); e.C = d.getTime();
+  if (!e.moving) xSlide(e);
+}
+function xSlide(e) {
+  if (!(e=xGetElementById(e))) return;
+  var now, s, t, newY, newX;
+  now = new Date();
+  t = now.getTime() - e.C;
+  if (e.stop) { e.moving = false; }
+  else if (t < e.slideTime) {
+    setTimeout("xSlide('"+e.id+"')", e.timeout);
+    s = Math.sin(e.B * t);
+    newX = Math.round(e.xA * s + e.xD);
+    newY = Math.round(e.yA * s + e.yD);
+    xMoveTo(e, newX, newY);
+    e.moving = true;
+  }  
+  else {
+    xMoveTo(e, e.xTarget, e.yTarget);
+    e.moving = false;
+  }  
+}
+// Event:
+function xAddEventListener(e,eventType,eventListener,useCapture) {
+  if(!(e=xGetElementById(e))) return;
+  eventType=eventType.toLowerCase();
+  if((!xIE4Up && !xOp7) && e==window) {
+    if(eventType=='resize') { window.xPCW=xClientWidth(); window.xPCH=xClientHeight(); window.xREL=eventListener; xResizeEvent(); return; }
+    if(eventType=='scroll') { window.xPSL=xScrollLeft(); window.xPST=xScrollTop(); window.xSEL=eventListener; xScrollEvent(); return; }
+  }
+  var eh='e.on'+eventType+'=eventListener';
+  if(e.addEventListener) e.addEventListener(eventType,eventListener,useCapture);
+  else if(e.attachEvent) e.attachEvent('on'+eventType,eventListener);
+  else if(e.captureEvents) {
+    if(useCapture||(eventType.indexOf('mousemove')!=-1)) { e.captureEvents(eval('Event.'+eventType.toUpperCase())); }
+    eval(eh);
+  }
+  else eval(eh);
+}
+function xRemoveEventListener(e,eventType,eventListener,useCapture) {
+  if(!(e=xGetElementById(e))) return;
+  eventType=eventType.toLowerCase();
+  if((!xIE4Up && !xOp7) && e==window) {
+    if(eventType=='resize') { window.xREL=null; return; }
+    if(eventType=='scroll') { window.xSEL=null; return; }
+  }
+  var eh='e.on'+eventType+'=null';
+  if(e.removeEventListener) e.removeEventListener(eventType,eventListener,useCapture);
+  else if(e.detachEvent) e.detachEvent('on'+eventType,eventListener);
+  else if(e.releaseEvents) {
+    if(useCapture||(eventType.indexOf('mousemove')!=-1)) { e.releaseEvents(eval('Event.'+eventType.toUpperCase())); }
+    eval(eh);
+  }
+  else eval(eh);
+}
+function xEvent(evt) { // cross-browser event object prototype
+  this.type = '';
+  this.target = null;
+  this.pageX = 0;
+  this.pageY = 0;
+  this.offsetX = 0;
+  this.offsetY = 0;
+  this.keyCode = 0;
+  var e = evt ? evt : window.event;
+  if(!e) return;
+  if(e.type) this.type = e.type;
+  if(e.target) this.target = e.target;
+  else if(e.srcElement) this.target = e.srcElement;
+  else if(xNN4) this.target = xLayerFromPoint(e.pageX, e.pageY);
+  if(xOp5or6) { this.pageX = e.clientX; this.pageY = e.clientY; }
+  else if(xDef(e.pageX,e.pageY)) { this.pageX = e.pageX; this.pageY = e.pageY; } // v3.14
+  else if(xDef(e.clientX,e.clientY)) { this.pageX = e.clientX + xScrollLeft(); this.pageY = e.clientY + xScrollTop(); }
+  if(xDef(e.offsetX,e.offsetY)) { this.offsetX = e.offsetX; this.offsetY = e.offsetY; }
+  else if(xDef(e.layerX,e.layerY)) { this.offsetX = e.layerX; this.offsetY = e.layerY; }
+  else { this.offsetX = this.pageX - xPageX(this.target); this.offsetY = this.pageY - xPageY(this.target); }
+  if (e.keyCode) { this.keyCode = e.keyCode; } // for moz/fb, if keyCode==0 use which
+  else if (xDef(e.which)) { this.keyCode = e.which; }
+}
+function xResizeEvent() { // window resize event simulation
+  if (window.xREL) setTimeout('xResizeEvent()', 250);
+  var cw = xClientWidth(), ch = xClientHeight();
+  if (window.xPCW != cw || window.xPCH != ch) { window.xPCW = cw; window.xPCH = ch; if (window.xREL) window.xREL(); }
+}
+function xScrollEvent() { // window scroll event simulation
+  if (window.xSEL) setTimeout('xScrollEvent()', 250);
+  var sl = xScrollLeft(), st = xScrollTop();
+  if (window.xPSL != sl || window.xPST != st) { window.xPSL = sl; window.xPST = st; if (window.xSEL) window.xSEL(); }
+}
+// end x.js
diff --git a/lib/simpletestlib/ui/webunit_reporter.php b/lib/simpletestlib/ui/webunit_reporter.php
new file mode 100644 (file)
index 0000000..038d244
--- /dev/null
@@ -0,0 +1,286 @@
+<?php
+/**
+ *     base include file for SimpleTest PUnit reporter
+ *     @package        SimpleTest
+ *     @subpackage     UnitTester
+ *     @version        $Id$
+ */
+
+/**
+ * @ignore    originally defined in simple_test.php
+ */
+if (!defined("SIMPLE_TEST")) {
+       define("SIMPLE_TEST", "simpletest/");
+}
+require_once(SIMPLE_TEST . 'runner.php');
+require_once(SIMPLE_TEST . 'reporter.php');
+/**
+ * Main sprintf template for the start of the page.
+ * Sequence of parameters is:
+ * - title - string
+ * - script path - string
+ * - script path - string
+ * - css path - string
+ * - additional css - string
+ * - title - string
+ * - image path - string
+ */
+define('SIMPLETEST_WEBUNIT_HEAD', <<<EOS
+<html>
+<head>
+<title>%s</title>
+<script type="text/javascript" src="%sx.js"></script>
+<script type="text/javascript" src="%swebunit.js"></script>
+<link rel="stylesheet" type="text/css" href="%swebunit.css" title="Default"></link>
+<style type="text/css">
+%s
+</style>
+</head>
+<body>
+<div id="wait">
+       <h1>&nbsp;Running %s&nbsp;</h1>
+       Please wait...<br />
+       <img src="%swait.gif" border="0"><br />&nbsp;
+</div>
+<script type="text/javascript">
+wait_start();
+</script>
+<div id="webunit">
+       <div id="run"></div><br />
+       <div id="tabs">
+               <div id="visible_tab">visible tab content</div>
+               &nbsp;&nbsp;<span id="failtab" class="activetab">&nbsp;&nbsp;<a href="javascript:activate_tab('fail');">Fail</a>&nbsp;&nbsp;</span>
+               <span id="treetab" class="inactivetab">&nbsp;&nbsp;<a href="javascript:activate_tab('tree');">Tree</a>&nbsp;&nbsp;</span>
+       </div>
+       <div id="msg">Click on a failed test case method in the tree tab to view output here.</div>
+</div>
+<div id="fail"></div>
+<div id="tree"></div>
+<!-- open a new script to capture js vars as the tests run -->
+<script type="text/javascript">
+layout();
+
+EOS
+);
+
+/**
+ *     Not used yet.
+ *  May be needed for localized styles we need at runtime, not in the stylesheet.
+ */
+define('SIMPLETEST_WEBUNIT_CSS', '/* this space reseved for future use */');
+
+    /**
+     *    Sample minimal test displayer. Generates only
+     *    failure messages and a pass count.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class WebUnitReporter extends SimpleReporter {
+       /**
+        *    @var string Base directory for PUnit script, images and style sheets.
+        *    Needs to be a relative path from where the test scripts are run 
+        *    (and obviously, visible in the document root).
+        */
+       var $path;
+        
+        /**
+         *    Does nothing yet. The first output will
+         *    be sent on the first test start. For use
+         *    by a web browser.
+         *    @access public
+         */
+        function WebUnitReporter($path='../ui/') {
+            $this->SimpleReporter();
+            $this->path = $path;
+        }
+        
+        /**
+         *    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();
+            echo sprintf(
+               SIMPLETEST_WEBUNIT_HEAD
+               ,$test_name
+               ,$this->path.'js/'
+               ,$this->path.'js/'
+               ,$this->path.'css/'
+               ,$this->_getCss()
+               ,$test_name
+               ,$this->path.'img/'
+               );
+            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() {
+            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 SIMPLETEST_WEBUNIT_CSS;
+        }
+        
+        /**
+         *    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) {
+            echo 'make_tree();</script>'.$this->outputScript("xHide('wait');");
+            $colour = ($this->getFailCount() + $this->getExceptionCount() > 0 ? "red" : "green");
+            $content = "<h1>$test_name</h1>\n";
+            $content .= "<div style=\"";
+            $content .= "padding: 8px; margin-top: 1em; background-color: $colour; color: white;";
+            $content .= "\">";
+            $content .= $this->getTestCaseProgress() . "/" . $this->getTestCaseCount();
+            $content .= " test cases complete:\n";
+            $content .= "<strong>" . $this->getPassCount() . "</strong> passes, ";
+            $content .= "<strong>" . $this->getFailCount() . "</strong> fails and ";
+            $content .= "<strong>" . $this->getExceptionCount() . "</strong> exceptions.";
+            $content .= "</div>\n";
+
+                       echo $this->outputScript('foo = "'.$this->toJsString($content).'";'."\nset_div_content('run', foo);");
+            echo "\n</body>\n</html>\n";
+        }
+        
+        
+        /**
+         *    Paints formatted text such as dumped variables.
+         *    @param string $message        Text to show.
+         *    @access public
+         */
+        function paintFormattedMessage($message) {
+           echo "add_log(\"".$this->toJsString("<pre>$message</pre>", true)."\");\n";
+        }
+        
+        /**
+         *    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) {
+             Parent::paintGroupStart($test_name, $size);
+             echo "add_group('$test_name');\n";
+        }
+         /**
+          *    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) {
+             Parent::paintCaseStart($test_name);
+             echo "add_case('$test_name');\n";
+         }
+
+
+         /**
+          *    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);
+             echo "add_method('$test_name');\n";
+         }
+
+         /**
+          *    Paints the end of a test method.
+          *    @param string $test_name   Name of test that is ending.
+          *    @access public
+          */
+         function paintMethodEnd($test_name) {
+             Parent::paintMethodEnd($test_name);
+         }
+
+         /**
+          *    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);
+             $msg = "<span class=\"fail\">Fail</span>: ";
+             $breadcrumb = $this->getTestList();
+             array_shift($breadcrumb);
+             $msg .= implode("-&gt;", $breadcrumb);
+             $msg .= "-&gt;" . htmlentities($message) . "<br />";
+             echo "add_fail('$msg');\n";
+         }
+
+        /**
+         *    Paints a PHP error or exception.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         *    @abstract
+         */
+        function paintException($message) {
+            parent::paintException($message);
+            $msg = "<span class=\"fail\">Exception</span>: ";
+            $breadcrumb = $this->getTestList();
+            array_shift($breadcrumb);
+            $msg .= implode("-&gt;", $breadcrumb);
+            $msg .= "-&gt;<strong>" . htmlentities($message) . "</strong><br />";
+            echo "add_fail('$msg');\n";
+        }
+        /**
+                * Returns the script passed in wrapped in script tags.
+                *
+                * @param       string  $script         the script to output
+                * @return      string  the script wrapped with script tags
+                */
+               function outputScript($script)
+               {
+                       return "<script type=\"text/javascript\">\n".$script."\n</script>\n";
+               }
+               
+        
+        /**
+                *      Transform a string into a format acceptable to JavaScript
+                *  @param string $str  the string to transform
+                *      @return string
+                */
+               function toJsString($str, $preserveCr=false) {
+                       $cr = ($preserveCr) ? '\\n' : '';
+                       return str_replace(
+                               array('"'
+                                       ,"\n")
+                               ,array('\"'
+                                       ,"$cr\"\n\t+\"")
+                               ,$str
+                               );
+               }
+    }
+    
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/unit_tester.php b/lib/simpletestlib/unit_tester.php
new file mode 100644 (file)
index 0000000..bbfe02b
--- /dev/null
@@ -0,0 +1,373 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/test_case.php');
+    require_once(dirname(__FILE__) . '/dumper.php');
+    /**#@-*/
+
+    /**
+     *    Standard unit test class for day to day testing
+     *    of PHP code XP style. Adds some useful standard
+     *    assertions.
+        *        @package      SimpleTest
+        *        @subpackage   UnitTester
+     */
+    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);
+        }
+
+        /**
+         *    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") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($value) . "] should be null");
+            return $this->assertTrue(! isset($value), $message);
+        }
+
+        /**
+         *    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") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($value) . "] should not be null");
+            return $this->assertTrue(isset($value), $message);
+        }
+
+        /**
+         *    Type and class test. Will pass if class
+         *    matches the type name or is a subclass or
+         *    if not an object, but the type is correct.
+         *    @param mixed $object         Object to test.
+         *    @param string $type          Type name as string.
+         *    @param string $message       Message to display.
+         *    @return boolean              True on pass.
+         *    @access public
+         */
+        function assertIsA($object, $type, $message = "%s") {
+            return $this->assert(
+                    new IsAExpectation($type),
+                    $object,
+                    $message);
+        }
+
+        /**
+         *    Type and class mismatch test. Will pass if class
+         *    name or underling type does not match the one
+         *    specified.
+         *    @param mixed $object         Object to test.
+         *    @param string $type          Type name as string.
+         *    @param string $message       Message to display.
+         *    @return boolean              True on pass.
+         *    @access public
+         */
+        function assertNotA($object, $type, $message = "%s") {
+            return $this->assert(
+                    new NotAExpectation($type),
+                    $object,
+                    $message);
+        }
+
+        /**
+         *    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") {
+            return $this->assert(
+                    new EqualExpectation($first),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    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") {
+            return $this->assert(
+                    new NotEqualExpectation($first),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    Will trigger a pass if the if the first parameter
+         *    is near enough to the second by the margin.
+         *    @param mixed $first          Value to compare.
+         *    @param mixed $second         Value to compare.
+         *    @param mixed $margin         Fuzziness of match.
+         *    @param string $message       Message to display.
+         *    @return boolean              True on pass
+         *    @access public
+         */
+        function assertWithinMargin($first, $second, $margin, $message = "%s") {
+            return $this->assert(
+                    new WithinMarginExpectation($first, $margin),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    Will trigger a pass if the two parameters differ
+         *    by more than the margin.
+         *    @param mixed $first          Value to compare.
+         *    @param mixed $second         Value to compare.
+         *    @param mixed $margin         Fuzziness of match.
+         *    @param string $message       Message to display.
+         *    @return boolean              True on pass
+         *    @access public
+         */
+        function assertOutsideMargin($first, $second, $margin, $message = "%s") {
+            return $this->assert(
+                    new OutsideMarginExpectation($first, $margin),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    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") {
+            return $this->assert(
+                    new IdenticalExpectation($first),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    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") {
+            return $this->assert(
+                    new NotIdenticalExpectation($first),
+                    $second,
+                    $message);
+        }
+
+        /**
+         *    Will trigger a pass if both parameters refer
+         *    to the same object. Fail otherwise.
+         *    @param mixed $first           Object reference to check.
+         *    @param mixed $second          Hopefully the same object.
+         *    @param string $message        Message to display.
+         *    @return boolean               True on pass
+         *    @access public
+         */
+        function assertReference(&$first, &$second, $message = "%s") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($first) .
+                            "] and [" . $dumper->describeValue($second) .
+                            "] should reference the same object");
+            return $this->assertTrue(
+                    SimpleTestCompatibility::isReference($first, $second),
+                    $message);
+        }
+
+        /**
+         *    Will trigger a pass if both parameters refer
+         *    to different objects. Fail otherwise. The objects
+         *    have to be identical though.
+         *    @param mixed $first           Object reference to check.
+         *    @param mixed $second          Hopefully not the same object.
+         *    @param string $message        Message to display.
+         *    @return boolean               True on pass
+         *    @access public
+         */
+        function assertClone(&$first, &$second, $message = "%s") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($first) .
+                            "] and [" . $dumper->describeValue($second) .
+                            "] should not be the same object");
+            $identical = &new IdenticalExpectation($first);
+            return $this->assertTrue(
+                    $identical->test($second) &&
+                            ! SimpleTestCompatibility::isReference($first, $second),
+                    $message);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function assertCopy(&$first, &$second, $message = "%s") {
+            $dumper = &new SimpleDumper();
+            $message = sprintf(
+                    $message,
+                    "[" . $dumper->describeValue($first) .
+                            "] and [" . $dumper->describeValue($second) .
+                            "] should not be the same object");
+            return $this->assertFalse(
+                    SimpleTestCompatibility::isReference($first, $second),
+                    $message);
+        }
+
+        /**
+         *    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") {
+            return $this->assert(
+                    new PatternExpectation($pattern),
+                    $subject,
+                    $message);
+        }
+
+        /**
+         *       @deprecated
+         */
+        function assertWantedPattern($pattern, $subject, $message = "%s") {
+               return $this->assertPattern($pattern, $subject, $message);
+        }
+
+        /**
+         *    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") {
+            return $this->assert(
+                    new NoPatternExpectation($pattern),
+                    $subject,
+                    $message);
+        }
+
+        /**
+         *       @deprecated
+         */
+        function assertNoUnwantedPattern($pattern, $subject, $message = "%s") {
+               return $this->assertNoPattern($pattern, $subject, $message);
+        }
+
+        /**
+         *    Confirms that no errors have occoured so
+         *    far in the test method.
+         *    @param string $message    Message to display.
+         *    @return boolean           True on pass
+         *    @access public
+         */
+        function assertNoErrors($message = "%s") {
+            $queue = &SimpleErrorQueue::instance();
+            return $this->assertTrue(
+                    $queue->isEmpty(),
+                    sprintf($message, "Should be no errors"));
+        }
+
+        /**
+         *    Confirms that an error has occoured and
+         *    optionally that the error text matches exactly.
+         *    @param string $expected   Expected error text or
+         *                              false for no check.
+         *    @param string $message    Message to display.
+         *    @return boolean           True on pass
+         *    @access public
+         */
+        function assertError($expected = false, $message = "%s") {
+            $queue = &SimpleErrorQueue::instance();
+            if ($queue->isEmpty()) {
+                $this->fail(sprintf($message, "Expected error not found"));
+                return;
+            }
+            list($severity, $content, $file, $line, $globals) = $queue->extract();
+            $severity = SimpleErrorQueue::getSeverityAsString($severity);
+            if (! $expected) {
+                return $this->pass(
+                        "Captured a PHP error of [$content] severity [$severity] in [$file] line [$line] -> %s");
+            }
+            $expected = $this->_coerceToExpectation($expected);
+            return $this->assert(
+                    $expected,
+                    $content,
+                    "Expected PHP error [$content] severity [$severity] in [$file] line [$line] -> %s");
+        }
+
+        /**
+         *    Creates an equality expectation if the
+         *    object/value is not already some type
+         *    of expectation.
+         *    @param mixed $expected      Expected value.
+         *    @return SimpleExpectation   Expectation object.
+         *    @access private
+         */
+        function _coerceToExpectation($expected) {
+            if (SimpleTestCompatibility::isA($expected, 'SimpleExpectation')) {
+                return $expected;
+            }
+            return new EqualExpectation($expected);
+        }
+
+        /**
+         *    @deprecated
+         */
+        function assertErrorPattern($pattern, $message = "%s") {
+            return $this->assertError(new PatternExpectation($pattern), $message);
+        }
+    }
+?>
diff --git a/lib/simpletestlib/url.php b/lib/simpletestlib/url.php
new file mode 100644 (file)
index 0000000..9a68156
--- /dev/null
@@ -0,0 +1,525 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/encoding.php');
+    /**#@-*/
+
+    /**
+     *    URL parser to replace parse_url() PHP function which
+     *    got broken in PHP 4.3.0. Adds some browser specific
+     *    functionality such as expandomatics.
+     *    Guesses a bit trying to separate the host from
+     *    the path and tries to keep a raw, possibly unparsable,
+     *    request string as long as possible.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleUrl {
+        var $_scheme;
+        var $_username;
+        var $_password;
+        var $_host;
+        var $_port;
+        var $_path;
+        var $_request;
+        var $_fragment;
+        var $_x;
+        var $_y;
+        var $_target;
+        var $_raw = false;
+        
+        /**
+         *    Constructor. Parses URL into sections.
+         *    @param string $url        Incoming URL.
+         *    @access public
+         */
+        function SimpleUrl($url) {
+            list($x, $y) = $this->_chompCoordinates($url);
+            $this->setCoordinates($x, $y);
+            $this->_scheme = $this->_chompScheme($url);
+            list($this->_username, $this->_password) = $this->_chompLogin($url);
+            $this->_host = $this->_chompHost($url);
+            $this->_port = false;
+            if (preg_match('/(.*?):(.*)/', $this->_host, $host_parts)) {
+                $this->_host = $host_parts[1];
+                $this->_port = (integer)$host_parts[2];
+            }
+            $this->_path = $this->_chompPath($url);
+            $this->_request = $this->_parseRequest($this->_chompRequest($url));
+            $this->_fragment = (strncmp($url, "#", 1) == 0 ? substr($url, 1) : false);
+            $this->_target = false;
+        }
+        
+        /**
+         *    Extracts the X, Y coordinate pair from an image map.
+         *    @param string $url   URL so far. The coordinates will be
+         *                         removed.
+         *    @return array        X, Y as a pair of integers.
+         *    @access private
+         */
+        function _chompCoordinates(&$url) {
+            if (preg_match('/(.*)\?(\d+),(\d+)$/', $url, $matches)) {
+                $url = $matches[1];
+                return array((integer)$matches[2], (integer)$matches[3]);
+            }
+            return array(false, false);
+        }
+        
+        /**
+         *    Extracts the scheme part of an incoming URL.
+         *    @param string $url   URL so far. The scheme will be
+         *                         removed.
+         *    @return string       Scheme part or false.
+         *    @access private
+         */
+        function _chompScheme(&$url) {
+            if (preg_match('/(.*?):(\/\/)(.*)/', $url, $matches)) {
+                $url = $matches[2] . $matches[3];
+                return $matches[1];
+            }
+            return false;
+        }
+        
+        /**
+         *    Extracts the username and password from the
+         *    incoming URL. The // prefix will be reattached
+         *    to the URL after the doublet is extracted.
+         *    @param string $url    URL so far. The username and
+         *                          password are removed.
+         *    @return array         Two item list of username and
+         *                          password. Will urldecode() them.
+         *    @access private
+         */
+        function _chompLogin(&$url) {
+            $prefix = '';
+            if (preg_match('/^(\/\/)(.*)/', $url, $matches)) {
+                $prefix = $matches[1];
+                $url = $matches[2];
+            }
+            if (preg_match('/(.*?)@(.*)/', $url, $matches)) {
+                $url = $prefix . $matches[2];
+                $parts = split(":", $matches[1]);
+                return array(
+                        urldecode($parts[0]),
+                        isset($parts[1]) ? urldecode($parts[1]) : false);
+            }
+            $url = $prefix . $url;
+            return array(false, false);
+        }
+        
+        /**
+         *    Extracts the host part of an incoming URL.
+         *    Includes the port number part. Will extract
+         *    the host if it starts with // or it has
+         *    a top level domain or it has at least two
+         *    dots.
+         *    @param string $url    URL so far. The host will be
+         *                          removed.
+         *    @return string        Host part guess or false.
+         *    @access private
+         */
+        function _chompHost(&$url) {
+            if (preg_match('/^(\/\/)(.*?)(\/.*|\?.*|#.*|$)/', $url, $matches)) {
+                $url = $matches[3];
+                return $matches[2];
+            }
+            if (preg_match('/(.*?)(\.\.\/|\.\/|\/|\?|#|$)(.*)/', $url, $matches)) {
+                $tlds = SimpleUrl::getAllTopLevelDomains();
+                if (preg_match('/[a-z0-9\-]+\.(' . $tlds . ')/i', $matches[1])) {
+                    $url = $matches[2] . $matches[3];
+                    return $matches[1];
+                } elseif (preg_match('/[a-z0-9\-]+\.[a-z0-9\-]+\.[a-z0-9\-]+/i', $matches[1])) {
+                    $url = $matches[2] . $matches[3];
+                    return $matches[1];
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Extracts the path information from the incoming
+         *    URL. Strips this path from the URL.
+         *    @param string $url     URL so far. The host will be
+         *                           removed.
+         *    @return string         Path part or '/'.
+         *    @access private
+         */
+        function _chompPath(&$url) {
+            if (preg_match('/(.*?)(\?|#|$)(.*)/', $url, $matches)) {
+                $url = $matches[2] . $matches[3];
+                return ($matches[1] ? $matches[1] : '');
+            }
+            return '';
+        }
+        
+        /**
+         *    Strips off the request data.
+         *    @param string $url  URL so far. The request will be
+         *                        removed.
+         *    @return string      Raw request part.
+         *    @access private
+         */
+        function _chompRequest(&$url) {
+            if (preg_match('/\?(.*?)(#|$)(.*)/', $url, $matches)) {
+                $url = $matches[2] . $matches[3];
+                return $matches[1];
+            }
+            return '';
+        }
+         
+        /**
+         *    Breaks the request down into an object.
+         *    @param string $raw           Raw request.
+         *    @return SimpleFormEncoding    Parsed data.
+         *    @access private
+         */
+        function _parseRequest($raw) {
+            $this->_raw = $raw;
+            $request = new SimpleGetEncoding();
+            foreach (split("&", $raw) as $pair) {
+                if (preg_match('/(.*?)=(.*)/', $pair, $matches)) {
+                    $request->add($matches[1], urldecode($matches[2]));
+                } elseif ($pair) {
+                    $request->add($pair, '');
+                }
+            }
+            return $request;
+        }
+        
+        /**
+         *    Accessor for protocol part.
+         *    @param string $default    Value to use if not present.
+         *    @return string            Scheme name, e.g "http".
+         *    @access public
+         */
+        function getScheme($default = false) {
+            return $this->_scheme ? $this->_scheme : $default;
+        }
+        
+        /**
+         *    Accessor for user name.
+         *    @return string    Username preceding host.
+         *    @access public
+         */
+        function getUsername() {
+            return $this->_username;
+        }
+        
+        /**
+         *    Accessor for password.
+         *    @return string    Password preceding host.
+         *    @access public
+         */
+        function getPassword() {
+            return $this->_password;
+        }
+        
+        /**
+         *    Accessor for hostname and port.
+         *    @param string $default    Value to use if not present.
+         *    @return string            Hostname only.
+         *    @access public
+         */
+        function getHost($default = false) {
+            return $this->_host ? $this->_host : $default;
+        }
+        
+        /**
+         *    Accessor for top level domain.
+         *    @return string       Last part of host.
+         *    @access public
+         */
+        function getTld() {
+            $path_parts = pathinfo($this->getHost());
+            return (isset($path_parts['extension']) ? $path_parts['extension'] : false);
+        }
+        
+        /**
+         *    Accessor for port number.
+         *    @return integer    TCP/IP port number.
+         *    @access public
+         */
+        function getPort() {
+            return $this->_port;
+        }        
+                
+       /**
+         *    Accessor for path.
+         *    @return string    Full path including leading slash if implied.
+         *    @access public
+         */
+        function getPath() {
+            if (! $this->_path && $this->_host) {
+                return '/';
+            }
+            return $this->_path;
+        }
+        
+        /**
+         *    Accessor for page if any. This may be a
+         *    directory name if ambiguious.
+         *    @return            Page name.
+         *    @access public
+         */
+        function getPage() {
+            if (! preg_match('/([^\/]*?)$/', $this->getPath(), $matches)) {
+                return false;
+            }
+            return $matches[1];
+        }
+        
+        /**
+         *    Gets the path to the page.
+         *    @return string       Path less the page.
+         *    @access public
+         */
+        function getBasePath() {
+            if (! preg_match('/(.*\/)[^\/]*?$/', $this->getPath(), $matches)) {
+                return false;
+            }
+            return $matches[1];
+        }
+        
+        /**
+         *    Accessor for fragment at end of URL after the "#".
+         *    @return string    Part after "#".
+         *    @access public
+         */
+        function getFragment() {
+            return $this->_fragment;
+        }
+        
+        /**
+         *    Sets image coordinates. Set to false to clear
+         *    them.
+         *    @param integer $x    Horizontal position.
+         *    @param integer $y    Vertical position.
+         *    @access public
+         */
+        function setCoordinates($x = false, $y = false) {
+            if (($x === false) || ($y === false)) {
+                $this->_x = $this->_y = false;
+                return;
+            }
+            $this->_x = (integer)$x;
+            $this->_y = (integer)$y;
+        }
+        
+        /**
+         *    Accessor for horizontal image coordinate.
+         *    @return integer        X value.
+         *    @access public
+         */
+        function getX() {
+            return $this->_x;
+        }
+         
+        /**
+         *    Accessor for vertical image coordinate.
+         *    @return integer        Y value.
+         *    @access public
+         */
+        function getY() {
+            return $this->_y;
+        }
+       
+        /**
+         *    Accessor for current request parameters
+         *    in URL string form. Will return teh original request
+         *    if at all possible even if it doesn't make much
+         *    sense.
+         *    @return string   Form is string "?a=1&b=2", etc.
+         *    @access public
+         */
+        function getEncodedRequest() {
+            if ($this->_raw) {
+                $encoded = $this->_raw;
+            } else {
+                $encoded = $this->_request->asUrlRequest();
+            }
+            if ($encoded) {
+                return '?' . preg_replace('/^\?/', '', $encoded);
+            }
+            return '';
+        }
+        
+        /**
+         *    Adds an additional parameter to the request.
+         *    @param string $key            Name of parameter.
+         *    @param string $value          Value as string.
+         *    @access public
+         */
+        function addRequestParameter($key, $value) {
+            $this->_raw = false;
+            $this->_request->add($key, $value);
+        }
+        
+        /**
+         *    Adds additional parameters to the request.
+         *    @param hash/SimpleFormEncoding $parameters   Additional
+         *                                                parameters.
+         *    @access public
+         */
+        function addRequestParameters($parameters) {
+            $this->_raw = false;
+            $this->_request->merge($parameters);
+        }
+        
+        /**
+         *    Clears down all parameters.
+         *    @access public
+         */
+        function clearRequest() {
+            $this->_raw = false;
+            $this->_request = &new SimpleGetEncoding();
+        }
+        
+        /**
+         *    Gets the frame target if present. Although
+         *    not strictly part of the URL specification it
+         *    acts as similarily to the browser.
+         *    @return boolean/string    Frame name or false if none.
+         *    @access public
+         */
+        function getTarget() {
+            return $this->_target;
+        }
+        
+        /**
+         *    Attaches a frame target.
+         *    @param string $frame        Name of frame.
+         *    @access public
+         */
+        function setTarget($frame) {
+            $this->_raw = false;
+            $this->_target = $frame;
+        }
+        
+        /**
+         *    Renders the URL back into a string.
+         *    @return string        URL in canonical form.
+         *    @access public
+         */
+        function asString() {
+            $scheme = $identity = $host = $path = $encoded = $fragment = '';
+            if ($this->_username && $this->_password) {
+                $identity = $this->_username . ':' . $this->_password . '@';
+            }
+            if ($this->getHost()) {
+                $scheme = $this->getScheme() ? $this->getScheme() : 'http';
+                $host = $this->getHost();
+            }
+            if (substr($this->_path, 0, 1) == '/') {
+                $path = $this->normalisePath($this->_path);
+            }
+            $encoded = $this->getEncodedRequest();
+            $fragment = $this->getFragment() ? '#'. $this->getFragment() : '';
+            $coords = $this->getX() === false ? '' : '?' . $this->getX() . ',' . $this->getY();
+            return "$scheme://$identity$host$path$encoded$fragment$coords";
+        }
+        
+        /**
+         *    Replaces unknown sections to turn a relative
+         *    URL into an absolute one. The base URL can
+         *    be either a string or a SimpleUrl object.
+         *    @param string/SimpleUrl $base       Base URL.
+         *    @access public
+         */
+        function makeAbsolute($base) {
+            if (! is_object($base)) {
+                $base = new SimpleUrl($base);
+            }
+            $scheme = $this->getScheme() ? $this->getScheme() : $base->getScheme();
+            if ($this->getHost()) {
+                $host = $this->getHost();
+                $port = $this->getPort() ? ':' . $this->getPort() : '';
+                $identity = $this->getIdentity() ? $this->getIdentity() . '@' : '';
+                if (! $identity) {
+                    $identity = $base->getIdentity() ? $base->getIdentity() . '@' : '';
+                }
+            } else {
+                $host = $base->getHost();
+                $port = $base->getPort() ? ':' . $base->getPort() : '';
+                $identity = $base->getIdentity() ? $base->getIdentity() . '@' : '';
+            }
+            $path = $this->normalisePath($this->_extractAbsolutePath($base));
+            $encoded = $this->getEncodedRequest();
+            $fragment = $this->getFragment() ? '#'. $this->getFragment() : '';
+            $coords = $this->getX() === false ? '' : '?' . $this->getX() . ',' . $this->getY();
+            return new SimpleUrl("$scheme://$identity$host$port$path$encoded$fragment$coords");
+        }
+        
+        /**
+         *    Replaces unknown sections of the path with base parts
+         *    to return a complete absolute one.
+         *    @param string/SimpleUrl $base       Base URL.
+         *    @param string                       Absolute path.
+         *    @access private
+         */
+        function _extractAbsolutePath($base) {
+            if ($this->getHost()) {
+                return $this->_path;
+            }
+            if (! $this->_isRelativePath($this->_path)) {
+                return $this->_path;
+            }
+            if ($this->_path) {
+                return $base->getBasePath() . $this->_path;
+            }
+            return $base->getPath();
+        }
+        
+        /**
+         *    Simple test to see if a path part is relative.
+         *    @param string $path        Path to test.
+         *    @return boolean            True if starts with a "/".
+         *    @access private
+         */
+        function _isRelativePath($path) {
+            return (substr($path, 0, 1) != '/');
+        }
+        
+        /**
+         *    Extracts the username and password for use in rendering
+         *    a URL.
+         *    @return string/boolean    Form of username:password or false.
+         *    @access public
+         */
+        function getIdentity() {
+            if ($this->_username && $this->_password) {
+                return $this->_username . ':' . $this->_password;
+            }
+            return false;
+        }
+        
+        /**
+         *    Replaces . and .. sections of the path.
+         *    @param string $path    Unoptimised path.
+         *    @return string         Path with dots removed if possible.
+         *    @access public
+         */
+        function normalisePath($path) {
+            $path = preg_replace('|/[^/]+/\.\./|', '/', $path);
+            return preg_replace('|/\./|', '/', $path);
+        }
+        
+        /**
+         *    A pipe seperated list of all TLDs that result in two part
+         *    domain names.
+         *    @return string        Pipe separated list.
+         *    @access public
+         *    @static
+         */
+        function getAllTopLevelDomains() {
+            return 'com|edu|net|org|gov|mil|int|biz|info|name|pro|aero|coop|museum';
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/user_agent.php b/lib/simpletestlib/user_agent.php
new file mode 100644 (file)
index 0000000..a5a6fd2
--- /dev/null
@@ -0,0 +1,333 @@
+<?php
+    /**
+     * Base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/cookies.php');
+    require_once(dirname(__FILE__) . '/http.php');
+    require_once(dirname(__FILE__) . '/encoding.php');
+    require_once(dirname(__FILE__) . '/authentication.php');
+    /**#@-*/
+   
+    if (! defined('DEFAULT_MAX_REDIRECTS')) {
+        define('DEFAULT_MAX_REDIRECTS', 3);
+    }
+    
+    if (! defined('DEFAULT_CONNECTION_TIMEOUT')) {
+        define('DEFAULT_CONNECTION_TIMEOUT', 15);
+    }
+
+    /**
+     *    Fetches web pages whilst keeping track of
+     *    cookies and authentication.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class SimpleUserAgent {
+        var $_cookie_jar;
+        var $_cookies_enabled = true;
+        var $_authenticator;
+        var $_max_redirects = DEFAULT_MAX_REDIRECTS;
+        var $_proxy = false;
+        var $_proxy_username = false;
+        var $_proxy_password = false;
+        var $_connection_timeout = DEFAULT_CONNECTION_TIMEOUT;
+        var $_additional_headers = array();
+        
+        /**
+         *    Starts with no cookies, realms or proxies.
+         *    @access public
+         */
+        function SimpleUserAgent() {
+            $this->_cookie_jar = &new SimpleCookieJar();
+            $this->_authenticator = &new SimpleAuthenticator();
+        }
+        
+        /**
+         *    Removes expired and temporary cookies as if
+         *    the browser was closed and re-opened. Authorisation
+         *    has to be obtained again as well.
+         *    @param string/integer $date   Time when session restarted.
+         *                                  If omitted then all persistent
+         *                                  cookies are kept.
+         *    @access public
+         */
+        function restart($date = false) {
+            $this->_cookie_jar->restartSession($date);
+            $this->_authenticator->restartSession();
+        }
+        
+        /**
+         *    Adds a header to every fetch.
+         *    @param string $header       Header line to add to every
+         *                                request until cleared.
+         *    @access public
+         */
+        function addHeader($header) {
+            $this->_additional_headers[] = $header;
+        }
+        
+        /**
+         *    Ages the cookies by the specified time.
+         *    @param integer $interval    Amount in seconds.
+         *    @access public
+         */
+        function ageCookies($interval) {
+            $this->_cookie_jar->agePrematurely($interval);
+        }
+        
+        /**
+         *    Sets an additional cookie. If a cookie has
+         *    the same name and path it is replaced.
+         *    @param string $name            Cookie key.
+         *    @param string $value           Value of cookie.
+         *    @param string $host            Host upon which the cookie is valid.
+         *    @param string $path            Cookie path if not host wide.
+         *    @param string $expiry          Expiry date.
+         *    @access public
+         */
+        function setCookie($name, $value, $host = false, $path = '/', $expiry = false) {
+            $this->_cookie_jar->setCookie($name, $value, $host, $path, $expiry);
+        }
+        
+        /**
+         *    Reads the most specific cookie value from the
+         *    browser cookies.
+         *    @param string $host        Host to search.
+         *    @param string $path        Applicable path.
+         *    @param string $name        Name of cookie to read.
+         *    @return string             False if not present, else the
+         *                               value as a string.
+         *    @access public
+         */
+        function getCookieValue($host, $path, $name) {
+            return $this->_cookie_jar->getCookieValue($host, $path, $name);
+        }
+        
+        /**
+         *    Reads the current cookies within the base URL.
+         *    @param string $name     Key of cookie to find.
+         *    @param SimpleUrl $base  Base URL to search from.
+         *    @return string/boolean  Null if there is no base URL, false
+         *                            if the cookie is not set.
+         *    @access public
+         */
+        function getBaseCookieValue($name, $base) {
+            if (! $base) {
+                return null;
+            }
+            return $this->getCookieValue($base->getHost(), $base->getPath(), $name);
+        }
+        
+        /**
+         *    Switches off cookie sending and recieving.
+         *    @access public
+         */
+        function ignoreCookies() {
+            $this->_cookies_enabled = false;
+        }
+        
+        /**
+         *    Switches back on the cookie sending and recieving.
+         *    @access public
+         */
+        function useCookies() {
+            $this->_cookies_enabled = true;
+        }
+        
+        /**
+         *    Sets the socket timeout for opening a connection.
+         *    @param integer $timeout      Maximum time in seconds.
+         *    @access public
+         */
+        function setConnectionTimeout($timeout) {
+            $this->_connection_timeout = $timeout;
+        }
+        
+        /**
+         *    Sets the maximum number of redirects before
+         *    a page will be loaded anyway.
+         *    @param integer $max        Most hops allowed.
+         *    @access public
+         */
+        function setMaximumRedirects($max) {
+            $this->_max_redirects = $max;
+        }
+        
+        /**
+         *    Sets proxy to use on all requests for when
+         *    testing from behind a firewall. Set URL
+         *    to false to disable.
+         *    @param string $proxy        Proxy URL.
+         *    @param string $username     Proxy username for authentication.
+         *    @param string $password     Proxy password for authentication.
+         *    @access public
+         */
+        function useProxy($proxy, $username, $password) {
+            if (! $proxy) {
+                $this->_proxy = false;
+                return;
+            }
+            if ((strncmp($proxy, 'http://', 7) != 0) && (strncmp($proxy, 'https://', 8) != 0)) {
+                $proxy = 'http://'. $proxy;
+            }
+            $this->_proxy = &new SimpleUrl($proxy);
+            $this->_proxy_username = $username;
+            $this->_proxy_password = $password;
+        }
+        
+        /**
+         *    Test to see if the redirect limit is passed.
+         *    @param integer $redirects        Count so far.
+         *    @return boolean                  True if over.
+         *    @access private
+         */
+        function _isTooManyRedirects($redirects) {
+            return ($redirects > $this->_max_redirects);
+        }
+        
+        /**
+         *    Sets the identity for the current realm.
+         *    @param string $host        Host to which realm applies.
+         *    @param string $realm       Full name of realm.
+         *    @param string $username    Username for realm.
+         *    @param string $password    Password for realm.
+         *    @access public
+         */
+        function setIdentity($host, $realm, $username, $password) {
+            $this->_authenticator->setIdentityForRealm($host, $realm, $username, $password);
+        }
+        
+        /**
+         *    Fetches a URL as a response object. Will keep trying if redirected.
+         *    It will also collect authentication realm information.
+         *    @param string/SimpleUrl $url      Target to fetch.
+         *    @param SimpleEncoding $encoding   Additional parameters for request.
+         *    @return SimpleHttpResponse        Hopefully the target page.
+         *    @access public
+         */
+        function &fetchResponse($url, $encoding) {
+            if ($encoding->getMethod() != 'POST') {
+                $url->addRequestParameters($encoding);
+                $encoding->clear();
+            }
+            $response = &$this->_fetchWhileRedirected($url, $encoding);
+            if ($headers = $response->getHeaders()) {
+                if ($headers->isChallenge()) {
+                    $this->_authenticator->addRealm(
+                            $url,
+                            $headers->getAuthentication(),
+                            $headers->getRealm());
+                }
+            }
+            return $response;
+        }
+        
+        /**
+         *    Fetches the page until no longer redirected or
+         *    until the redirect limit runs out.
+         *    @param SimpleUrl $url                  Target to fetch.
+         *    @param SimpelFormEncoding $encoding    Additional parameters for request.
+         *    @return SimpleHttpResponse             Hopefully the target page.
+         *    @access private
+         */
+        function &_fetchWhileRedirected($url, $encoding) {
+            $redirects = 0;
+            do {
+                $response = &$this->_fetch($url, $encoding);
+                if ($response->isError()) {
+                    return $response;
+                }
+                $headers = $response->getHeaders();
+                $location = new SimpleUrl($headers->getLocation());
+                $url = $location->makeAbsolute($url);
+                if ($this->_cookies_enabled) {
+                    $headers->writeCookiesToJar($this->_cookie_jar, $url);
+                }
+                if (! $headers->isRedirect()) {
+                    break;
+                }
+                $encoding = new SimpleGetEncoding();
+            } while (! $this->_isTooManyRedirects(++$redirects));
+            return $response;
+        }
+        
+        /**
+         *    Actually make the web request.
+         *    @param SimpleUrl $url                   Target to fetch.
+         *    @param SimpleFormEncoding $encoding     Additional parameters for request.
+         *    @return SimpleHttpResponse              Headers and hopefully content.
+         *    @access protected
+         */
+        function &_fetch($url, $encoding) {
+            $request = &$this->_createRequest($url, $encoding);
+            $response = &$request->fetch($this->_connection_timeout);
+            return $response;
+        }
+        
+        /**
+         *    Creates a full page request.
+         *    @param SimpleUrl $url                 Target to fetch as url object.
+         *    @param SimpleFormEncoding $encoding   POST/GET parameters.
+         *    @return SimpleHttpRequest             New request.
+         *    @access private
+         */
+        function &_createRequest($url, $encoding) {
+            $request = &$this->_createHttpRequest($url, $encoding);
+            $this->_addAdditionalHeaders($request);
+            if ($this->_cookies_enabled) {
+                $request->readCookiesFromJar($this->_cookie_jar, $url);
+            }
+            $this->_authenticator->addHeaders($request, $url);
+            return $request;
+        }
+        
+        /**
+         *    Builds the appropriate HTTP request object.
+         *    @param SimpleUrl $url                  Target to fetch as url object.
+         *    @param SimpleFormEncoding $parameters  POST/GET parameters.
+         *    @return SimpleHttpRequest              New request object.
+         *    @access protected
+         */
+        function &_createHttpRequest($url, $encoding) {
+            $request = &new SimpleHttpRequest($this->_createRoute($url), $encoding);
+            return $request;
+        }
+        
+        /**
+         *    Sets up either a direct route or via a proxy.
+         *    @param SimpleUrl $url   Target to fetch as url object.
+         *    @return SimpleRoute     Route to take to fetch URL.
+         *    @access protected
+         */
+        function &_createRoute($url) {
+            if ($this->_proxy) {
+                $route = &new SimpleProxyRoute(
+                        $url,
+                        $this->_proxy,
+                        $this->_proxy_username,
+                        $this->_proxy_password);
+            } else {
+                $route = &new SimpleRoute($url);
+            }
+            return $route;
+        }
+        
+        /**
+         *    Adds additional manual headers.
+         *    @param SimpleHttpRequest $request    Outgoing request.
+         *    @access private
+         */
+        function _addAdditionalHeaders(&$request) {
+            foreach ($this->_additional_headers as $header) {
+                $request->addHeaderLine($header);
+            }
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/web_tester.php b/lib/simpletestlib/web_tester.php
new file mode 100644 (file)
index 0000000..c98507d
--- /dev/null
@@ -0,0 +1,1455 @@
+<?php
+    /**
+     * Base include file for SimpleTest.
+     * @package        SimpleTest
+     * @subpackage     WebTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/test_case.php');
+    require_once(dirname(__FILE__) . '/browser.php');
+    require_once(dirname(__FILE__) . '/page.php');
+    require_once(dirname(__FILE__) . '/expectation.php');
+    /**#@-*/
+    
+    /**
+     *    Test for an HTML widget value match.
+        *        @package SimpleTest
+        *        @subpackage WebTester
+     */
+    class FieldExpectation extends SimpleExpectation {
+        var $_value;
+        
+        /**
+         *    Sets the field value to compare against.
+         *    @param mixed $value     Test value to match. Can be an
+         *                            expectation for say pattern matching.
+         *    @param string $message  Optiona message override. Can use %s as
+         *                            a placeholder for the original message.
+         *    @access public
+         */
+        function FieldExpectation($value, $message = '%s') {
+            $this->SimpleExpectation($message);
+            if (is_array($value)) {
+                sort($value);
+            }
+            $this->_value = $value;
+        }
+        
+        /**
+         *    Tests the expectation. True if it matches
+         *    a string value or an array value in any order.
+         *    @param mixed $compare        Comparison value. False for
+         *                                 an unset field.
+         *    @return boolean              True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            if ($this->_value === false) {
+                return ($compare === false);
+            }
+            if ($this->_isSingle($this->_value)) {
+                return $this->_testSingle($compare);
+            }
+            if (is_array($this->_value)) {
+                return $this->_testMultiple($compare);
+            }
+            return false;
+        }
+        
+        /**
+         *    Tests for valid field comparisons with a single option.
+         *    @param mixed $value       Value to type check.
+         *    @return boolean           True if integer, string or float.
+         *    @access private
+         */
+        function _isSingle($value) {
+            return is_string($value) || is_integer($value) || is_float($value);
+        }
+        
+        /**
+         *    String comparison for simple field with a single option.
+         *    @param mixed $compare    String to test against.
+         *    @returns boolean         True if matching.
+         *    @access private
+         */
+        function _testSingle($compare) {
+            if (is_array($compare) && count($compare) == 1) {
+                $compare = $compare[0];
+            }
+            if (! $this->_isSingle($compare)) {
+                return false;
+            }
+            return ($this->_value == $compare);
+        }
+        
+        /**
+         *    List comparison for multivalue field.
+         *    @param mixed $compare    List in any order to test against.
+         *    @returns boolean         True if matching.
+         *    @access private
+         */
+        function _testMultiple($compare) {
+            if (is_string($compare)) {
+                $compare = array($compare);
+            }
+            if (! is_array($compare)) {
+                return false;
+            }
+            sort($compare);
+            return ($this->_value === $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();
+            if (is_array($compare)) {
+                sort($compare);
+            }
+            if ($this->test($compare)) {
+                return "Field expectation [" . $dumper->describeValue($this->_value) . "]";
+            } else {
+                return "Field expectation [" . $dumper->describeValue($this->_value) .
+                        "] fails with [" .
+                        $this->_dumper->describeValue($compare) . "] " .
+                        $this->_dumper->describeDifference($this->_value, $compare);
+            }
+        }
+    }
+    
+    /**
+     *    Test for a specific HTTP header within a header block.
+        *        @package SimpleTest
+        *        @subpackage WebTester
+     */
+    class HttpHeaderExpectation extends SimpleExpectation {
+        var $_expected_header;
+        var $_expected_value;
+        
+        /**
+         *    Sets the field and value to compare against.
+         *    @param string $header   Case insenstive trimmed header name.
+         *    @param mixed $value     Optional value to compare. If not
+         *                            given then any value will match. If
+         *                            an expectation object then that will
+         *                            be used instead.
+         *    @param string $message  Optiona message override. Can use %s as
+         *                            a placeholder for the original message.
+         */
+        function HttpHeaderExpectation($header, $value = false, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_expected_header = $this->_normaliseHeader($header);
+            $this->_expected_value = $value;
+        }
+        
+        /**
+         *    Accessor for aggregated object.
+         *    @return mixed        Expectation set in constructor.
+         *    @access protected
+         */
+        function _getExpectation() {
+            return $this->_expected_value;
+        }
+        
+        /**
+         *    Removes whitespace at ends and case variations.
+         *    @param string $header    Name of header.
+         *    @param string            Trimmed and lowecased header
+         *                             name.
+         *    @access private
+         */
+        function _normaliseHeader($header) {
+            return strtolower(trim($header));
+        }
+        
+        /**
+         *    Tests the expectation. True if it matches
+         *    a string value or an array value in any order.
+         *    @param mixed $compare   Raw header block to search.
+         *    @return boolean         True if header present.
+         *    @access public
+         */
+        function test($compare) {
+            return is_string($this->_findHeader($compare));
+        }
+        
+        /**
+         *    Searches the incoming result. Will extract the matching
+         *    line as text.
+         *    @param mixed $compare   Raw header block to search.
+         *    @return string          Matching header line.
+         *    @access protected
+         */
+        function _findHeader($compare) {
+            $lines = split("\r\n", $compare);
+            foreach ($lines as $line) {
+                if ($this->_testHeaderLine($line)) {
+                    return $line;
+                }
+            }
+            return false;
+        }
+        
+        /**
+         *    Compares a single header line against the expectation.
+         *    @param string $line      A single line to compare.
+         *    @return boolean          True if matched.
+         *    @access private
+         */
+        function _testHeaderLine($line) {
+            if (count($parsed = split(':', $line, 2)) < 2) {
+                return false;
+            }
+            list($header, $value) = $parsed;
+            if ($this->_normaliseHeader($header) != $this->_expected_header) {
+                return false;
+            }
+            return $this->_testHeaderValue($value, $this->_expected_value);
+        }
+        
+        /**
+         *    Tests the value part of the header.
+         *    @param string $value        Value to test.
+         *    @param mixed $expected      Value to test against.
+         *    @return boolean             True if matched.
+         *    @access protected
+         */
+        function _testHeaderValue($value, $expected) {
+            if ($expected === false) {
+                return true;
+            }
+            if (SimpleExpectation::isExpectation($expected)) {
+                return $expected->test(trim($value));
+            }
+            return (trim($value) == trim($expected));
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Raw header block to search.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if (SimpleExpectation::isExpectation($this->_expected_value)) {
+                $message = $this->_expected_value->testMessage($compare);
+            } else {
+                $message = $this->_expected_header .
+                        ($this->_expected_value ? ': ' . $this->_expected_value : '');
+            }
+            if (is_string($line = $this->_findHeader($compare))) {
+                return "Searching for header [$message] found [$line]";
+            } else {
+                return "Failed to find header [$message]";
+            }
+        }
+    }
+      
+    /**
+     *    Test for a specific HTTP header within a header block that
+     *    should not be found.
+        *        @package SimpleTest
+        *        @subpackage WebTester
+     */
+    class NoHttpHeaderExpectation extends HttpHeaderExpectation {
+        var $_expected_header;
+        var $_expected_value;
+        
+        /**
+         *    Sets the field and value to compare against.
+         *    @param string $unwanted   Case insenstive trimmed header name.
+         *    @param string $message    Optiona message override. Can use %s as
+         *                              a placeholder for the original message.
+         */
+        function NoHttpHeaderExpectation($unwanted, $message = '%s') {
+            $this->HttpHeaderExpectation($unwanted, false, $message);
+        }
+        
+        /**
+         *    Tests that the unwanted header is not found.
+         *    @param mixed $compare   Raw header block to search.
+         *    @return boolean         True if header present.
+         *    @access public
+         */
+        function test($compare) {
+            return ($this->_findHeader($compare) === false);
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Raw header block to search.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            $expectation = $this->_getExpectation();
+            if (is_string($line = $this->_findHeader($compare))) {
+                return "Found unwanted header [$expectation] with [$line]";
+            } else {
+                return "Did not find unwanted header [$expectation]";
+            }
+        }
+    }
+    
+    /**
+     *    Test for a text substring.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class TextExpectation extends SimpleExpectation {
+        var $_substring;
+        
+        /**
+         *    Sets the value to compare against.
+         *    @param string $substring  Text to search for.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function TextExpectation($substring, $message = '%s') {
+            $this->SimpleExpectation($message);
+            $this->_substring = $substring;
+        }
+        
+        /**
+         *    Accessor for the substring.
+         *    @return string       Text to match.
+         *    @access protected
+         */
+        function _getSubstring() {
+            return $this->_substring;
+        }
+        
+        /**
+         *    Tests the expectation. True if the text contains the
+         *    substring.
+         *    @param string $compare        Comparison value.
+         *    @return boolean               True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return (strpos($compare, $this->_substring) !== false);
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param mixed $compare      Comparison value.
+         *    @return string             Description of success
+         *                               or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if ($this->test($compare)) {
+                return $this->_describeTextMatch($this->_getSubstring(), $compare);
+            } else {
+                $dumper = &$this->_getDumper();
+                return "Text [" . $this->_getSubstring() .
+                        "] not detected in [" .
+                        $dumper->describeValue($compare) . "]";
+            }
+        }
+        
+        /**
+         *    Describes a pattern match including the string
+         *    found and it's position.
+         *    @param string $substring      Text to search for.
+         *    @param string $subject        Subject to search.
+         *    @access protected
+         */
+        function _describeTextMatch($substring, $subject) {
+            $position = strpos($subject, $substring);
+            $dumper = &$this->_getDumper();
+            return "Text [$substring] detected at character [$position] in [" .
+                    $dumper->describeValue($subject) . "] in region [" .
+                    $dumper->clipString($subject, 100, $position) . "]";
+        }
+    }
+    
+    /**
+     *    Fail if a substring is detected within the
+     *    comparison text.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class NoTextExpectation extends TextExpectation {
+        
+        /**
+         *    Sets the reject pattern
+         *    @param string $substring  Text to search for.
+         *    @param string $message    Customised message on failure.
+         *    @access public
+         */
+        function NoTextExpectation($substring, $message = '%s') {
+            $this->TextExpectation($substring, $message);
+        }
+        
+        /**
+         *    Tests the expectation. False if the substring appears
+         *    in the text.
+         *    @param string $compare        Comparison value.
+         *    @return boolean               True if correct.
+         *    @access public
+         */
+        function test($compare) {
+            return ! parent::test($compare);
+        }
+        
+        /**
+         *    Returns a human readable test message.
+         *    @param string $compare      Comparison value.
+         *    @return string              Description of success
+         *                                or failure.
+         *    @access public
+         */
+        function testMessage($compare) {
+            if ($this->test($compare)) {
+                $dumper = &$this->_getDumper();
+                return "Text [" . $this->_getSubstring() .
+                        "] not detected in [" .
+                        $dumper->describeValue($compare) . "]";
+            } else {
+                return $this->_describeTextMatch($this->_getSubstring(), $compare);
+            }
+        }
+    }
+    
+    /**
+     *    Test case for testing of web pages. Allows
+     *    fetching of pages, parsing of HTML and
+     *    submitting forms.
+        *    @package SimpleTest
+        *    @subpackage WebTester
+     */
+    class WebTestCase extends SimpleTestCase {
+        var $_browser;
+        var $_ignore_errors = false;
+        
+        /**
+         *    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 WebTestCase($label = false) {
+            $this->SimpleTestCase($label);
+        }
+        
+        /**
+         *    Announces the start of the test.
+         *    @param string $method    Test method just started.
+         *    @access public
+         */
+        function before($method) {
+            parent::before($method);
+            $this->setBrowser($this->createBrowser());
+        }
+
+        /**
+         *    Announces the end of the test. Includes private clean up.
+         *    @param string $method    Test method just finished.
+         *    @access public
+         */
+        function after($method) {
+            $this->unsetBrowser();
+            parent::after($method);
+        }
+        
+        /**
+         *    Gets a current browser reference for setting
+         *    special expectations or for detailed
+         *    examination of page fetches.
+         *    @return SimpleBrowser     Current test browser object.
+         *    @access public
+         */
+        function &getBrowser() {
+            return $this->_browser;
+        }
+        
+        /**
+         *    Gets a current browser reference for setting
+         *    special expectations or for detailed
+         *    examination of page fetches.
+         *    @param SimpleBrowser $browser    New test browser object.
+         *    @access public
+         */
+        function setBrowser(&$browser) {
+            return $this->_browser = &$browser;
+        }
+          
+        /**
+         *    Clears the current browser reference to help the
+         *    PHP garbage collector.
+         *    @access public
+         */
+        function unsetBrowser() {
+            unset($this->_browser);
+        }
+      
+        /**
+         *    Creates a new default web browser object.
+         *    Will be cleared at the end of the test method.
+         *    @return TestBrowser           New browser.
+         *    @access public
+         */
+        function &createBrowser() {
+            $browser = &new SimpleBrowser();
+            return $browser;
+        }
+        
+        /**
+         *    Gets the last response error.
+         *    @return string    Last low level HTTP error.
+         *    @access public
+         */
+        function getTransportError() {
+            return $this->_browser->getTransportError();
+        }
+          
+        /**
+         *    Accessor for the currently selected URL.
+         *    @return string        Current location or false if
+         *                          no page yet fetched.
+         *    @access public
+         */
+        function getUrl() {
+            return $this->_browser->getUrl();
+        }
+        
+        /**
+         *    Dumps the current request for debugging.
+         *    @access public
+         */
+        function showRequest() {
+            $this->dump($this->_browser->getRequest());
+        }
+        
+        /**
+         *    Dumps the current HTTP headers for debugging.
+         *    @access public
+         */
+        function showHeaders() {
+            $this->dump($this->_browser->getHeaders());
+        }
+      
+        /**
+         *    Dumps the current HTML source for debugging.
+         *    @access public
+         */
+        function showSource() {
+            $this->dump($this->_browser->getContent());
+        }
+      
+        /**
+         *    Dumps the visible text only for debugging.
+         *    @access public
+         */
+        function showText() {
+            $this->dump(wordwrap($this->_browser->getContentAsText(), 80));
+        }
+        
+        /**
+         *    Simulates the closing and reopening of the browser.
+         *    Temporary cookies will be discarded and timed
+         *    cookies will be expired if later than the
+         *    specified time.
+         *    @param string/integer $date Time when session restarted.
+         *                                If ommitted then all persistent
+         *                                cookies are kept. Time is either
+         *                                Cookie format string or timestamp.
+         *    @access public
+         */
+        function restart($date = false) {
+            if ($date === false) {
+                $date = time();
+            }
+            $this->_browser->restart($date);
+        }
+        
+        /**
+         *    Moves cookie expiry times back into the past.
+         *    Useful for testing timeouts and expiries.
+         *    @param integer $interval    Amount to age in seconds.
+         *    @access public
+         */
+        function ageCookies($interval) {
+            $this->_browser->ageCookies($interval);
+        }
+        
+        /**
+         *    Disables frames support. Frames will not be fetched
+         *    and the frameset page will be used instead.
+         *    @access public
+         */
+        function ignoreFrames() {
+            $this->_browser->ignoreFrames();
+        }
+        
+        /**
+         *    Switches off cookie sending and recieving.
+         *    @access public
+         */
+        function ignoreCookies() {
+            $this->_browser->ignoreCookies();
+        }
+        
+        /**
+         *    Skips errors for the next request only. You might
+         *    want to confirm that a page is unreachable for
+         *    example.
+         *    @access public
+         */
+        function ignoreErrors() {
+            $this->_ignore_errors = true;
+        }
+        
+        /**
+         *    Issues a fail if there is a transport error anywhere
+         *    in the current frameset. Only one such error is
+         *    reported.
+         *    @param string/boolean $result   HTML or failure.
+         *    @return string/boolean $result  Passes through result.
+         *    @access private
+         */
+        function _failOnError($result) {
+            if (! $this->_ignore_errors) {
+                if ($error = $this->_browser->getTransportError()) {
+                    $this->fail($error);
+                }
+            }
+            $this->_ignore_errors = false;
+            return $result;
+        }
+
+        /**
+         *    Adds a header to every fetch.
+         *    @param string $header       Header line to add to every
+         *                                request until cleared.
+         *    @access public
+         */
+        function addHeader($header) {
+            $this->_browser->addHeader($header);
+        }
+        
+        /**
+         *    Sets the maximum number of redirects before
+         *    the web page is loaded regardless.
+         *    @param integer $max        Maximum hops.
+         *    @access public
+         */
+        function setMaximumRedirects($max) {
+            if (! $this->_browser) {
+                trigger_error(
+                        'Can only set maximum redirects in a test method, setUp() or tearDown()');
+            }
+            $this->_browser->setMaximumRedirects($max);
+        }
+        
+        /**
+         *    Sets the socket timeout for opening a connection and
+         *    receiving at least one byte of information.
+         *    @param integer $timeout      Maximum time in seconds.
+         *    @access public
+         */
+        function setConnectionTimeout($timeout) {
+            $this->_browser->setConnectionTimeout($timeout);
+        }
+        
+        /**
+         *    Sets proxy to use on all requests for when
+         *    testing from behind a firewall. Set URL
+         *    to false to disable.
+         *    @param string $proxy        Proxy URL.
+         *    @param string $username     Proxy username for authentication.
+         *    @param string $password     Proxy password for authentication.
+         *    @access public
+         */
+        function useProxy($proxy, $username = false, $password = false) {
+            $this->_browser->useProxy($proxy, $username, $password);
+        }
+        
+        /**
+         *    Fetches a page into the page buffer. If
+         *    there is no base for the URL then the
+         *    current base URL is used. After the fetch
+         *    the base URL reflects the new location.
+         *    @param string $url          URL to fetch.
+         *    @param hash $parameters     Optional additional GET data.
+         *    @return boolean/string      Raw page on success.
+         *    @access public
+         */
+        function get($url, $parameters = false) {
+            return $this->_failOnError($this->_browser->get($url, $parameters));
+        }
+        
+        /**
+         *    Fetches a page by POST into the page buffer.
+         *    If there is no base for the URL then the
+         *    current base URL is used. After the fetch
+         *    the base URL reflects the new location.
+         *    @param string $url          URL to fetch.
+         *    @param hash $parameters     Optional additional GET data.
+         *    @return boolean/string      Raw page on success.
+         *    @access public
+         */
+        function post($url, $parameters = false) {
+            return $this->_failOnError($this->_browser->post($url, $parameters));
+        }
+        
+        /**
+         *    Does a HTTP HEAD fetch, fetching only the page
+         *    headers. The current base URL is unchanged by this.
+         *    @param string $url          URL to fetch.
+         *    @param hash $parameters     Optional additional GET data.
+         *    @return boolean             True on success.
+         *    @access public
+         */
+        function head($url, $parameters = false) {
+            return $this->_failOnError($this->_browser->head($url, $parameters));
+        }
+        
+        /**
+         *    Equivalent to hitting the retry button on the
+         *    browser. Will attempt to repeat the page fetch.
+         *    @return boolean     True if fetch succeeded.
+         *    @access public
+         */
+        function retry() {
+            return $this->_failOnError($this->_browser->retry());
+        }
+        
+        /**
+         *    Equivalent to hitting the back button on the
+         *    browser.
+         *    @return boolean     True if history entry and
+         *                        fetch succeeded.
+         *    @access public
+         */
+        function back() {
+            return $this->_failOnError($this->_browser->back());
+        }
+        
+        /**
+         *    Equivalent to hitting the forward button on the
+         *    browser.
+         *    @return boolean     True if history entry and
+         *                        fetch succeeded.
+         *    @access public
+         */
+        function forward() {
+            return $this->_failOnError($this->_browser->forward());
+        }
+        
+        /**
+         *    Retries a request after setting the authentication
+         *    for the current realm.
+         *    @param string $username    Username for realm.
+         *    @param string $password    Password for realm.
+         *    @return boolean/string     HTML on successful fetch. Note
+         *                               that authentication may still have
+         *                               failed.
+         *    @access public
+         */
+        function authenticate($username, $password) {
+            return $this->_failOnError(
+                    $this->_browser->authenticate($username, $password));
+        }
+        
+        /**
+         *    Gets the cookie value for the current browser context.
+         *    @param string $name          Name of cookie.
+         *    @return string               Value of cookie or false if unset.
+         *    @access public
+         */
+        function getCookie($name) {
+            return $this->_browser->getCurrentCookieValue($name);
+        }
+        
+        /**
+         *    Sets a cookie in the current browser.
+         *    @param string $name          Name of cookie.
+         *    @param string $value         Cookie value.
+         *    @param string $host          Host upon which the cookie is valid.
+         *    @param string $path          Cookie path if not host wide.
+         *    @param string $expiry        Expiry date.
+         *    @access public
+         */
+        function setCookie($name, $value, $host = false, $path = "/", $expiry = false) {
+            $this->_browser->setCookie($name, $value, $host, $path, $expiry);
+        }
+        
+        /**
+         *    Accessor for current frame focus. Will be
+         *    false if no frame has focus.
+         *    @return integer/string/boolean    Label if any, otherwise
+         *                                      the position in the frameset
+         *                                      or false if none.
+         *    @access public
+         */
+        function getFrameFocus() {
+            return $this->_browser->getFrameFocus();
+        }
+        
+        /**
+         *    Sets the focus by index. The integer index starts from 1.
+         *    @param integer $choice    Chosen frame.
+         *    @return boolean           True if frame exists.
+         *    @access public
+         */
+        function setFrameFocusByIndex($choice) {
+            return $this->_browser->setFrameFocusByIndex($choice);
+        }
+        
+        /**
+         *    Sets the focus by name.
+         *    @param string $name    Chosen frame.
+         *    @return boolean        True if frame exists.
+         *    @access public
+         */
+        function setFrameFocus($name) {
+            return $this->_browser->setFrameFocus($name);
+        }
+        
+        /**
+         *    Clears the frame focus. All frames will be searched
+         *    for content.
+         *    @access public
+         */
+        function clearFrameFocus() {
+            return $this->_browser->clearFrameFocus();
+        }
+        
+        /**
+         *    Clicks a visible text item. Will first try buttons,
+         *    then links and then images.
+         *    @param string $label        Visible text or alt text.
+         *    @return string/boolean      Raw page or false.
+         *    @access public
+         */
+        function click($label) {
+            return $this->_failOnError($this->_browser->click($label));
+        }
+        
+        /**
+         *    Clicks the submit button by label. The owning
+         *    form will be submitted by this.
+         *    @param string $label    Button label. An unlabeled
+         *                            button can be triggered by 'Submit'.
+         *    @param hash $additional Additional form values.
+         *    @return boolean/string  Page on success, else false.
+         *    @access public
+         */
+        function clickSubmit($label = 'Submit', $additional = false) {
+            return $this->_failOnError(
+                    $this->_browser->clickSubmit($label, $additional));
+        }
+        
+        /**
+         *    Clicks the submit button by name attribute. The owning
+         *    form will be submitted by this.
+         *    @param string $name     Name attribute of button.
+         *    @param hash $additional Additional form values.
+         *    @return boolean/string  Page on success.
+         *    @access public
+         */
+        function clickSubmitByName($name, $additional = false) {
+            return $this->_failOnError(
+                    $this->_browser->clickSubmitByName($name, $additional));
+        }
+        
+        /**
+         *    Clicks the submit button by ID attribute. The owning
+         *    form will be submitted by this.
+         *    @param string $id       ID attribute of button.
+         *    @param hash $additional Additional form values.
+         *    @return boolean/string  Page on success.
+         *    @access public
+         */
+        function clickSubmitById($id, $additional = false) {
+            return $this->_failOnError(
+                    $this->_browser->clickSubmitById($id, $additional));
+        }
+        
+        /**
+         *    Clicks the submit image by some kind of label. Usually
+         *    the alt tag or the nearest equivalent. The owning
+         *    form will be submitted by this. Clicking outside of
+         *    the boundary of the coordinates will result in
+         *    a failure.
+         *    @param string $label    Alt attribute of button.
+         *    @param integer $x       X-coordinate of imaginary click.
+         *    @param integer $y       Y-coordinate of imaginary click.
+         *    @param hash $additional Additional form values.
+         *    @return boolean/string  Page on success.
+         *    @access public
+         */
+        function clickImage($label, $x = 1, $y = 1, $additional = false) {
+            return $this->_failOnError(
+                    $this->_browser->clickImage($label, $x, $y, $additional));
+        }
+        
+        /**
+         *    Clicks the submit image by the name. Usually
+         *    the alt tag or the nearest equivalent. The owning
+         *    form will be submitted by this. Clicking outside of
+         *    the boundary of the coordinates will result in
+         *    a failure.
+         *    @param string $name     Name attribute of button.
+         *    @param integer $x       X-coordinate of imaginary click.
+         *    @param integer $y       Y-coordinate of imaginary click.
+         *    @param hash $additional Additional form values.
+         *    @return boolean/string  Page on success.
+         *    @access public
+         */
+        function clickImageByName($name, $x = 1, $y = 1, $additional = false) {
+            return $this->_failOnError(
+                    $this->_browser->clickImageByName($name, $x, $y, $additional));
+        }
+        
+        /**
+         *    Clicks the submit image by ID attribute. The owning
+         *    form will be submitted by this. Clicking outside of
+         *    the boundary of the coordinates will result in
+         *    a failure.
+         *    @param integer/string $id   ID attribute of button.
+         *    @param integer $x           X-coordinate of imaginary click.
+         *    @param integer $y           Y-coordinate of imaginary click.
+         *    @param hash $additional     Additional form values.
+         *    @return boolean/string      Page on success.
+         *    @access public
+         */
+        function clickImageById($id, $x = 1, $y = 1, $additional = false) {
+            return $this->_failOnError(
+                    $this->_browser->clickImageById($id, $x, $y, $additional));
+        }
+        
+        /**
+         *    Submits a form by the ID.
+         *    @param string $id       Form ID. No button information
+         *                            is submitted this way.
+         *    @return boolean/string  Page on success.
+         *    @access public
+         */
+        function submitFormById($id) {
+            return $this->_failOnError($this->_browser->submitFormById($id));
+        }
+        
+        /**
+         *    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 normalised space.
+         *    @param string $label     Text between the anchor tags.
+         *    @param integer $index    Link position counting from zero.
+         *    @return boolean/string   Page on success.
+         *    @access public
+         */
+        function clickLink($label, $index = 0) {
+            return $this->_failOnError($this->_browser->clickLink($label, $index));
+        }
+        
+        /**
+         *    Follows a link by id attribute.
+         *    @param string $id        ID attribute value.
+         *    @return boolean/string   Page on success.
+         *    @access public
+         */
+        function clickLinkById($id) {
+            return $this->_failOnError($this->_browser->clickLinkById($id));
+        }
+        
+        /**
+         *    Will trigger a pass if the two parameters have
+         *    the same value only. Otherwise a fail. This
+         *    is for testing hand extracted text, etc.
+         *    @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") {
+            return $this->assert(
+                    new EqualExpectation($first),
+                    $second,
+                    $message);
+        }
+        
+        /**
+         *    Will trigger a pass if the two parameters have
+         *    a different value. Otherwise a fail. This
+         *    is for testing hand extracted text, etc.
+         *    @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") {
+            return $this->assert(
+                    new NotEqualExpectation($first),
+                    $second,
+                    $message);
+        }
+        
+        /**
+         *    Tests for the presence of a link label. Match is
+         *    case insensitive with normalised space.
+         *    @param string $label     Text between the anchor tags.
+         *    @param string $message   Message to display. Default
+         *                             can be embedded with %s.
+         *    @return boolean          True if link present.
+         *    @access public
+         */
+        function assertLink($label, $message = "%s") {
+            return $this->assertTrue(
+                    $this->_browser->isLink($label),
+                    sprintf($message, "Link [$label] should exist"));
+        }
+
+        /**
+         *    Tests for the non-presence of a link label. Match is
+         *    case insensitive with normalised space.
+         *    @param string/integer $label    Text between the anchor tags
+         *                                    or ID attribute.
+         *    @param string $message          Message to display. Default
+         *                                    can be embedded with %s.
+         *    @return boolean                 True if link missing.
+         *    @access public
+         */
+        function assertNoLink($label, $message = "%s") {
+            return $this->assertFalse(
+                    $this->_browser->isLink($label),
+                    sprintf($message, "Link [$label] should not exist"));
+        }
+        
+        /**
+         *    Tests for the presence of a link id attribute.
+         *    @param string $id        Id attribute value.
+         *    @param string $message   Message to display. Default
+         *                             can be embedded with %s.
+         *    @return boolean          True if link present.
+         *    @access public
+         */
+        function assertLinkById($id, $message = "%s") {
+            return $this->assertTrue(
+                    $this->_browser->isLinkById($id),
+                    sprintf($message, "Link ID [$id] should exist"));
+        }
+
+        /**
+         *    Tests for the non-presence of a link label. Match is
+         *    case insensitive with normalised space.
+         *    @param string $id        Id attribute value.
+         *    @param string $message   Message to display. Default
+         *                             can be embedded with %s.
+         *    @return boolean          True if link missing.
+         *    @access public
+         */
+        function assertNoLinkById($id, $message = "%s") {
+            return $this->assertFalse(
+                    $this->_browser->isLinkById($id),
+                    sprintf($message, "Link ID [$id] should not exist"));
+        }
+        
+        /**
+         *    Sets all form fields with that label, or name if there
+         *    is no label attached.
+         *    @param string $name    Name of field in forms.
+         *    @param string $value   New value of field.
+         *    @return boolean        True if field exists, otherwise false.
+         *    @access public
+         */
+        function setField($label, $value) {
+            return $this->_browser->setField($label, $value);
+        }
+        
+        /**
+         *    Sets all form fields with that name.
+         *    @param string $name    Name of field in forms.
+         *    @param string $value   New value of field.
+         *    @return boolean        True if field exists, otherwise false.
+         *    @access public
+         */
+        function setFieldByName($name, $value) {
+            return $this->_browser->setFieldByName($name, $value);
+        }
+          
+        /**
+         *    Sets all form fields with that name.
+         *    @param string/integer $id   Id of field in forms.
+         *    @param string $value        New value of field.
+         *    @return boolean             True if field exists, otherwise false.
+         *    @access public
+         */
+        function setFieldById($id, $value) {
+            return $this->_browser->setFieldById($id, $value);
+        }
+        
+        /**
+         *    Confirms that the form element is currently set
+         *    to the expected value. A missing form will always
+         *    fail. If no value is given then only the existence
+         *    of the field is checked.
+         *    @param string $name       Name of field in forms.
+         *    @param mixed $expected    Expected string/array value or
+         *                              false for unset fields.
+         *    @param string $message    Message to display. Default
+         *                              can be embedded with %s.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertField($label, $expected = true, $message = '%s') {
+            $value = $this->_browser->getField($label);
+            return $this->_assertFieldValue($label, $value, $expected, $message);
+        }
+        
+        /**
+         *    Confirms that the form element is currently set
+         *    to the expected value. A missing form element will always
+         *    fail. If no value is given then only the existence
+         *    of the field is checked.
+         *    @param string $name       Name of field in forms.
+         *    @param mixed $expected    Expected string/array value or
+         *                              false for unset fields.
+         *    @param string $message    Message to display. Default
+         *                              can be embedded with %s.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertFieldByName($name, $expected = true, $message = '%s') {
+            $value = $this->_browser->getFieldByName($name);
+            return $this->_assertFieldValue($name, $value, $expected, $message);
+        }
+         
+        /**
+         *    Confirms that the form element is currently set
+         *    to the expected value. A missing form will always
+         *    fail. If no ID is given then only the existence
+         *    of the field is checked.
+         *    @param string/integer $id  Name of field in forms.
+         *    @param mixed $expected     Expected string/array value or
+         *                               false for unset fields.
+         *    @param string $message     Message to display. Default
+         *                               can be embedded with %s.
+         *    @return boolean            True if pass.
+         *    @access public
+         */
+        function assertFieldById($id, $expected = true, $message = '%s') {
+            $value = $this->_browser->getFieldById($id);
+            return $this->_assertFieldValue($id, $value, $expected, $message);
+        }
+        
+        /**
+         *    Tests the field value against the expectation.
+         *    @param string $identifier      Name, ID or label.
+         *    @param mixed $value            Current field value.
+         *    @param mixed $expected         Expected value to match.
+         *    @param string $message         Failure message.
+         *    @return boolean                True if pass
+         *    @access protected
+         */
+        function _assertFieldValue($identifier, $value, $expected, $message) {
+            if ($expected === true) {
+                return $this->assertTrue(
+                        isset($value),
+                        sprintf($message, "Field [$identifier] should exist"));
+            }
+            if (! SimpleExpectation::isExpectation($expected)) {
+                $identifier = str_replace('%', '%%', $identifier);
+                $expected = new FieldExpectation(
+                        $expected,
+                        "Field [$identifier] should match with [%s]");
+            }
+            return $this->assert($expected, $value, $message);
+        }
+       
+        /**
+         *    Checks the response code against a list
+         *    of possible values.
+         *    @param array $responses    Possible responses for a pass.
+         *    @param string $message     Message to display. Default
+         *                               can be embedded with %s.
+         *    @return boolean            True if pass.
+         *    @access public
+         */
+        function assertResponse($responses, $message = '%s') {
+            $responses = (is_array($responses) ? $responses : array($responses));
+            $code = $this->_browser->getResponseCode();
+            $message = sprintf($message, "Expecting response in [" .
+                    implode(", ", $responses) . "] got [$code]");
+            return $this->assertTrue(in_array($code, $responses), $message);
+        }
+        
+        /**
+         *    Checks the mime type against a list
+         *    of possible values.
+         *    @param array $types      Possible mime types for a pass.
+         *    @param string $message   Message to display.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertMime($types, $message = '%s') {
+            $types = (is_array($types) ? $types : array($types));
+            $type = $this->_browser->getMimeType();
+            $message = sprintf($message, "Expecting mime type in [" .
+                    implode(", ", $types) . "] got [$type]");
+            return $this->assertTrue(in_array($type, $types), $message);
+        }
+        
+        /**
+         *    Attempt to match the authentication type within
+         *    the security realm we are currently matching.
+         *    @param string $authentication   Usually basic.
+         *    @param string $message          Message to display.
+         *    @return boolean                 True if pass.
+         *    @access public
+         */
+        function assertAuthentication($authentication = false, $message = '%s') {
+            if (! $authentication) {
+                $message = sprintf($message, "Expected any authentication type, got [" .
+                        $this->_browser->getAuthentication() . "]");
+                return $this->assertTrue(
+                        $this->_browser->getAuthentication(),
+                        $message);
+            } else {
+                $message = sprintf($message, "Expected authentication [$authentication] got [" .
+                        $this->_browser->getAuthentication() . "]");
+                return $this->assertTrue(
+                        strtolower($this->_browser->getAuthentication()) == strtolower($authentication),
+                        $message);
+            }
+        }
+        
+        /**
+         *    Checks that no authentication is necessary to view
+         *    the desired page.
+         *    @param string $message     Message to display.
+         *    @return boolean            True if pass.
+         *    @access public
+         */
+        function assertNoAuthentication($message = '%s') {
+            $message = sprintf($message, "Expected no authentication type, got [" .
+                    $this->_browser->getAuthentication() . "]");
+            return $this->assertFalse($this->_browser->getAuthentication(), $message);
+        }
+        
+        /**
+         *    Attempts to match the current security realm.
+         *    @param string $realm     Name of security realm.
+         *    @param string $message   Message to display.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertRealm($realm, $message = '%s') {
+            if (! SimpleExpectation::isExpectation($realm)) {
+                $realm = new EqualExpectation($realm);
+            }
+            return $this->assert(
+                    $realm,
+                    $this->_browser->getRealm(),
+                    "Expected realm -> $message");
+        }
+        
+        /**
+         *    Checks each header line for the required value. If no
+         *    value is given then only an existence check is made.
+         *    @param string $header    Case insensitive header name.
+         *    @param mixed $value      Case sensitive trimmed string to
+         *                             match against. An expectation object
+         *                             can be used for pattern matching.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertHeader($header, $value = false, $message = '%s') {
+            return $this->assert(
+                    new HttpHeaderExpectation($header, $value),
+                    $this->_browser->getHeaders(),
+                    $message);
+        }
+          
+        /**
+         *    @deprecated
+         */
+        function assertHeaderPattern($header, $pattern, $message = '%s') {
+            return $this->assert(
+                    new HttpHeaderExpectation($header, new PatternExpectation($pattern)),
+                    $this->_browser->getHeaders(),
+                    $message);
+        }
+
+        /**
+         *    Confirms that the header type has not been received.
+         *    Only the landing page is checked. If you want to check
+         *    redirect pages, then you should limit redirects so
+         *    as to capture the page you want.
+         *    @param string $header    Case insensitive header name.
+         *    @return boolean          True if pass.
+         *    @access public
+         */
+        function assertNoHeader($header, $message = '%s') {
+            return $this->assert(
+                    new NoHttpHeaderExpectation($header),
+                    $this->_browser->getHeaders(),
+                    $message);
+        }
+          
+        /**
+         *    @deprecated
+         */
+        function assertNoUnwantedHeader($header, $message = '%s') {
+            return $this->assertNoHeader($header, $message);
+        }
+        
+        /**
+         *    Tests the text between the title tags.
+         *    @param string $title      Expected title.
+         *    @param string $message    Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertTitle($title = false, $message = '%s') {
+            if (! SimpleExpectation::isExpectation($title)) {
+                $title = new EqualExpectation($title);
+            }
+            return $this->assert($title, $this->_browser->getTitle(), $message);
+        }
+        
+        /**
+         *    Will trigger a pass if the text is found in the plain
+         *    text form of the page.
+         *    @param string $text       Text to look for.
+         *    @param string $message    Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertText($text, $message = '%s') {
+            return $this->assert(
+                    new TextExpectation($text),
+                    $this->_browser->getContentAsText(),
+                    $message);
+        }
+        
+        /**
+         *       @deprecated
+         */
+        function assertWantedText($text, $message = '%s') {
+               return $this->assertText($text, $message);
+        }
+        
+        /**
+         *    Will trigger a pass if the text is not found in the plain
+         *    text form of the page.
+         *    @param string $text       Text to look for.
+         *    @param string $message    Message to display.
+         *    @return boolean           True if pass.
+         *    @access public
+         */
+        function assertNoText($text, $message = '%s') {
+            return $this->assert(
+                    new NoTextExpectation($text),
+                    $this->_browser->getContentAsText(),
+                    $message);
+        }
+        
+        /**
+         *       @deprecated
+         */
+        function assertNoUnwantedText($text, $message = '%s') {
+               return $this->assertNoText($text, $message);
+        }
+        
+        /**
+         *    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.
+         *    @access public
+         */
+        function assertPattern($pattern, $message = '%s') {
+            return $this->assert(
+                    new PatternExpectation($pattern),
+                    $this->_browser->getContent(),
+                    $message);
+        }
+        
+        /**
+         *       @deprecated
+         */
+        function assertWantedPattern($pattern, $message = '%s') {
+               return $this->assertPattern($pattern, $message);
+        }
+        
+        /**
+         *    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.
+         *    @access public
+         */
+        function assertNoPattern($pattern, $message = '%s') {
+            return $this->assert(
+                    new NoPatternExpectation($pattern),
+                    $this->_browser->getContent(),
+                    $message);
+        }
+        
+        /**
+         *       @deprecated
+         */
+        function assertNoUnwantedPattern($pattern, $message = '%s') {
+               return $this->assertNoPattern($pattern, $message);
+        }
+        
+        /**
+         *    Checks that a cookie is set for the current page
+         *    and optionally checks the value.
+         *    @param string $name        Name of cookie to test.
+         *    @param string $expected    Expected value as a string or
+         *                               false if any value will do.
+         *    @param string $message     Message to display.
+         *    @return boolean            True if pass.
+         *    @access public
+         */
+        function assertCookie($name, $expected = false, $message = '%s') {
+            $value = $this->getCookie($name);
+            if (! $expected) {
+                return $this->assertTrue(
+                        $value,
+                        sprintf($message, "Expecting cookie [$name]"));
+            }
+            if (! SimpleExpectation::isExpectation($expected)) {
+                $expected = new EqualExpectation($expected);
+            }
+            return $this->assert($expected, $value, "Expecting cookie [$name] -> $message");
+        }
+        
+        /**
+         *    Checks that no cookie is present or that it has
+         *    been successfully cleared.
+         *    @param string $name        Name of cookie to test.
+         *    @param string $message     Message to display.
+         *    @return boolean            True if pass.
+         *    @access public
+         */
+        function assertNoCookie($name, $message = '%s') {
+            return $this->assertTrue(
+                    $this->getCookie($name) === false,
+                    sprintf($message, "Not expecting cookie [$name]"));
+        }
+    }
+?>
\ No newline at end of file
diff --git a/lib/simpletestlib/xml.php b/lib/simpletestlib/xml.php
new file mode 100644 (file)
index 0000000..c6158cf
--- /dev/null
@@ -0,0 +1,614 @@
+<?php
+    /**
+     * base include file for SimpleTest
+     * @package        SimpleTest
+     * @subpackage     UnitTester
+     * @version        $Id$
+     */
+
+    /**#@+
+     * include other SimpleTest class files
+     */
+    require_once(dirname(__FILE__) . '/scorer.php');
+    /**#@-*/
+
+    /**
+     *    Creates the XML needed for remote communication
+     *    by SimpleTest.
+        *        @package SimpleTest
+        *        @subpackage UnitTester
+     */
+    class XmlReporter extends SimpleReporter {
+        var $_indent;
+        var $_namespace;
+
+        /**
+         *    Does nothing yet.
+         *    @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);
+        }
+
+        /**
+         *    Increments the pass count.
+         *    @param string $message        Message is ignored.
+         *    @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";
+        }
+
+        /**
+         *    Increments the fail count.
+         *    @param string $message        Message is ignored.
+         *    @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 a PHP error or exception.
+         *    @param string $message        Message is ignored.
+         *    @access public
+         *    @abstract
+         */
+        function paintError($message) {
+            parent::paintError($message);
+            print $this->_getIndent(1);
+            print "<" . $this->_namespace . "exception>";
+            print $this->toParsedXml($message);
+            print "</" . $this->_namespace . "exception>\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', '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 == '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) {
+        }
+    }
+?>