From a0cb32f4b200577f77d8a7c26c36c1af5ee111b1 Mon Sep 17 00:00:00 2001 From: shish Date: Sat, 11 Oct 2008 07:05:24 +0000 Subject: [PATCH] simpletest unit testing framework git-svn-id: file:///home/shish/svn/shimmie2/trunk@1068 7f39781d-f577-437e-ae19-be835c7a54ca --- contrib/simpletest/main.php | 44 + .../HELP_MY_TESTS_DONT_WORK_ANYMORE | 348 ++++ contrib/simpletest/simpletest/LICENSE | 502 ++++++ contrib/simpletest/simpletest/README | 108 ++ contrib/simpletest/simpletest/VERSION | 1 + .../simpletest/simpletest/authentication.php | 238 +++ contrib/simpletest/simpletest/autorun.php | 87 + contrib/simpletest/simpletest/browser.php | 1098 ++++++++++++ contrib/simpletest/simpletest/collector.php | 122 ++ .../simpletest/simpletest/compatibility.php | 173 ++ contrib/simpletest/simpletest/cookies.php | 380 ++++ .../simpletest/default_reporter.php | 133 ++ contrib/simpletest/simpletest/detached.php | 96 + contrib/simpletest/simpletest/docs.zip | Bin 0 -> 129563 bytes contrib/simpletest/simpletest/dumper.php | 360 ++++ contrib/simpletest/simpletest/eclipse.php | 307 ++++ contrib/simpletest/simpletest/encoding.php | 552 ++++++ contrib/simpletest/simpletest/errors.php | 288 +++ contrib/simpletest/simpletest/exceptions.php | 198 +++ contrib/simpletest/simpletest/expectation.php | 895 ++++++++++ .../simpletest/extensions/pear_test_case.php | 198 +++ .../extensions/phpunit_test_case.php | 96 + .../simpletest/extensions/testdox.php | 42 + .../simpletest/extensions/testdox/test.php | 108 ++ contrib/simpletest/simpletest/form.php | 355 ++++ contrib/simpletest/simpletest/frames.php | 596 +++++++ contrib/simpletest/simpletest/http.php | 624 +++++++ contrib/simpletest/simpletest/invoker.php | 139 ++ .../simpletest/simpletest/mock_objects.php | 1581 +++++++++++++++++ contrib/simpletest/simpletest/page.php | 983 ++++++++++ contrib/simpletest/simpletest/parser.php | 764 ++++++++ .../simpletest/simpletest/reflection_php4.php | 136 ++ .../simpletest/simpletest/reflection_php5.php | 380 ++++ contrib/simpletest/simpletest/remote.php | 117 ++ contrib/simpletest/simpletest/reporter.php | 447 +++++ contrib/simpletest/simpletest/scorer.php | 863 +++++++++ contrib/simpletest/simpletest/selector.php | 137 ++ .../simpletest/simpletest/shell_tester.php | 333 ++++ contrib/simpletest/simpletest/simpletest.php | 478 +++++ contrib/simpletest/simpletest/socket.php | 216 +++ contrib/simpletest/simpletest/tag.php | 1418 +++++++++++++++ contrib/simpletest/simpletest/test.zip | Bin 0 -> 67459 bytes contrib/simpletest/simpletest/test_case.php | 708 ++++++++ contrib/simpletest/simpletest/unit_tester.php | 420 +++++ contrib/simpletest/simpletest/url.php | 528 ++++++ contrib/simpletest/simpletest/user_agent.php | 332 ++++ contrib/simpletest/simpletest/web_tester.php | 1541 ++++++++++++++++ contrib/simpletest/simpletest/xml.php | 647 +++++++ contrib/simpletest/theme.php | 52 + 49 files changed, 20169 insertions(+) create mode 100644 contrib/simpletest/main.php create mode 100644 contrib/simpletest/simpletest/HELP_MY_TESTS_DONT_WORK_ANYMORE create mode 100644 contrib/simpletest/simpletest/LICENSE create mode 100644 contrib/simpletest/simpletest/README create mode 100644 contrib/simpletest/simpletest/VERSION create mode 100644 contrib/simpletest/simpletest/authentication.php create mode 100644 contrib/simpletest/simpletest/autorun.php create mode 100644 contrib/simpletest/simpletest/browser.php create mode 100644 contrib/simpletest/simpletest/collector.php create mode 100644 contrib/simpletest/simpletest/compatibility.php create mode 100644 contrib/simpletest/simpletest/cookies.php create mode 100644 contrib/simpletest/simpletest/default_reporter.php create mode 100644 contrib/simpletest/simpletest/detached.php create mode 100644 contrib/simpletest/simpletest/docs.zip create mode 100644 contrib/simpletest/simpletest/dumper.php create mode 100644 contrib/simpletest/simpletest/eclipse.php create mode 100644 contrib/simpletest/simpletest/encoding.php create mode 100644 contrib/simpletest/simpletest/errors.php create mode 100644 contrib/simpletest/simpletest/exceptions.php create mode 100644 contrib/simpletest/simpletest/expectation.php create mode 100644 contrib/simpletest/simpletest/extensions/pear_test_case.php create mode 100644 contrib/simpletest/simpletest/extensions/phpunit_test_case.php create mode 100644 contrib/simpletest/simpletest/extensions/testdox.php create mode 100644 contrib/simpletest/simpletest/extensions/testdox/test.php create mode 100644 contrib/simpletest/simpletest/form.php create mode 100644 contrib/simpletest/simpletest/frames.php create mode 100644 contrib/simpletest/simpletest/http.php create mode 100644 contrib/simpletest/simpletest/invoker.php create mode 100644 contrib/simpletest/simpletest/mock_objects.php create mode 100644 contrib/simpletest/simpletest/page.php create mode 100644 contrib/simpletest/simpletest/parser.php create mode 100644 contrib/simpletest/simpletest/reflection_php4.php create mode 100644 contrib/simpletest/simpletest/reflection_php5.php create mode 100644 contrib/simpletest/simpletest/remote.php create mode 100644 contrib/simpletest/simpletest/reporter.php create mode 100644 contrib/simpletest/simpletest/scorer.php create mode 100644 contrib/simpletest/simpletest/selector.php create mode 100644 contrib/simpletest/simpletest/shell_tester.php create mode 100644 contrib/simpletest/simpletest/simpletest.php create mode 100644 contrib/simpletest/simpletest/socket.php create mode 100644 contrib/simpletest/simpletest/tag.php create mode 100644 contrib/simpletest/simpletest/test.zip create mode 100644 contrib/simpletest/simpletest/test_case.php create mode 100644 contrib/simpletest/simpletest/unit_tester.php create mode 100644 contrib/simpletest/simpletest/url.php create mode 100644 contrib/simpletest/simpletest/user_agent.php create mode 100644 contrib/simpletest/simpletest/web_tester.php create mode 100644 contrib/simpletest/simpletest/xml.php create mode 100644 contrib/simpletest/theme.php diff --git a/contrib/simpletest/main.php b/contrib/simpletest/main.php new file mode 100644 index 00000000..52df5dce --- /dev/null +++ b/contrib/simpletest/main.php @@ -0,0 +1,44 @@ + + * License: GPLv2 + * Description: adds unit testing to SCore + */ + +require_once('simpletest/web_tester.php'); +require_once('simpletest/reporter.php'); + +class AllTests extends TestSuite { + function AllTests() { + $this->TestSuite('All tests'); + foreach(glob("ext/*/test.php") as $file) { + $this->addFile($file); + } + } +} + +class SimpleSCoreTest implements Extension { + var $theme; + + public function receive_event(Event $event) { + if(is_null($this->theme)) $this->theme = get_theme_object($this); + + if(($event instanceof PageRequestEvent) && $event->page_matches("test/all")) { + $event->page->set_title("Test Results"); + $event->page->set_heading("Test Results"); + $event->page->add_block(new NavBlock()); + + $all = new AllTests(); + $all->run(new SCoreReporter($event->page)); + } + + if($event instanceof UserBlockBuildingEvent) { + if($event->user->is_admin()) { + $event->add_link("Run Tests", make_link("test/all")); + } + } + } +} +add_event_listener(new SimpleSCoreTest()); +?> diff --git a/contrib/simpletest/simpletest/HELP_MY_TESTS_DONT_WORK_ANYMORE b/contrib/simpletest/simpletest/HELP_MY_TESTS_DONT_WORK_ANYMORE new file mode 100644 index 00000000..8ac9cf2a --- /dev/null +++ b/contrib/simpletest/simpletest/HELP_MY_TESTS_DONT_WORK_ANYMORE @@ -0,0 +1,348 @@ +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... + +No method getRelativeUrls() or getAbsoluteUrls() +------------------------------------------------ +These methods were always a bit weird anyway, and +the new parsing of the base tag makes them more so. +They have been replaced with getUrls() instead. If +you want the old functionality then simply chop +off the current domain from getUrl(). + +Method setWildcard() removed in mocks +------------------------------------- +Even setWildcard() has been removed in 1.0.1beta now. +If you want to test explicitely for a '*' string, then +simply pass in new IdenticalExpectation('*') instead. + +No method _getTest() on mocks +----------------------------- +This has finally been removed. It was a pretty esoteric +flex point anyway. It was there to allow the mocks to +work with other test tools, but no one does this. + +No method assertError(), assertNoErrors(), swallowErrors() +---------------------------------------------------------- +These have been deprecated in 1.0.1beta in favour of +expectError() and expectException(). assertNoErrors() is +redundant if you use expectError() as failures are now reported +immediately. + +No method TestCase::signal() +---------------------------- +This has been deprecated in favour of triggering an error or +throwing an exception. Deprecated as of 1.0.1beta. + +No method TestCase::sendMessage() +--------------------------------- +This has been deprecated as of 1.0.1beta. + +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::PHPUnit2 +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/contrib/simpletest/simpletest/LICENSE b/contrib/simpletest/simpletest/LICENSE new file mode 100644 index 00000000..09f465ab --- /dev/null +++ b/contrib/simpletest/simpletest/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, 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. + + + Copyright (C) + + 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 Street, 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. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/contrib/simpletest/simpletest/README b/contrib/simpletest/simpletest/README new file mode 100644 index 00000000..c52e3f7e --- /dev/null +++ b/contrib/simpletest/simpletest/README @@ -0,0 +1,108 @@ +SimpleTest +========== +You probably got this package from... +http://simpletest.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+ (and PHP 5.0 to 5.3 without E_STRICT). + +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. YOucan download an Eclipse plug-in separately. + +You are looking at a second 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 often 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 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 SVN only. +The SVN check-ins always have all the tests passing and so SVN 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. + +Marcus Baker, Jason Sweat, Travis Swicegood, Perrick Penet and Edward Z. Yang. +-- +marcus@lastcraft.com diff --git a/contrib/simpletest/simpletest/VERSION b/contrib/simpletest/simpletest/VERSION new file mode 100644 index 00000000..7f207341 --- /dev/null +++ b/contrib/simpletest/simpletest/VERSION @@ -0,0 +1 @@ +1.0.1 \ No newline at end of file diff --git a/contrib/simpletest/simpletest/authentication.php b/contrib/simpletest/simpletest/authentication.php new file mode 100644 index 00000000..c56d11bb --- /dev/null +++ b/contrib/simpletest/simpletest/authentication.php @@ -0,0 +1,238 @@ +_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/contrib/simpletest/simpletest/autorun.php b/contrib/simpletest/simpletest/autorun.php new file mode 100644 index 00000000..7d97d2d7 --- /dev/null +++ b/contrib/simpletest/simpletest/autorun.php @@ -0,0 +1,87 @@ +createSuiteFromClasses( + basename(initial_file()), + $loader->selectRunnableTests($candidates)); + $result = $suite->run(new DefaultReporter()); + if (SimpleReporter::inCli()) { + exit($result ? 0 : 1); + } +} + +/** + * Checks the current test context to see if a test has + * ever been run. + * @return boolean True if tests have run. + */ +function tests_have_run() { + if ($context = SimpleTest::getContext()) { + return (boolean)$context->getTest(); + } + return false; +} + +/** + * The first autorun file. + * @return string Filename of first autorun script. + */ +function initial_file() { + static $file = false; + if (! $file) { + $file = reset(get_included_files()); + } + return $file; +} + +/** + * Just the classes from the first autorun script. May + * get a few false positives, as it just does a regex based + * on following the word "class". + * @return array List of all possible classes in first + * autorun script. + */ +function classes_defined_in_initial_file() { + if (preg_match_all('/\bclass\s+(\w+)/i', file_get_contents(initial_file()), $matches)) { + return array_map('strtolower', $matches[1]); + } + return array(); +} + +/** + * Every class since the first autorun include. This + * is safe enough if require_once() is alwyas used. + * @return array Class names. + */ +function capture_new_classes() { + global $SIMPLETEST_AUTORUNNER_INITIAL_CLASSES; + return array_map('strtolower', array_diff(get_declared_classes(), + $SIMPLETEST_AUTORUNNER_INITIAL_CLASSES ? + $SIMPLETEST_AUTORUNNER_INITIAL_CLASSES : array())); +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/browser.php b/contrib/simpletest/simpletest/browser.php new file mode 100644 index 00000000..e2a1fe1d --- /dev/null +++ b/contrib/simpletest/simpletest/browser.php @@ -0,0 +1,1098 @@ +_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); + return $page->getRaw(); + } + + /** + * 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 base URL of page if set via BASE tag + * @return string base URL + */ + function getBaseUrl() { + $url = $this->_page->getBaseUrl(); + 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 links in current page. + * @return array List of urls with scheme of + * http or https and hostname. + * @access public + */ + function getUrls() { + return $this->_page->getUrls(); + } + + /** + * 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, $position=false) { + return $this->_page->setField(new SimpleByLabelOrName($label), $value, $position); + } + + /** + * 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, $position=false) { + return $this->_page->setField(new SimpleByName($name), $value, $position); + } + + /** + * 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); + } + + /** + * Tests to see if a submit button exists with this + * label. + * @param string $label Button label. + * @return boolean True if present. + * @access public + */ + function isSubmit($label) { + return (boolean)$this->_page->getFormBySubmit(new SimpleByLabel($label)); + } + + /** + * 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); + } + + /** + * Tests to see if an image exists with this + * title or alt text. + * @param string $label Image text. + * @return boolean True if present. + * @access public + */ + function isImage($label) { + return (boolean)$this->_page->getFormByImage(new SimpleByLabel($label)); + } + + /** + * 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); + } + + /** + * Finds a URL by label. Will find 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 URL on success. + * @access public + */ + function getLink($label, $index = 0) { + $urls = $this->_page->getUrlsByLabel($label); + if (count($urls) == 0) { + return false; + } + if (count($urls) < $index + 1) { + return false; + } + return $urls[$index]; + } + + /** + * 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) { + $url = $this->getLink($label, $index); + if ($url === false) { + return false; + } + $this->_load($url, new SimpleGetEncoding()); + return $this->getContent(); + } + + /** + * Finds a link by id attribute. + * @param string $id ID attribute value. + * @return string/boolean URL on success. + * @access public + */ + function getLinkById($id) { + return $this->_page->getUrlById($id); + } + + /** + * 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->getLinkById($id))) { + return false; + } + $this->_load($url, new SimpleGetEncoding()); + return $this->getContent(); + } + + /** + * 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; + } + + /** + * Tests to see if a click target exists. + * @param string $label Visible text or alt text. + * @return boolean True if target present. + * @access public + */ + function isClickable($label) { + return $this->isSubmit($label) || ($this->getLink($label) !== false) || $this->isImage($label); + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/collector.php b/contrib/simpletest/simpletest/collector.php new file mode 100644 index 00000000..5b8255d3 --- /dev/null +++ b/contrib/simpletest/simpletest/collector.php @@ -0,0 +1,122 @@ + + * @package SimpleTest + * @subpackage UnitTester + * @version $Id: collector.php 1723 2008-04-08 00:34:10Z lastcraft $ + */ + +/** + * 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. + * @return string Path without trailing slash. + */ + function _removeTrailingSlash($path) { + if (substr($path, -1) == DIRECTORY_SEPARATOR) { + return substr($path, 0, -1); + } elseif (substr($path, -1) == '/') { + 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) { + if ($this->_isHidden($entry)) { + continue; + } + $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)) { + return; + } + $test->addTestFile($file); + } + + /** + * Tests for hidden files so as to skip them. Currently + * only tests for Unix hidden files. + * @param string $filename Plain filename. + * @return boolean True if hidden file. + * @access private + */ + function _isHidden($filename) { + return strncmp($filename, '.', 1) == 0; + } +} + +/** + * 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/contrib/simpletest/simpletest/compatibility.php b/contrib/simpletest/simpletest/compatibility.php new file mode 100644 index 00000000..4e0f78a4 --- /dev/null +++ b/contrib/simpletest/simpletest/compatibility.php @@ -0,0 +1,173 @@ += 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 (version_compare(phpversion(), '5') >= 0) { + return SimpleTestCompatibility::_isIdenticalType($first, $second); + } + if ($first != $second) { + return false; + } + 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); + } + if ($first !== $second) { + return false; + } + 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 (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; + } + if (function_exists('is_a')) { + return is_a($object, $class); + } + 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); + } + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/cookies.php b/contrib/simpletest/simpletest/cookies.php new file mode 100644 index 00000000..ed1c025d --- /dev/null +++ b/contrib/simpletest/simpletest/cookies.php @@ -0,0 +1,380 @@ +_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/contrib/simpletest/simpletest/default_reporter.php b/contrib/simpletest/simpletest/default_reporter.php new file mode 100644 index 00000000..bd4c6a19 --- /dev/null +++ b/contrib/simpletest/simpletest/default_reporter.php @@ -0,0 +1,133 @@ + '_case', 'c' => '_case', + 'test' => '_test', 't' => '_test', + 'xml' => '_xml', 'x' => '_xml'); + var $_case = ''; + var $_test = ''; + var $_xml = false; + var $_no_skips = false; + + /** + * Parses raw command line arguments into object properties. + * @param string $arguments Raw commend line arguments. + */ + function SimpleCommandLineParser($arguments) { + if (! is_array($arguments)) { + return; + } + foreach ($arguments as $i => $argument) { + if (preg_match('/^--?(test|case|t|c)=(.+)$/', $argument, $matches)) { + $property = $this->_to_property[$matches[1]]; + $this->$property = $matches[2]; + } elseif (preg_match('/^--?(test|case|t|c)$/', $argument, $matches)) { + $property = $this->_to_property[$matches[1]]; + if (isset($arguments[$i + 1])) { + $this->$property = $arguments[$i + 1]; + } + } elseif (preg_match('/^--?(xml|x)$/', $argument)) { + $this->_xml = true; + } elseif (preg_match('/^--?(no-skip|no-skips|s)$/', $argument)) { + $this->_no_skips = true; + } + } + } + + /** + * Run only this test. + * @return string Test name to run. + * @access public + */ + function getTest() { + return $this->_test; + } + + /** + * Run only this test suite. + * @return string Test class name to run. + * @access public + */ + function getTestCase() { + return $this->_case; + } + + /** + * Output should be XML or not. + * @return boolean True if XML desired. + * @access public + */ + function isXml() { + return $this->_xml; + } + + /** + * Output should suppress skip messages. + * @return boolean True for no skips. + * @access public + */ + function noSkips() { + return $this->_no_skips; + } +} + +/** + * The default reporter used by SimpleTest's autorun + * feature. The actual reporters used are dependency + * injected and can be overridden. + * @package SimpleTest + * @subpackage UnitTester + */ +class DefaultReporter extends SimpleReporterDecorator { + + /** + * Assembles the appopriate reporter for the environment. + */ + function DefaultReporter() { + if (SimpleReporter::inCli()) { + global $argv; + $parser = new SimpleCommandLineParser($argv); + $interfaces = $parser->isXml() ? array('XmlReporter') : array('TextReporter'); + $reporter = &new SelectiveReporter( + SimpleTest::preferred($interfaces), + $parser->getTestCase(), + $parser->getTest()); + if ($parser->noSkips()) { + $reporter = &new NoSkipsReporter($reporter); + } + } else { + $reporter = &new SelectiveReporter( + SimpleTest::preferred('HtmlReporter'), + @$_GET['c'], + @$_GET['t']); + if (@$_GET['skips'] == 'no' || @$_GET['show-skips'] == 'no') { + $reporter = &new NoSkipsReporter($reporter); + } + } + $this->SimpleReporterDecorator($reporter); + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/detached.php b/contrib/simpletest/simpletest/detached.php new file mode 100644 index 00000000..e323d8cd --- /dev/null +++ b/contrib/simpletest/simpletest/detached.php @@ -0,0 +1,96 @@ +_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/contrib/simpletest/simpletest/docs.zip b/contrib/simpletest/simpletest/docs.zip new file mode 100644 index 0000000000000000000000000000000000000000..b47802d7e2d790f4fe7549ab950849e753bd0e57 GIT binary patch literal 129563 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≀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

JPF{mz6Rhzz6 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 Z|8GeJ1OFLFxNk52w|Mh8{rNxT{{emahg|>w literal 0 HcmV?d00001 diff --git a/contrib/simpletest/simpletest/dumper.php b/contrib/simpletest/simpletest/dumper.php new file mode 100644 index 00000000..2d75985c --- /dev/null +++ b/contrib/simpletest/simpletest/dumper.php @@ -0,0 +1,360 @@ +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; + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/eclipse.php b/contrib/simpletest/simpletest/eclipse.php new file mode 100644 index 00000000..0f1a4fcb --- /dev/null +++ b/contrib/simpletest/simpletest/eclipse.php @@ -0,0 +1,307 @@ +_listener = &$listener; + $this->SimpleScorer(); + $this->_case = ""; + $this->_group = ""; + $this->_method = ""; + $this->_cc = $cc; + $this->_error = false; + $this->_fail = false; + } + + /** + * Means to display human readable object comparisons. + * @return SimpleDumper Visual comparer. + */ + function getDumper() { + return new SimpleDumper(); + } + + /** + * Localhost connection from Eclipse. + * @param integer $port Port to connect to Eclipse. + * @param string $host Normally localhost. + * @return SimpleSocket Connection to Eclipse. + */ + function &createListener($port, $host="127.0.0.1"){ + $tmplistener = &new SimpleSocket($host, $port, 5); + return $tmplistener; + } + + /** + * Wraps the test in an output buffer. + * @param SimpleInvoker $invoker Current test runner. + * @return EclipseInvoker Decorator with output buffering. + * @access public + */ + function &createInvoker(&$invoker){ + $eclinvoker = &new EclipseInvoker($invoker, $this->_listener); + return $eclinvoker; + } + + /** + * C style escaping. + * @param string $raw String with backslashes, quotes and whitespace. + * @return string Replaced with C backslashed tokens. + */ + function escapeVal($raw){ + $needle = array("\\","\"","/","\b","\f","\n","\r","\t"); + $replace = array('\\\\','\"','\/','\b','\f','\n','\r','\t'); + return str_replace($needle, $replace, $raw); + } + + /** + * Stash the first passing item. Clicking the test + * item goes to first pass. + * @param string $message Test message, but we only wnat the first. + * @access public + */ + function paintPass($message){ + if (! $this->_pass){ + $this->_message = $this->escapeVal($message); + } + $this->_pass = true; + } + + /** + * Stash the first failing item. Clicking the test + * item goes to first fail. + * @param string $message Test message, but we only wnat the first. + * @access public + */ + function paintFail($message){ + //only get the first failure or error + if (! $this->_fail && ! $this->_error){ + $this->_fail = true; + $this->_message = $this->escapeVal($message); + $this->_listener->write('{status:"fail",message:"'.$this->_message.'",group:"'.$this->_group.'",case:"'.$this->_case.'",method:"'.$this->_method.'"}'); + } + } + + /** + * Stash the first error. Clicking the test + * item goes to first error. + * @param string $message Test message, but we only wnat the first. + * @access public + */ + function paintError($message){ + if (! $this->_fail && ! $this->_error){ + $this->_error = true; + $this->_message = $this->escapeVal($message); + $this->_listener->write('{status:"error",message:"'.$this->_message.'",group:"'.$this->_group.'",case:"'.$this->_case.'",method:"'.$this->_method.'"}'); + } + } + + + /** + * Stash the first exception. Clicking the test + * item goes to first message. + * @param string $message Test message, but we only wnat the first. + * @access public + */ + function paintException($exception){ + if (! $this->_fail && ! $this->_error){ + $this->_error = true; + $message = 'Unexpected exception of type[' . get_class($exception) . + '] with message [' . $exception->getMessage() . '] in [' . + $exception->getFile() .' line '. $exception->getLine() . ']'; + $this->_message = $this->escapeVal($message); + $this->_listener->write( + '{status:"error",message:"' . $this->_message . '",group:"' . + $this->_group . '",case:"' . $this->_case . '",method:"' . $this->_method + . '"}'); + } + } + + + /** + * We don't display any special header. + * @param string $test_name First test top level + * to start. + * @access public + */ + function paintHeader($test_name) { + } + + /** + * We don't display any special footer. + * @param string $test_name The top level test. + * @access public + */ + function paintFooter($test_name) { + } + + /** + * Paints nothing at the start of a test method, but stash + * the method name for later. + * @param string $test_name Name of test that is starting. + * @access public + */ + function paintMethodStart($method) { + $this->_pass = false; + $this->_fail = false; + $this->_error = false; + $this->_method = $this->escapeVal($method); + } + + /** + * Only send one message if the test passes, after that + * suppress the message. + * @param string $test_name Name of test that is ending. + * @access public + */ + function paintMethodEnd($method){ + if ($this->_fail || $this->_error || ! $this->_pass){ + } else { + $this->_listener->write( + '{status:"pass",message:"' . $this->_message . '",group:"' . + $this->_group . '",case:"' . $this->_case . '",method:"' . + $this->_method . '"}'); + } + } + + /** + * Stashes the test case name for the later failure message. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintCaseStart($case){ + $this->_case = $this->escapeVal($case); + } + + /** + * Drops the name. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintCaseEnd($case){ + $this->_case = ""; + } + + /** + * Stashes the name of the test suite. Starts test coverage + * if enabled. + * @param string $group Name of test or other label. + * @param integer $size Number of test cases starting. + * @access public + */ + function paintGroupStart($group, $size){ + $this->_group = $this->escapeVal($group); + if ($this->_cc){ + if (extension_loaded('xdebug')){ + xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); + } + } + } + + /** + * Paints coverage report if enabled. + * @param string $group Name of test or other label. + * @access public + */ + function paintGroupEnd($group){ + $this->_group = ""; + $cc = ""; + if ($this->_cc){ + if (extension_loaded('xdebug')){ + $arrfiles = xdebug_get_code_coverage(); + xdebug_stop_code_coverage(); + $thisdir = dirname(__FILE__); + $thisdirlen = strlen($thisdir); + foreach ($arrfiles as $index=>$file){ + if (substr($index, 0, $thisdirlen)===$thisdir){ + continue; + } + $lcnt = 0; + $ccnt = 0; + foreach ($file as $line){ + if ($line == -2){ + continue; + } + $lcnt++; + if ($line==1){ + $ccnt++; + } + } + if ($lcnt > 0){ + $cc .= round(($ccnt/$lcnt) * 100, 2) . '%'; + }else{ + $cc .= "0.00%"; + } + $cc.= "\t". $index . "\n"; + } + } + } + $this->_listener->write('{status:"coverage",message:"' . + EclipseReporter::escapeVal($cc) . '"}'); + } +} + +/** + * Invoker decorator for Eclipse. Captures output until + * the end of the test. + * @package SimpleTest + * @subpackage Eclipse + */ +class EclipseInvoker extends SimpleInvokerDecorator{ + function EclipseInvoker(&$invoker, &$listener) { + $this->_listener = &$listener; + $this->SimpleInvokerDecorator($invoker); + } + + /** + * Starts output buffering. + * @param string $method Test method to call. + * @access public + */ + function before($method){ + ob_start(); + $this->_invoker->before($method); + } + + /** + * Stops output buffering and send the captured output + * to the listener. + * @param string $method Test method to call. + * @access public + */ + function after($method) { + $this->_invoker->after($method); + $output = ob_get_contents(); + ob_end_clean(); + if ($output !== ""){ + $result = $this->_listener->write('{status:"info",message:"' . + EclipseReporter::escapeVal($output) . '"}'); + } + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/encoding.php b/contrib/simpletest/simpletest/encoding.php new file mode 100644 index 00000000..112fe330 --- /dev/null +++ b/contrib/simpletest/simpletest/encoding.php @@ -0,0 +1,552 @@ +_key = $key; + $this->_value = $value; + } + + /** + * The pair as a single string. + * @return string Encoded pair. + * @access public + */ + function asRequest() { + return urlencode($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) { + if (is_array($query) and $this->hasMoreThanOneLevel($query)) { + $query = $this->rewriteArrayWithMultipleLevels($query); + } + $this->SimpleEncoding($query); + } + + function hasMoreThanOneLevel($query) { + foreach ($query as $key => $value) { + if (is_array($value)) { + return true; + } + } + return false; + } + + function rewriteArrayWithMultipleLevels($query) { + $query_ = array(); + foreach ($query as $key => $value) { + if (is_array($value)) { + foreach ($value as $sub_key => $sub_value) { + $query_[$key."[".$sub_key."]"] = $sub_value; + } + } else { + $query_[$key] = $value; + } + } + if ($this->hasMoreThanOneLevel($query_)) { + $query_ = $this->rewriteArrayWithMultipleLevels($query_); + } + + return $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/contrib/simpletest/simpletest/errors.php b/contrib/simpletest/simpletest/errors.php new file mode 100644 index 00000000..5a078851 --- /dev/null +++ b/contrib/simpletest/simpletest/errors.php @@ -0,0 +1,288 @@ +SimpleInvokerDecorator($invoker); + } + + /** + * Invokes a test method and dispatches any + * untrapped errors. Called back from + * the visiting runner. + * @param string $method Test method to call. + * @access public + */ + function invoke($method) { + $queue = &$this->_createErrorQueue(); + set_error_handler('SimpleTestErrorHandler'); + parent::invoke($method); + restore_error_handler(); + $queue->tally(); + } + + /** + * Wires up the error queue for a single test. + * @return SimpleErrorQueue Queue connected to the test. + * @access private + */ + function &_createErrorQueue() { + $context = &SimpleTest::getContext(); + $test = &$this->getTestCase(); + $queue = &$context->get('SimpleErrorQueue'); + $queue->setTestCase($test); + return $queue; + } +} + +/** + * Error queue used to record trapped + * errors. + * @package SimpleTest + * @subpackage UnitTester + */ +class SimpleErrorQueue { + var $_queue; + var $_expectation_queue; + var $_test; + var $_using_expect_style = false; + + /** + * Starts with an empty queue. + */ + function SimpleErrorQueue() { + $this->clear(); + } + + /** + * Discards the contents of the error queue. + * @access public + */ + function clear() { + $this->_queue = array(); + $this->_expectation_queue = array(); + } + + /** + * Sets the currently running test case. + * @param SimpleTestCase $test Test case to send messages to. + * @access public + */ + function setTestCase(&$test) { + $this->_test = &$test; + } + + /** + * Sets up an expectation of an error. If this is + * not fulfilled at the end of the test, a failure + * will occour. If the error does happen, then this + * will cancel it out and send a pass message. + * @param SimpleExpectation $expected Expected error match. + * @param string $message Message to display. + * @access public + */ + function expectError($expected, $message) { + $this->_using_expect_style = true; + array_push($this->_expectation_queue, array($expected, $message)); + } + + /** + * Adds an error to the front of the queue. + * @param integer $severity PHP error code. + * @param string $content Text of error. + * @param string $filename File error occoured in. + * @param integer $line Line number of error. + * @access public + */ + function add($severity, $content, $filename, $line) { + $content = str_replace('%', '%%', $content); + if ($this->_using_expect_style) { + $this->_testLatestError($severity, $content, $filename, $line); + } else { + array_push( + $this->_queue, + array($severity, $content, $filename, $line)); + } + } + + /** + * Any errors still in the queue are sent to the test + * case. Any unfulfilled expectations trigger failures. + * @access public + */ + function tally() { + while (list($severity, $message, $file, $line) = $this->extract()) { + $severity = $this->getSeverityAsString($severity); + $this->_test->error($severity, $message, $file, $line); + } + while (list($expected, $message) = $this->_extractExpectation()) { + $this->_test->assert($expected, false, "%s -> Expected error not caught"); + } + } + + /** + * Tests the error against the most recent expected + * error. + * @param integer $severity PHP error code. + * @param string $content Text of error. + * @param string $filename File error occoured in. + * @param integer $line Line number of error. + * @access private + */ + function _testLatestError($severity, $content, $filename, $line) { + if ($expectation = $this->_extractExpectation()) { + list($expected, $message) = $expectation; + $this->_test->assert($expected, $content, sprintf( + $message, + "%s -> PHP error [$content] severity [" . + $this->getSeverityAsString($severity) . + "] in [$filename] line [$line]")); + } else { + $this->_test->error($severity, $content, $filename, $line); + } + } + + /** + * Pulls the earliest error from the queue. + * @return mixed False if none, or a list of error + * information. Elements are: severity + * as the PHP error code, the error message, + * the file with the error, the line number + * and a list of PHP super global arrays. + * @access public + */ + function extract() { + if (count($this->_queue)) { + return array_shift($this->_queue); + } + return false; + } + + /** + * Pulls the earliest expectation from the queue. + * @return SimpleExpectation False if none. + * @access private + */ + function _extractExpectation() { + if (count($this->_expectation_queue)) { + return array_shift($this->_expectation_queue); + } + return false; + } + + /** + * @deprecated + */ + function assertNoErrors($message) { + return $this->_test->assert( + new TrueExpectation(), + count($this->_queue) == 0, + sprintf($message, 'Should be no errors')); + } + + /** + * @deprecated + */ + function assertError($expected, $message) { + if (count($this->_queue) == 0) { + $this->_test->fail(sprintf($message, 'Expected error not found')); + return false; + } + list($severity, $content, $file, $line) = $this->extract(); + $severity = $this->getSeverityAsString($severity); + return $this->_test->assert( + $expected, + $content, + sprintf($message, "Expected PHP error [$content] severity [$severity] in [$file] line [$line]")); + } + + /** + * Converts an error code into it's string + * representation. + * @param $severity PHP integer error code. + * @return String version of error code. + * @access public + * @static + */ + function getSeverityAsString($severity) { + static $map = array( + E_STRICT => 'E_STRICT', + E_ERROR => 'E_ERROR', + E_WARNING => 'E_WARNING', + E_PARSE => 'E_PARSE', + E_NOTICE => 'E_NOTICE', + E_CORE_ERROR => 'E_CORE_ERROR', + E_CORE_WARNING => 'E_CORE_WARNING', + E_COMPILE_ERROR => 'E_COMPILE_ERROR', + E_COMPILE_WARNING => 'E_COMPILE_WARNING', + E_USER_ERROR => 'E_USER_ERROR', + E_USER_WARNING => 'E_USER_WARNING', + E_USER_NOTICE => 'E_USER_NOTICE'); + if (defined('E_RECOVERABLE_ERROR')) { + $map[E_RECOVERABLE_ERROR] = 'E_RECOVERABLE_ERROR'; + } + if (defined('E_DEPRECATED')) { + $map[E_DEPRECATED] = 'E_DEPRECATED'; + } + return $map[$severity]; + } +} + +/** + * Error handler that simply stashes any errors into the global + * error queue. Simulates the existing behaviour with respect to + * logging errors, but this feature may be removed in future. + * @param $severity PHP error code. + * @param $message Text of error. + * @param $filename File error occoured in. + * @param $line Line number of error. + * @param $super_globals Hash of PHP super global arrays. + * @static + * @access public + */ +function SimpleTestErrorHandler($severity, $message, $filename = null, $line = null, $super_globals = null, $mask = null) { + $severity = $severity & error_reporting(); + if ($severity) { + restore_error_handler(); + if (ini_get('log_errors')) { + $label = SimpleErrorQueue::getSeverityAsString($severity); + error_log("$label: $message in $filename on line $line"); + } + $context = &SimpleTest::getContext(); + $queue = &$context->get('SimpleErrorQueue'); + $queue->add($severity, $message, $filename, $line); + set_error_handler('SimpleTestErrorHandler'); + } + return true; +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/exceptions.php b/contrib/simpletest/simpletest/exceptions.php new file mode 100644 index 00000000..c19a04e3 --- /dev/null +++ b/contrib/simpletest/simpletest/exceptions.php @@ -0,0 +1,198 @@ +SimpleInvokerDecorator($invoker); + } + + /** + * Invokes a test method whilst trapping expected + * exceptions. Any left over unthrown exceptions + * are then reported as failures. + * @param string $method Test method to call. + */ + function invoke($method) { + $trap = SimpleTest::getContext()->get('SimpleExceptionTrap'); + $trap->clear(); + try { + $has_thrown = false; + parent::invoke($method); + } catch (Exception $exception) { + $has_thrown = true; + if (! $trap->isExpected($this->getTestCase(), $exception)) { + $this->getTestCase()->exception($exception); + } + $trap->clear(); + } + if ($message = $trap->getOutstanding()) { + $this->getTestCase()->fail($message); + } + if ($has_thrown) { + try { + parent::getTestCase()->tearDown(); + } catch (Exception $e) { } + } + } +} + +/** + * Tests exceptions either by type or the exact + * exception. This could be improved to accept + * a pattern expectation to test the error + * message, but that will have to come later. + * @package SimpleTest + * @subpackage UnitTester + */ +class ExceptionExpectation extends SimpleExpectation { + private $expected; + + /** + * Sets up the conditions to test against. + * If the expected value is a string, then + * it will act as a test of the class name. + * An exception as the comparison will + * trigger an identical match. Writing this + * down now makes it look doubly dumb. I hope + * come up with a better scheme later. + * @param mixed $expected A class name or an actual + * exception to compare with. + * @param string $message Message to display. + */ + function __construct($expected, $message = '%s') { + $this->expected = $expected; + parent::__construct($message); + } + + /** + * Carry out the test. + * @param Exception $compare Value to check. + * @return boolean True if matched. + */ + function test($compare) { + if (is_string($this->expected)) { + return ($compare instanceof $this->expected); + } + if (get_class($compare) != get_class($this->expected)) { + return false; + } + return $compare->getMessage() == $this->expected->getMessage(); + } + + /** + * Create the message to display describing the test. + * @param Exception $compare Exception to match. + * @return string Final message. + */ + function testMessage($compare) { + if (is_string($this->expected)) { + return "Exception [" . $this->describeException($compare) . + "] should be type [" . $this->expected . "]"; + } + return "Exception [" . $this->describeException($compare) . + "] should match [" . + $this->describeException($this->expected) . "]"; + } + + /** + * Summary of an Exception object. + * @param Exception $compare Exception to describe. + * @return string Text description. + */ + protected function describeException($exception) { + return get_class($exception) . ": " . $exception->getMessage(); + } +} + +/** + * Stores expected exceptions for when they + * get thrown. Saves the irritating try...catch + * block. + * @package SimpleTest + * @subpackage UnitTester + */ +class SimpleExceptionTrap { + private $expected; + private $message; + + /** + * Clears down the queue ready for action. + */ + function __construct() { + $this->clear(); + } + + /** + * Sets up an expectation of an exception. + * This has the effect of intercepting an + * exception that matches. + * @param SimpleExpectation $expected Expected exception to match. + * @param string $message Message to display. + * @access public + */ + function expectException($expected = false, $message = '%s') { + if ($expected === false) { + $expected = new AnythingExpectation(); + } + if (! SimpleExpectation::isExpectation($expected)) { + $expected = new ExceptionExpectation($expected); + } + $this->expected = $expected; + $this->message = $message; + } + + /** + * Compares the expected exception with any + * in the queue. Issues a pass or fail and + * returns the state of the test. + * @param SimpleTestCase $test Test case to send messages to. + * @param Exception $exception Exception to compare. + * @return boolean False on no match. + */ + function isExpected($test, $exception) { + if ($this->expected) { + return $test->assert($this->expected, $exception, $this->message); + } + return false; + } + + /** + * Tests for any left over exception. + * @return string/false The failure message or false if none. + */ + function getOutstanding() { + return sprintf($this->message, 'Failed to trap exception'); + } + + /** + * Discards the contents of the error queue. + */ + function clear() { + $this->expected = false; + $this->message = false; + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/expectation.php b/contrib/simpletest/simpletest/expectation.php new file mode 100644 index 00000000..194afd5c --- /dev/null +++ b/contrib/simpletest/simpletest/expectation.php @@ -0,0 +1,895 @@ +_message = $message; + } + + /** + * Tests the expectation. True if correct. + * @param mixed $compare Comparison value. + * @return boolean True if correct. + * @access public + * @abstract + */ + function test($compare) { + } + + /** + * Returns a human readable test message. + * @param mixed $compare Comparison value. + * @return string Description of success + * or failure. + * @access public + * @abstract + */ + function testMessage($compare) { + } + + /** + * Overlays the generated message onto the stored user + * message. An additional message can be interjected. + * @param mixed $compare Comparison value. + * @param SimpleDumper $dumper For formatting the results. + * @return string Description of success + * or failure. + * @access public + */ + function overlayMessage($compare, $dumper) { + $this->_dumper = $dumper; + return sprintf($this->_message, $this->testMessage($compare)); + } + + /** + * Accessor for the dumper. + * @return SimpleDumper Current value dumper. + * @access protected + */ + function &_getDumper() { + if (! $this->_dumper) { + $dumper = &new SimpleDumper(); + return $dumper; + } + return $this->_dumper; + } + + /** + * Test to see if a value is an expectation object. + * A useful utility method. + * @param mixed $expectation Hopefully an Epectation + * class. + * @return boolean True if descended from + * this class. + * @access public + * @static + */ + function isExpectation($expectation) { + return is_object($expectation) && + SimpleTestCompatibility::isA($expectation, 'SimpleExpectation'); + } +} + +/** + * A wildcard expectation always matches. + * @package SimpleTest + * @subpackage MockObjects + */ +class AnythingExpectation extends SimpleExpectation { + + /** + * Tests the expectation. Always true. + * @param mixed $compare Ignored. + * @return boolean True. + * @access public + */ + function test($compare) { + return true; + } + + /** + * Returns a human readable test message. + * @param mixed $compare Comparison value. + * @return string Description of success + * or failure. + * @access public + */ + function testMessage($compare) { + $dumper = &$this->_getDumper(); + return 'Anything always matches [' . $dumper->describeValue($compare) . ']'; + } +} + +/** + * An expectation that never matches. + * @package SimpleTest + * @subpackage MockObjects + */ +class FailedExpectation extends SimpleExpectation { + + /** + * Tests the expectation. Always false. + * @param mixed $compare Ignored. + * @return boolean True. + * @access public + */ + function test($compare) { + return false; + } + + /** + * Returns a human readable test message. + * @param mixed $compare Comparison value. + * @return string Description of failure. + * @access public + */ + function testMessage($compare) { + $dumper = &$this->_getDumper(); + return 'Failed expectation never matches [' . $dumper->describeValue($compare) . ']'; + } +} + +/** + * An expectation that passes on boolean true. + * @package SimpleTest + * @subpackage MockObjects + */ +class TrueExpectation extends SimpleExpectation { + + /** + * Tests the expectation. + * @param mixed $compare Should be true. + * @return boolean True on match. + * @access public + */ + function test($compare) { + return (boolean)$compare; + } + + /** + * Returns a human readable test message. + * @param mixed $compare Comparison value. + * @return string Description of success + * or failure. + * @access public + */ + function testMessage($compare) { + $dumper = &$this->_getDumper(); + return 'Expected true, got [' . $dumper->describeValue($compare) . ']'; + } +} + +/** + * An expectation that passes on boolean false. + * @package SimpleTest + * @subpackage MockObjects + */ +class FalseExpectation extends SimpleExpectation { + + /** + * Tests the expectation. + * @param mixed $compare Should be false. + * @return boolean True on match. + * @access public + */ + function test($compare) { + return ! (boolean)$compare; + } + + /** + * Returns a human readable test message. + * @param mixed $compare Comparison value. + * @return string Description of success + * or failure. + * @access public + */ + function testMessage($compare) { + $dumper = &$this->_getDumper(); + return 'Expected false, got [' . $dumper->describeValue($compare) . ']'; + } +} + +/** + * 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 reference. + * @package SimpleTest + * @subpackage UnitTester + */ +class ReferenceExpectation extends SimpleExpectation { + var $_value; + + /** + * Sets the reference value to compare against. + * @param mixed $value Test reference to match. + * @param string $message Customised message on failure. + * @access public + */ + function ReferenceExpectation(&$value, $message = '%s') { + $this->SimpleExpectation($message); + $this->_value =& $value; + } + + /** + * Tests the expectation. True if it exactly + * references the held value. + * @param mixed $compare Comparison reference. + * @return boolean True if correct. + * @access public + */ + function test(&$compare) { + return SimpleTestCompatibility::isReference($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) { + if ($this->test($compare)) { + return "Reference expectation [" . $this->_dumper->describeValue($this->_value) . "]"; + } else { + return "Reference expectation fails " . + $this->_dumper->describeDifference($this->_value, $compare); + } + } + + function _getValue() { + return $this->_value; + } +} + +/** + * 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. + * @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) . "]"; + } +} + +/** + * @package SimpleTest + * @subpackage UnitTester + * @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]"; + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/extensions/pear_test_case.php b/contrib/simpletest/simpletest/extensions/pear_test_case.php new file mode 100644 index 00000000..f5e5a7b8 --- /dev/null +++ b/contrib/simpletest/simpletest/extensions/pear_test_case.php @@ -0,0 +1,198 @@ +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::assert(new TrueExpectation(), 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::assert(new TrueExpectation(), !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->assert( + new TrueExpectation(), + 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->assert( + new falseExpectation(), + 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::assert(new TrueExpectation(), $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::assert(new FalseExpectation(), $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::assert(new TrueExpectation(), 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/contrib/simpletest/simpletest/extensions/phpunit_test_case.php b/contrib/simpletest/simpletest/extensions/phpunit_test_case.php new file mode 100644 index 00000000..e038a496 --- /dev/null +++ b/contrib/simpletest/simpletest/extensions/phpunit_test_case.php @@ -0,0 +1,96 @@ +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::assert(new TrueExpectation(), $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/contrib/simpletest/simpletest/extensions/testdox.php b/contrib/simpletest/simpletest/extensions/testdox.php new file mode 100644 index 00000000..7db7c872 --- /dev/null +++ b/contrib/simpletest/simpletest/extensions/testdox.php @@ -0,0 +1,42 @@ +_test_case_pattern = empty($test_case_pattern) ? '/^(.*)$/' : $test_case_pattern; + } + + function paintCaseStart($test_name) { + preg_match($this->_test_case_pattern, $test_name, $matches); + if (!empty($matches[1])) { + echo $matches[1] . "\n"; + } else { + echo $test_name . "\n"; + } + } + + function paintCaseEnd() { + echo "\n"; + } + + function paintMethodStart($test_name) { + if (!preg_match('/^test(.*)$/i', $test_name, $matches)) { + return; + } + $test_name = $matches[1]; + + $test_name = preg_replace('/([A-Z])([A-Z])/', '$1 $2', $test_name); + echo '- ' . strtolower(preg_replace('/([a-zA-Z])([A-Z0-9])/', '$1 $2', $test_name)); + } + + function paintMethodEnd() { + echo "\n"; + } + + function paintFail() { + echo " [FAILED]"; + } +} diff --git a/contrib/simpletest/simpletest/extensions/testdox/test.php b/contrib/simpletest/simpletest/extensions/testdox/test.php new file mode 100644 index 00000000..82c5b7da --- /dev/null +++ b/contrib/simpletest/simpletest/extensions/testdox/test.php @@ -0,0 +1,108 @@ +assertIsA($dox, 'SimpleScorer'); + $this->assertIsA($dox, 'SimpleReporter'); + } + + function testOutputsNameOfTestCase() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintCaseStart('TestOfTestDoxReporter'); + $buffer = ob_get_clean(); + $this->assertWantedPattern('/^TestDoxReporter/', $buffer); + } + + function testOutputOfTestCaseNameFilteredByConstructParameter() { + $dox = new TestDoxReporter('/^(.*)Test$/'); + ob_start(); + $dox->paintCaseStart('SomeGreatWidgetTest'); + $buffer = ob_get_clean(); + $this->assertWantedPattern('/^SomeGreatWidget/', $buffer); + } + + function testIfTest_case_patternIsEmptyAssumeEverythingMatches() { + $dox = new TestDoxReporter(''); + ob_start(); + $dox->paintCaseStart('TestOfTestDoxReporter'); + $buffer = ob_get_clean(); + $this->assertWantedPattern('/^TestOfTestDoxReporter/', $buffer); + } + + function testEmptyLineInsertedWhenCaseEnds() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintCaseEnd('TestOfTestDoxReporter'); + $buffer = ob_get_clean(); + $this->assertEqual("\n", $buffer); + } + + function testPaintsTestMethodInTestDoxFormat() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintMethodStart('testSomeGreatTestCase'); + $buffer = ob_get_clean(); + $this->assertEqual("- some great test case", $buffer); + unset($buffer); + + $random = rand(100, 200); + ob_start(); + $dox->paintMethodStart("testRandomNumberIs{$random}"); + $buffer = ob_get_clean(); + $this->assertEqual("- random number is {$random}", $buffer); + } + + function testDoesNotOutputAnythingOnNoneTestMethods() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintMethodStart('nonMatchingMethod'); + $buffer = ob_get_clean(); + $this->assertEqual('', $buffer); + } + + function testPaintMethodAddLineBreak() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintMethodEnd('someMethod'); + $buffer = ob_get_clean(); + $this->assertEqual("\n", $buffer); + $this->assertNoErrors(); + } + + function testProperlySpacesSingleLettersInMethodName() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintMethodStart('testAVerySimpleAgainAVerySimpleMethod'); + $buffer = ob_get_clean(); + $this->assertEqual('- a very simple again a very simple method', $buffer); + } + + function testOnFailureThisPrintsFailureNotice() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintFail(); + $buffer = ob_get_clean(); + $this->assertEqual(' [FAILED]', $buffer); + } + + function testWhenMatchingMethodNamesTestPrefixIsCaseInsensitive() { + $dox = new TestDoxReporter(); + ob_start(); + $dox->paintMethodStart('TESTSupportsAllUppercaseTestPrefixEvenThoughIDoNotKnowWhyYouWouldDoThat'); + $buffer = ob_get_clean(); + $this->assertEqual( + '- supports all uppercase test prefix even though i do not know why you would do that', + $buffer + ); + } +} + diff --git a/contrib/simpletest/simpletest/form.php b/contrib/simpletest/simpletest/form.php new file mode 100644 index 00000000..cbef6636 --- /dev/null +++ b/contrib/simpletest/simpletest/form.php @@ -0,0 +1,355 @@ +_method = $tag->getAttribute('method'); + $this->_action = $this->_createAction($tag->getAttribute('action'), $page); + $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, &$page) { + if (($action === '') || ($action === false)) { + return $page->expandUrl($page->getUrl()); + } + return $page->expandUrl(new SimpleUrl($action));; + } + + /** + * 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, $position=false) { + $success = false; + $_position = 0; + for ($i = 0, $count = count($this->_widgets); $i < $count; $i++) { + if ($selector->isMatch($this->_widgets[$i])) { + $_position++; + if ($position === false or $_position === (int)$position) { + 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(); + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/frames.php b/contrib/simpletest/simpletest/frames.php new file mode 100644 index 00000000..ec098df9 --- /dev/null +++ b/contrib/simpletest/simpletest/frames.php @@ -0,0 +1,596 @@ +_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; + } + + /** + * Page base URL. + * @return SimpleUrl Current url. + * @access public + */ + function getBaseUrl() { + if (is_integer($this->_focus)) { + $url = $this->_frames[$this->_focus]->getBaseUrl(); + } else { + $url = $this->_frameset->getBaseUrl(); + } + return $url; + } + + /** + * Expands expandomatic URLs into fully qualified + * URLs for the frameset page. + * @param SimpleUrl $url Relative URL. + * @return SimpleUrl Absolute URL. + * @access public + */ + function expandUrl($url) { + return $this->_frameset->expandUrl($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 as strings. + * @access public + */ + function getUrls() { + if (is_integer($this->_focus)) { + return $this->_frames[$this->_focus]->getUrls(); + } + $urls = array(); + foreach ($this->_frames as $frame) { + $urls = array_merge($urls, $frame->getUrls()); + } + 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/contrib/simpletest/simpletest/http.php b/contrib/simpletest/simpletest/http.php new file mode 100644 index 00000000..e6c6e89d --- /dev/null +++ b/contrib/simpletest/simpletest/http.php @@ -0,0 +1,624 @@ +_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/contrib/simpletest/simpletest/invoker.php b/contrib/simpletest/simpletest/invoker.php new file mode 100644 index 00000000..e2730013 --- /dev/null +++ b/contrib/simpletest/simpletest/invoker.php @@ -0,0 +1,139 @@ +_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); + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/mock_objects.php b/contrib/simpletest/simpletest/mock_objects.php new file mode 100644 index 00000000..5ef15751 --- /dev/null +++ b/contrib/simpletest/simpletest/mock_objects.php @@ -0,0 +1,1581 @@ += 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', '*'); +} + +/** + * 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 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], $this->_getDumper()) . "]"; + } + } + 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. + * @package SimpleTest + * @subpackage MockObjects + */ +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. + * @package SimpleTest + * @subpackage MockObjects + */ +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. + * @package SimpleTest + * @subpackage MockObjects + */ +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 method actions by searching the + * parameter lists until an expected match is found. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleSignatureMap { + var $_map; + + /** + * Creates an empty call map. + * @access public + */ + function SimpleSignatureMap() { + $this->_map = array(); + } + + /** + * Stashes a reference against a method call. + * @param array $parameters Array of arguments (including wildcards). + * @param mixed $action Reference placed in the map. + * @access public + */ + function add($parameters, &$action) { + $place = count($this->_map); + $this->_map[$place] = array(); + $this->_map[$place]['params'] = new ParametersExpectation($parameters); + $this->_map[$place]['content'] = &$action; + } + + /** + * 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 &findFirstAction($parameters) { + $slot = $this->_findFirstSlot($parameters); + if (isset($slot) && isset($slot['content'])) { + return $slot['content']; + } + $null = null; + return $null; + } + + /** + * 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); + } + + /** + * Compares the incoming parameters with the + * internal expectation. Uses the incoming $test + * to dispatch the test message. + * @param SimpleTestCase $test Test to dispatch to. + * @param array $parameters The actual calling arguments. + * @param string $message The message to overlay. + * @access public + */ + function test(&$test, $parameters, $message) { + } + + /** + * 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; + } +} + +/** + * Allows setting of actions against call signatures either + * at a specific time, or always. Specific time settings + * trump lasting ones, otherwise the most recently added + * will mask an earlier match. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleCallSchedule { + var $_wildcard = MOCK_ANYTHING; + var $_always; + var $_at; + + /** + * Sets up an empty response schedule. + * Creates an empty call map. + */ + function SimpleCallSchedule() { + $this->_always = array(); + $this->_at = array(); + } + + /** + * Stores an action against a signature that + * will always fire unless masked by a time + * specific one. + * @param string $method Method name. + * @param array $args Calling parameters. + * @param SimpleAction $action Actually simpleByValue, etc. + * @access public + */ + function register($method, $args, &$action) { + $args = $this->_replaceWildcards($args); + $method = strtolower($method); + if (! isset($this->_always[$method])) { + $this->_always[$method] = new SimpleSignatureMap(); + } + $this->_always[$method]->add($args, $action); + } + + /** + * Stores an action against a signature that + * will fire at a specific time in the future. + * @param integer $step delay of calls to this method, + * 0 is next. + * @param string $method Method name. + * @param array $args Calling parameters. + * @param SimpleAction $action Actually SimpleByValue, etc. + * @access public + */ + function registerAt($step, $method, $args, &$action) { + $args = $this->_replaceWildcards($args); + $method = strtolower($method); + if (! isset($this->_at[$method])) { + $this->_at[$method] = array(); + } + if (! isset($this->_at[$method][$step])) { + $this->_at[$method][$step] = new SimpleSignatureMap(); + } + $this->_at[$method][$step]->add($args, $action); + } + + function expectArguments($method, $args, $message) { + $args = $this->_replaceWildcards($args); + $message .= Mock::getExpectationLine(); + $this->_expected_args[strtolower($method)] = + new ParametersExpectation($args, $message); + + } + + /** + * Actually carry out the action stored previously, + * if the parameters match. + * @param integer $step Time of call. + * @param string $method Method name. + * @param array $args The parameters making up the + * rest of the call. + * @return mixed The result of the action. + * @access public. + */ + function &respond($step, $method, $args) { + $method = strtolower($method); + if (isset($this->_at[$method][$step])) { + if ($this->_at[$method][$step]->isMatch($args)) { + $action = &$this->_at[$method][$step]->findFirstAction($args); + if (isset($action)) { + return $action->act(); + } + } + } + if (isset($this->_always[$method])) { + $action = &$this->_always[$method]->findFirstAction($args); + if (isset($action)) { + return $action->act(); + } + } + $null = null; + return $null; + } + + /** + * 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; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes a reference for returning later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleByReference { + var $_reference; + + /** + * Stashes it for later. + * @param mixed $reference Actual PHP4 style reference. + * @access public + */ + function SimpleByReference(&$reference) { + $this->_reference = &$reference; + } + + /** + * Returns the reference stored earlier. + * @return mixed Whatever was stashed. + * @access public + */ + function &act() { + return $this->_reference; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes a value for returning later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleByValue { + var $_value; + + /** + * Stashes it for later. + * @param mixed $value You need to clone objects + * if you want copy semantics + * for these. + * @access public + */ + function SimpleByValue($value) { + $this->_value = $value; + } + + /** + * Returns the value stored earlier. + * @return mixed Whatever was stashed. + * @access public + */ + function &act() { + $dummy = $this->_value; + return $dummy; + } +} + +/** + * A type of SimpleMethodAction. + * Stashes an exception for throwing later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleThrower { + var $_exception; + + /** + * Stashes it for later. + * @param Exception $exception The exception object to throw. + * @access public + */ + function SimpleThrower($exception) { + $this->_exception = $exception; + } + + /** + * Throws the exceptins stashed earlier. + * @access public + */ + function act() { + eval('throw $this->_exception;'); + } +} + +/** + * A type of SimpleMethodAction. + * Stashes an error for emitting later. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleErrorThrower { + var $_error; + var $_severity; + + /** + * Stashes an error to throw later. + * @param string $error Error message. + * @param integer $severity PHP error constant, e.g E_USER_ERROR. + * @access public + */ + function SimpleErrorThrower($error, $severity) { + $this->_error = $error; + $this->_severity = $severity; + } + + /** + * Triggers the stashed error. + * @return null The usual PHP4.4 shenanigans are needed here. + * @access public + */ + function &act() { + trigger_error($this->_error, $this->_severity); + $null = null; + return $null; + } +} + +/** + * A base class or delegate that extends 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 or returning + * preprogrammed responses. + * @package SimpleTest + * @subpackage MockObjects + */ +class SimpleMock { + var $_actions; + var $_wildcard = MOCK_ANYTHING; + var $_is_strict = true; + var $_call_counts; + var $_expected_counts; + var $_max_counts; + var $_expected_args; + var $_expected_args_at; + + /** + * Creates an empty action list and expectation list. + * All call counts are set to zero. + * @access public + */ + function SimpleMock() { + $this->_actions = &new SimpleCallSchedule(); + $this->_expectations = &new SimpleCallSchedule(); + $this->_call_counts = array(); + $this->_expected_counts = array(); + $this->_max_counts = array(); + $this->_expected_args = array(); + $this->_expected_args_at = array(); + $test = &$this->_getCurrentTestCase(); + $test->tell($this); + } + + /** + * Disables a name check when setting expectations. + * This hack is needed for the partial mocks. + * @access public + */ + function disableExpectationNameChecks() { + $this->_is_strict = false; + } + + /** + * Finds currently running test. + * @return SimpeTestCase Current test case. + * @access protected + */ + function &_getCurrentTestCase() { + $context = &SimpleTest::getContext(); + return $context->getTest(); + } + + /** + * 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 integer 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"); + $this->_actions->register($method, $args, new SimpleByValue($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"); + $this->_actions->registerAt($timing, $method, $args, new SimpleByValue($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"); + $this->_actions->register($method, $args, new SimpleByReference($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"); + $this->_actions->registerAt($timing, $method, $args, new SimpleByReference($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'); + $this->_expectations->expectArguments($method, $args, $message); + $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->expect($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->expect($method, $args, $message); + } + } + + /** + * Sets up a trigger to throw an exception upon the + * method call. + * @param string $method Method name to throw on. + */ + function throwOn($method, $exception = false, $args = false) { + $this->_dieOnNoMethod($method, "throw on"); + $this->_actions->register($method, $args, + new SimpleThrower($exception ? $exception : new Exception())); + } + + /** + * Sets up a trigger to throw an exception upon the + * method call. + */ + function throwAt($timing, $method, $exception = false, $args = false) { + $this->_dieOnNoMethod($method, "throw at"); + $this->_actions->registerAt($timing, $method, $args, + new SimpleThrower($exception ? $exception : new Exception())); + } + + /** + * Sets up a trigger to throw an error upon the + * method call. + */ + function errorOn($method, $error = 'A mock error', $args = false, $severity = E_USER_ERROR) { + $this->_dieOnNoMethod($method, "error on"); + $this->_actions->register($method, $args, new SimpleErrorThrower($error, $severity)); + } + + /** + * Sets up a trigger to throw an error upon the + * method call. + */ + function errorAt($timing, $method, $error = 'A mock error', $args = false, $severity = E_USER_ERROR) { + $this->_dieOnNoMethod($method, "error at"); + $this->_actions->registerAt($timing, $method, $args, new SimpleErrorThrower($error, $severity)); + } + + /** + * @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 $test_method Current method name. + * @param SimpleTestCase $test Test to send message to. + * @access public + */ + function atTestEnd($test_method, &$test) { + foreach ($this->_expected_counts as $method => $expectation) { + $test->assert($expectation, $this->getCallCount($method)); + } + foreach ($this->_max_counts as $method => $expectation) { + if ($expectation->test($this->getCallCount($method))) { + $test->assert($expectation, $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->_emulateCall($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 or other action. + * @access protected + */ + function &_emulateCall($method, $args, $step) { + return $this->_actions->respond($step, $method, $args); + } + + /** + * 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) { + $test = &$this->_getCurrentTestCase(); + if (isset($this->_max_counts[$method])) { + if (! $this->_max_counts[$method]->test($timing + 1)) { + $test->assert($this->_max_counts[$method], $timing + 1); + } + } + if (isset($this->_expected_args_at[$timing][$method])) { + $test->assert( + $this->_expected_args_at[$timing][$method], + $args, + "Mock method [$method] at [$timing] -> %s"); + } elseif (isset($this->_expected_args[$method])) { + $test->assert( + $this->_expected_args[$method], + $args, + "Mock method [$method] -> %s"); + } + } +} + +/** + * 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 static.'); + } + + /** + * 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 the 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->generateSubclass($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. + * @access public + * @static + */ + function getExpectationLine() { + $trace = new SimpleStackTrace(array('expect')); + return $trace->traceMethod(); + } +} + +/** + * @package SimpleTest + * @subpackage MockObjects + * @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; + + /** + * Builds initial reflection object. + * @param string $class Class to be mocked. + * @param string $mock_class New class with identical interface, + * but no behaviour. + */ + function MockGenerator($class, $mock_class) { + $this->_class = $class; + $this->_mock_class = $mock_class; + if (! $this->_mock_class) { + $this->_mock_class = 'Mock' . $this->_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; + } + $mock_reflection = new SimpleReflection($this->_mock_class); + if ($mock_reflection->classExistsSansAutoload()) { + return false; + } + $code = $this->_createClassCode($methods ? $methods : array()); + return eval("$code return \$code;"); + } + + /** + * Subclasses a class and overrides every method with a mock one + * that can have return values and expectations set. Chains + * to an aggregated SimpleMock. + * @param array $methods Additional methods to add beyond + * those in the 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 generateSubclass($methods) { + if (! $this->_reflection->classOrInterfaceExists()) { + return false; + } + $mock_reflection = new SimpleReflection($this->_mock_class); + if ($mock_reflection->classExistsSansAutoload()) { + return false; + } + if ($this->_reflection->isInterface() || $this->_reflection->hasFinal()) { + $code = $this->_createClassCode($methods ? $methods : array()); + return eval("$code return \$code;"); + } else { + $code = $this->_createSubclassCode($methods ? $methods : array()); + return eval("$code return \$code;"); + } + } + + /** + * 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 [' . $this->_mock_class . '] already exists'); + return false; + } + $code = $this->_extendClassCode($methods); + return eval("$code return \$code;"); + } + + /** + * 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"; + if (in_array('__construct', $this->_reflection->getMethods())) { + $code .= " " . $this->_reflection->getSignature('__construct') . " {\n"; + $code .= " \$this->" . $this->_mock_base . "();\n"; + $code .= " }\n"; + } + $code .= $this->_createHandlerCode($methods); + $code .= "}\n"; + return $code; + } + + /** + * The new mock class code as a string. The mock will + * be a subclass of the original mocked class. + * @param array $methods Additional methods. + * @return string Code for new mock class. + * @access private + */ + function _createSubclassCode($methods) { + $code = "class " . $this->_mock_class . " extends " . $this->_class . " {\n"; + $code .= " var \$_mock;\n"; + $code .= $this->_addMethodList(array_merge($methods, $this->_reflection->getMethods())); + $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->_chainThrowMethods(); + $code .= $this->_overrideMethods($this->_reflection->getMethods()); + $code .= $this->_createNewMethodCode($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->_chainThrowMethods(); + $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; + } + + /** + * Creates code within a class to generate a new + * methods. All methods call the _invoke() handler + * on the internal mock with the method name and + * the arguments in an array. + * @param array $methods Additional methods. + * @access private + */ + function _createNewMethodCode($methods) { + $code = ''; + 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->_mock->_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')); + } + + /** + * 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("', '", array_map('strtolower', $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(strtolower($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, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expect(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectArguments(\$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectArguments(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectAt(\$timing, \$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectArgumentsAt(\$timing, \$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectArgumentsAt(\$timing, \$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectArgumentsAt(\$timing, \$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectCallCount(\$method, \$count) {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectCallCount(\$method, \$count, \$msg = '%s');\n"; + $code .= " }\n"; + $code .= " function expectMaximumCallCount(\$method, \$count, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectMaximumCallCount(\$method, \$count, \$msg = '%s');\n"; + $code .= " }\n"; + $code .= " function expectMinimumCallCount(\$method, \$count, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectMinimumCallCount(\$method, \$count, \$msg = '%s');\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, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectOnce(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function expectAtLeastOnce(\$method, \$args = false, \$msg = '%s') {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->expectAtLeastOnce(\$method, \$args, \$msg);\n"; + $code .= " }\n"; + $code .= " function tally() {\n"; + $code .= " }\n"; + return $code; + } + + /** + * Adds code for chaining the throw methods. + * @return string Code for chains. + * @access private + */ + function _chainThrowMethods() { + $code = " function throwOn(\$method, \$exception = false, \$args = false) {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->throwOn(\$method, \$exception, \$args);\n"; + $code .= " }\n"; + $code .= " function throwAt(\$timing, \$method, \$exception = false, \$args = false) {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->throwAt(\$timing, \$method, \$exception, \$args);\n"; + $code .= " }\n"; + $code .= " function errorOn(\$method, \$error = 'A mock error', \$args = false, \$severity = E_USER_ERROR) {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->errorOn(\$method, \$error, \$args, \$severity);\n"; + $code .= " }\n"; + $code .= " function errorAt(\$timing, \$method, \$error = 'A mock error', \$args = false, \$severity = E_USER_ERROR) {\n"; + $code .= $this->_bailOutIfNotMocked("\$method"); + $code .= " \$this->_mock->errorAt(\$timing, \$method, \$error, \$args, \$severity);\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) { + if ($this->_isConstructor($method)) { + continue; + } + $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; + } +} +?> diff --git a/contrib/simpletest/simpletest/page.php b/contrib/simpletest/simpletest/page.php new file mode 100644 index 00000000..08e5649d --- /dev/null +++ b/contrib/simpletest/simpletest/page.php @@ -0,0 +1,983 @@ + 'SimpleAnchorTag', + 'title' => 'SimpleTitleTag', + 'base' => 'SimpleBaseTag', + '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 $_base = false; + 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; + } + + /** + * Base URL if set via BASE tag page url otherwise + * @return SimpleUrl Base url. + * @access public + */ + function getBaseUrl() { + return $this->_base; + } + + /** + * 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() == "base") { + $this->_setBase($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); + } + + /** + * 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] = $this->expandUrl($url); + } + return $urls; + } + + /** + * Fetches a list of loaded frames. + * @return array/string Just the URL for a single page. + * @access public + */ + function getFrames() { + $url = $this->expandUrl($this->getUrl()); + return $url->asString(); + } + + /** + * Accessor for a list of all links. + * @return array List of urls with scheme of + * http or https and hostname. + * @access public + */ + function getUrls() { + $all = array(); + foreach ($this->_links as $link) { + $url = $this->_getUrlFromLink($link); + $all[] = $url->asString(); + } + 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 tag into a target URL. + * @param SimpleAnchor $link Parsed link. + * @return SimpleUrl URL with frame target if any. + * @access private + */ + function _getUrlFromLink($link) { + $url = $this->expandUrl($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 public + */ + function expandUrl($url) { + if (! is_object($url)) { + $url = new SimpleUrl($url); + } + $location = $this->getBaseUrl() ? $this->getBaseUrl() : new SimpleUrl(); + return $url->makeAbsolute($location->makeAbsolute($this->getUrl())); + } + + /** + * Sets the base url for the page. + * @param SimpleTag $tag Base URL for page. + * @access protected + */ + function _setBase(&$tag) { + $url = $tag->getAttribute('href'); + $this->_base = new SimpleUrl($url); + } + + /** + * 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, $position=false) { + $is_set = false; + for ($i = 0; $i < count($this->_complete_forms); $i++) { + if ($this->_complete_forms[$i]->setField($selector, $value, $position)) { + $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; + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/parser.php b/contrib/simpletest/simpletest/parser.php new file mode 100644 index 00000000..3f3b37b8 --- /dev/null +++ b/contrib/simpletest/simpletest/parser.php @@ -0,0 +1,764 @@ + $constant) { + if (! defined($constant)) { + define($constant, $i + 1); + } +} +/**#@-*/ + +/** + * 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; + } +} + +/** + * Breaks 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', 'base', '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('addExitPattern('', 'css'); + $this->mapHandler('js', 'ignore'); + $this->addEntryPattern('addExitPattern('', 'js'); + $this->mapHandler('comment', 'ignore'); + $this->addEntryPattern('', '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("", '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 ($this->_current_attribute) { + 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) { + return html_entity_decode($html, ENT_QUOTES); + } + + /** + * 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('|]*>.*?|', '', $text); + $text = preg_replace('|]*alt\s*=\s*"([^"]*)"[^>]*>|', ' \1 ', $text); + $text = preg_replace('|]*alt\s*=\s*\'([^\']*)\'[^>]*>|', ' \1 ', $text); + $text = preg_replace('|]*alt\s*=\s*([a-zA-Z_]+)[^>]*>|', ' \1 ', $text); + $text = preg_replace('|<[^>]*>|', '', $text); + $text = SimpleHtmlSaxParser::decodeHtml($text); + $text = preg_replace('|\s+|', ' ', $text); + return trim(trim($text), "\xA0"); // TODO: The \xAO is a  . Add a test for this. + } +} + +/** + * 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/contrib/simpletest/simpletest/reflection_php4.php b/contrib/simpletest/simpletest/reflection_php4.php new file mode 100644 index 00000000..6c93915a --- /dev/null +++ b/contrib/simpletest/simpletest/reflection_php4.php @@ -0,0 +1,136 @@ +_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; + } + + /** + * Determines if the the entity is an interface, which for PHP 4 + * will never be the case. + * @returns boolean True if interface. + * @access public + */ + function isInterface() { + return false; + } + + /** + * Scans for final methods, but as it's PHP 4 there + * aren't any. + * @returns boolean True if the class has a final method. + * @access public + */ + function hasFinal() { + 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/contrib/simpletest/simpletest/reflection_php5.php b/contrib/simpletest/simpletest/reflection_php5.php new file mode 100644 index 00000000..8383bccd --- /dev/null +++ b/contrib/simpletest/simpletest/reflection_php5.php @@ -0,0 +1,380 @@ +_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. + * @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; + } + + /** + * Trivially determines if the class is abstract. + * @returns boolean True if abstract. + * @access public + */ + function isAbstract() { + $reflection = new ReflectionClass($this->_interface); + return $reflection->isAbstract(); + } + + /** + * Trivially determines if the class is an interface. + * @returns boolean True if interface. + * @access public + */ + function isInterface() { + $reflection = new ReflectionClass($this->_interface); + return $reflection->isInterface(); + } + + /** + * Scans for final methods, as they screw up inherited + * mocks by not allowing you to override them. + * @returns boolean True if the class has a final method. + * @access public + */ + function hasFinal() { + $reflection = new ReflectionClass($this->_interface); + foreach ($reflection->getMethods() as $method) { + if ($method->isFinal()) { + return true; + } + } + return false; + } + + /** + * Whittles a list of interfaces down to only the + * necessary 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(); + $blacklist = array(); + foreach ($interfaces as $interface) { + foreach($interfaces as $possible_parent) { + if ($interface->getName() == $possible_parent->getName()) { + continue; + } + if ($interface->isSubClassOf($possible_parent)) { + $blacklist[$possible_parent->getName()] = true; + } + } + if (!isset($blacklist[$interface->getName()])) { + $parents[] = $interface->getName(); + } + } + return $parents; + } + + /** + * Checks whether a method is abstract or not. + * @param string $name Method name. + * @return bool true if method is abstract, else false + * @access private + */ + function _isAbstractMethod($name) { + $interface = new ReflectionClass($this->_interface); + if (! $interface->hasMethod($name)) { + return false; + } + return $interface->getMethod($name)->isAbstract(); + } + + /** + * Checks whether a method is the constructor. + * @param string $name Method name. + * @return bool true if method is the constructor + * @access private + */ + function _isConstructor($name) { + return ($name == '__construct') || ($name == $this->_interface); + } + + /** + * Checks whether a method is abstract in all parents or not. + * @param string $name Method name. + * @return bool true if method is abstract in parent, else false + * @access private + */ + function _isAbstractMethodInParents($name) { + $interface = new ReflectionClass($this->_interface); + $parent = $interface->getParentClass(); + while($parent) { + if (! $parent->hasMethod($name)) { + return false; + } + if ($parent->getMethod($name)->isAbstract()) { + return true; + } + $parent = $parent->getParentClass(); + } + return false; + } + + /** + * Checks whether a method is static or not. + * @param string $name Method name + * @return bool true if method is static, else false + * @access private + */ + function _isStaticMethod($name) { + $interface = new ReflectionClass($this->_interface); + if (! $interface->hasMethod($name)) { + return false; + } + return $interface->getMethod($name)->isStatic(); + } + + /** + * Writes 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 == '__set') { + return 'function __set($key, $value)'; + } + if ($name == '__call') { + return 'function __call($method, $arguments)'; + } + if (version_compare(phpversion(), '5.1.0', '>=')) { + if (in_array($name, array('__get', '__isset', $name == '__unset'))) { + return "function {$name}(\$key)"; + } + } + if ($name == '__toString') { + return "function $name()"; + } + if ($this->_isInterfaceMethod($name) || + $this->_isAbstractMethod($name) || + $this->_isAbstractMethodInParents($name) || + $this->_isStaticMethod($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() ? '&' : ''; + $static = $method->isStatic() ? 'static ' : ''; + return "{$static}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) { + $signature = ''; + $type = $parameter->getClass(); + if (is_null($type) && version_compare(phpversion(), '5.1.0', '>=') && $parameter->isArray()) { + $signature .= 'array '; + } elseif (!is_null($type)) { + $signature .= $type->getName() . ' '; + } + if ($parameter->isPassedByReference()) { + $signature .= '&'; + } + $signature .= '$' . $this->_suppressSpurious($parameter->getName()); + if ($this->_isOptional($parameter)) { + $signature .= ' = null'; + } + $signatures[] = $signature; + } + 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; + } +} +?> diff --git a/contrib/simpletest/simpletest/remote.php b/contrib/simpletest/simpletest/remote.php new file mode 100644 index 00000000..8889ed7b --- /dev/null +++ b/contrib/simpletest/simpletest/remote.php @@ -0,0 +1,117 @@ +_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() { + $browser = &new SimpleBrowser(); + return $browser; + } + + /** + * Creates the XML parser. + * @param SimpleReporter $reporter Target of test results. + * @return SimpleTestXmlListener XML reader. + * @access protected + */ + function &_createParser(&$reporter) { + $parser = &new SimpleTestXmlParser($reporter); + return $parser; + } + + /** + * 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/contrib/simpletest/simpletest/reporter.php b/contrib/simpletest/simpletest/reporter.php new file mode 100644 index 00000000..a13eff8c --- /dev/null +++ b/contrib/simpletest/simpletest/reporter.php @@ -0,0 +1,447 @@ +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 ""; + print "\n\n$test_name\n"; + print "\n"; + print "\n"; + print "\n\n"; + print "

$test_name

\n"; + flush(); + } + + /** + * Send the headers necessary to ensure the page is + * reloaded on every request. Otherwise you could be + * scratching your head over out of date test data. + * @access public + * @static + */ + function sendNoCacheHeaders() { + if (! headers_sent()) { + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); + header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); + header("Cache-Control: no-store, no-cache, must-revalidate"); + header("Cache-Control: post-check=0, pre-check=0", false); + header("Pragma: no-cache"); + } + } + + /** + * Paints the CSS. Add additional styles here. + * @return string CSS code as text. + * @access protected + */ + function _getCss() { + return ".fail { background-color: inherit; color: red; }" . + ".pass { background-color: inherit; color: green; }" . + " pre { background-color: lightgray; color: inherit; }"; + } + + /** + * Paints the end of the test with a summary of + * the passes and failures. + * @param string $test_name Name class of test. + * @access public + */ + function paintFooter($test_name) { + $colour = ($this->getFailCount() + $this->getExceptionCount() > 0 ? "red" : "green"); + print "
"; + print $this->getTestCaseProgress() . "/" . $this->getTestCaseCount(); + print " test cases complete:\n"; + print "" . $this->getPassCount() . " passes, "; + print "" . $this->getFailCount() . " fails and "; + print "" . $this->getExceptionCount() . " exceptions."; + print "
\n"; + print "\n\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 "Fail: "; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print implode(" -> ", $breadcrumb); + print " -> " . $this->_htmlEntities($message) . "
\n"; + } + + /** + * Paints a PHP error. + * @param string $message Message is ignored. + * @access public + */ + function paintError($message) { + parent::paintError($message); + print "Exception: "; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print implode(" -> ", $breadcrumb); + print " -> " . $this->_htmlEntities($message) . "
\n"; + } + + /** + * Paints a PHP exception. + * @param Exception $exception Exception to display. + * @access public + */ + function paintException($exception) { + parent::paintException($exception); + print "Exception: "; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print implode(" -> ", $breadcrumb); + $message = 'Unexpected exception of type [' . get_class($exception) . + '] with message ['. $exception->getMessage() . + '] in ['. $exception->getFile() . + ' line ' . $exception->getLine() . ']'; + print " -> " . $this->_htmlEntities($message) . "
\n"; + } + + /** + * Prints the message for skipping tests. + * @param string $message Text of skip condition. + * @access public + */ + function paintSkip($message) { + parent::paintSkip($message); + print "Skipped: "; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print implode(" -> ", $breadcrumb); + print " -> " . $this->_htmlEntities($message) . "
\n"; + } + + /** + * Paints formatted text such as dumped variables. + * @param string $message Text to show. + * @access public + */ + function paintFormattedMessage($message) { + print '
' . $this->_htmlEntities($message) . '
'; + } + + /** + * Character set adjusted entity conversion. + * @param string $message Plain text or Unicode message. + * @return string Browser readable message. + * @access protected + */ + function _htmlEntities($message) { + return htmlentities($message, ENT_COMPAT, $this->_character_set); + } +} + +/** + * Sample minimal test displayer. Generates only + * failure messages and a pass count. For command + * line use. I've tried to make it look like JUnit, + * but I wanted to output the errors as they arrived + * which meant dropping the dots. + * @package SimpleTest + * @subpackage UnitTester + */ +class TextReporter extends SimpleReporter { + + /** + * Does nothing yet. The first output will + * be sent on the first test start. + * @access public + */ + function TextReporter() { + $this->SimpleReporter(); + } + + /** + * Paints the title only. + * @param string $test_name Name class of test. + * @access public + */ + function paintHeader($test_name) { + if (! SimpleReporter::inCli()) { + header('Content-type: text/plain'); + } + print "$test_name\n"; + flush(); + } + + /** + * Paints the end of the test with a summary of + * the passes and failures. + * @param string $test_name Name class of test. + * @access public + */ + function paintFooter($test_name) { + if ($this->getFailCount() + $this->getExceptionCount() == 0) { + print "OK\n"; + } else { + print "FAILURES!!!\n"; + } + print "Test cases run: " . $this->getTestCaseProgress() . + "/" . $this->getTestCaseCount() . + ", Passes: " . $this->getPassCount() . + ", Failures: " . $this->getFailCount() . + ", Exceptions: " . $this->getExceptionCount() . "\n"; + } + + /** + * Paints the test failure as a stack trace. + * @param string $message Failure message displayed in + * the context of the other tests. + * @access public + */ + function paintFail($message) { + parent::paintFail($message); + print $this->getFailCount() . ") $message\n"; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print "\tin " . implode("\n\tin ", array_reverse($breadcrumb)); + print "\n"; + } + + /** + * Paints a PHP error or exception. + * @param string $message Message to be shown. + * @access public + * @abstract + */ + function paintError($message) { + parent::paintError($message); + print "Exception " . $this->getExceptionCount() . "!\n$message\n"; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print "\tin " . implode("\n\tin ", array_reverse($breadcrumb)); + print "\n"; + } + + /** + * Paints a PHP error or exception. + * @param Exception $exception Exception to describe. + * @access public + * @abstract + */ + function paintException($exception) { + parent::paintException($exception); + $message = 'Unexpected exception of type [' . get_class($exception) . + '] with message ['. $exception->getMessage() . + '] in ['. $exception->getFile() . + ' line ' . $exception->getLine() . ']'; + print "Exception " . $this->getExceptionCount() . "!\n$message\n"; + $breadcrumb = $this->getTestList(); + array_shift($breadcrumb); + print "\tin " . implode("\n\tin ", array_reverse($breadcrumb)); + print "\n"; + } + + /** + * Prints the message for skipping tests. + * @param string $message Text of skip condition. + * @access public + */ + function paintSkip($message) { + parent::paintSkip($message); + print "Skip: $message\n"; + } + + /** + * Paints formatted text such as dumped variables. + * @param string $message Text to show. + * @access public + */ + function paintFormattedMessage($message) { + print "$message\n"; + flush(); + } +} + +/** + * 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 $_on; + + /** + * 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->_off(); + } else { + $this->_on(); + } + 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 _matchesTestCase($test_case) { + return $this->_just_this_case == strtolower($test_case); + } + + /** + * Compares criteria to actual the test name. If no + * name was specified at the beginning, then all tests + * can run. + * @param string $method The incoming test method. + * @return boolean True if matched. + * @access protected + */ + function _shouldRunTest($test_case, $method) { + if ($this->_isOn() || $this->_matchesTestCase($test_case)) { + if ($this->_just_this_test) { + return $this->_just_this_test == strtolower($method); + } else { + return true; + } + } + return false; + } + + /** + * Switch on testing for the group or subgroup. + * @access private + */ + function _on() { + $this->_on = true; + } + + /** + * Switch off testing for the group or subgroup. + * @access private + */ + function _off() { + $this->_on = false; + } + + /** + * Is this group actually being tested? + * @return boolean True if the current test group is active. + * @access private + */ + function _isOn() { + return $this->_on; + } + + /** + * 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->_shouldRunTest($test_case, $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->_just_this_case && $this->_matchesTestCase($test_case)) { + $this->_on(); + } + $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) { + $this->_reporter->paintGroupEnd($test_case); + if ($this->_just_this_case && $this->_matchesTestCase($test_case)) { + $this->_off(); + } + } +} + +/** + * Suppresses skip messages. + * @package SimpleTest + * @subpackage UnitTester + */ +class NoSkipsReporter extends SimpleReporterDecorator { + + /** + * Does nothing. + * @param string $message Text of skip condition. + * @access public + */ + function paintSkip($message) { } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/scorer.php b/contrib/simpletest/simpletest/scorer.php new file mode 100644 index 00000000..cc1331b8 --- /dev/null +++ b/contrib/simpletest/simpletest/scorer.php @@ -0,0 +1,863 @@ +_passes = 0; + $this->_fails = 0; + $this->_exceptions = 0; + $this->_is_dry_run = false; + } + + /** + * Signals that the next evaluation will be a dry + * run. That is, the structure events will be + * recorded, but no tests will be run. + * @param boolean $is_dry Dry run if true. + * @access public + */ + function makeDry($is_dry = true) { + $this->_is_dry_run = $is_dry; + } + + /** + * The reporter has a veto on what should be run. + * @param string $test_case_name name of test case. + * @param string $method Name of test method. + * @access public + */ + function shouldInvoke($test_case_name, $method) { + return ! $this->_is_dry_run; + } + + /** + * Can wrap the invoker in preperation for running + * a test. + * @param SimpleInvoker $invoker Individual test runner. + * @return SimpleInvoker Wrapped test runner. + * @access public + */ + function &createInvoker(&$invoker) { + return $invoker; + } + + /** + * Accessor for current status. Will be false + * if there have been any failures or exceptions. + * Used for command line tools. + * @return boolean True if no failures. + * @access public + */ + function getStatus() { + if ($this->_exceptions + $this->_fails > 0) { + return false; + } + return true; + } + + /** + * Paints the start of a group test. + * @param string $test_name Name of test or other label. + * @param integer $size Number of test cases starting. + * @access public + */ + function paintGroupStart($test_name, $size) { + } + + /** + * Paints the end of a group test. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintGroupEnd($test_name) { + } + + /** + * Paints the start of a test case. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintCaseStart($test_name) { + } + + /** + * Paints the end of a test case. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintCaseEnd($test_name) { + } + + /** + * Paints the start of a test method. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintMethodStart($test_name) { + } + + /** + * Paints the end of a test method. + * @param string $test_name Name of test or other label. + * @access public + */ + function paintMethodEnd($test_name) { + } + + /** + * Increments the pass count. + * @param string $message Message is ignored. + * @access public + */ + function paintPass($message) { + $this->_passes++; + } + + /** + * Increments the fail count. + * @param string $message Message is ignored. + * @access public + */ + function paintFail($message) { + $this->_fails++; + } + + /** + * Deals with PHP 4 throwing an error. + * @param string $message Text of error formatted by + * the test case. + * @access public + */ + function paintError($message) { + $this->_exceptions++; + } + + /** + * Deals with PHP 5 throwing an exception. + * @param Exception $exception The actual exception thrown. + * @access public + */ + function paintException($exception) { + $this->_exceptions++; + } + + /** + * Prints the message for skipping tests. + * @param string $message Text of skip condition. + * @access public + */ + function paintSkip($message) { + } + + /** + * Accessor for the number of passes so far. + * @return integer Number of passes. + * @access public + */ + function getPassCount() { + return $this->_passes; + } + + /** + * Accessor for the number of fails so far. + * @return integer Number of fails. + * @access public + */ + function getFailCount() { + return $this->_fails; + } + + /** + * Accessor for the number of untrapped errors + * so far. + * @return integer Number of exceptions. + * @access public + */ + function getExceptionCount() { + return $this->_exceptions; + } + + /** + * Paints a simple supplementary message. + * @param string $message Text to display. + * @access public + */ + function paintMessage($message) { + } + + /** + * Paints a formatted ASCII message such as a + * variable dump. + * @param string $message Text to display. + * @access public + */ + function paintFormattedMessage($message) { + } + + /** + * By default just ignores user generated events. + * @param string $type Event type as text. + * @param mixed $payload Message or object. + * @access public + */ + function paintSignal($type, $payload) { + } +} + +/** + * Recipient of generated test messages that can display + * page footers and headers. Also keeps track of the + * test nesting. This is the main base class on which + * to build the finished test (page based) displays. + * @package SimpleTest + * @subpackage UnitTester + */ +class SimpleReporter extends SimpleScorer { + var $_test_stack; + var $_size; + var $_progress; + + /** + * Starts the display with no results in. + * @access public + */ + function SimpleReporter() { + $this->SimpleScorer(); + $this->_test_stack = array(); + $this->_size = null; + $this->_progress = 0; + } + + /** + * Gets the formatter for variables and other small + * generic data items. + * @return SimpleDumper Formatter. + * @access public + */ + function getDumper() { + return new SimpleDumper(); + } + + /** + * Paints the start of a group test. Will also paint + * the page header and footer if this is the + * first test. Will stash the size if the first + * start. + * @param string $test_name Name of test that is starting. + * @param integer $size Number of test cases starting. + * @access public + */ + function paintGroupStart($test_name, $size) { + if (! isset($this->_size)) { + $this->_size = $size; + } + if (count($this->_test_stack) == 0) { + $this->paintHeader($test_name); + } + $this->_test_stack[] = $test_name; + } + + /** + * Paints the end of a group test. Will paint the page + * footer if the stack of tests has unwound. + * @param string $test_name Name of test that is ending. + * @param integer $progress Number of test cases ending. + * @access public + */ + function paintGroupEnd($test_name) { + array_pop($this->_test_stack); + if (count($this->_test_stack) == 0) { + $this->paintFooter($test_name); + } + } + + /** + * Paints the start of a test case. Will also paint + * the page header and footer if this is the + * first test. Will stash the size if the first + * start. + * @param string $test_name Name of test that is starting. + * @access public + */ + function paintCaseStart($test_name) { + if (! isset($this->_size)) { + $this->_size = 1; + } + if (count($this->_test_stack) == 0) { + $this->paintHeader($test_name); + } + $this->_test_stack[] = $test_name; + } + + /** + * Paints the end of a test case. Will paint the page + * footer if the stack of tests has unwound. + * @param string $test_name Name of test that is ending. + * @access public + */ + function paintCaseEnd($test_name) { + $this->_progress++; + array_pop($this->_test_stack); + if (count($this->_test_stack) == 0) { + $this->paintFooter($test_name); + } + } + + /** + * Paints the start of a test method. + * @param string $test_name Name of test that is starting. + * @access public + */ + function paintMethodStart($test_name) { + $this->_test_stack[] = $test_name; + } + + /** + * Paints the end of a test method. Will paint the page + * footer if the stack of tests has unwound. + * @param string $test_name Name of test that is ending. + * @access public + */ + function paintMethodEnd($test_name) { + array_pop($this->_test_stack); + } + + /** + * Paints the test document header. + * @param string $test_name First test top level + * to start. + * @access public + * @abstract + */ + function paintHeader($test_name) { + } + + /** + * Paints the test document footer. + * @param string $test_name The top level test. + * @access public + * @abstract + */ + function paintFooter($test_name) { + } + + /** + * Accessor for internal test stack. For + * subclasses that need to see the whole test + * history for display purposes. + * @return array List of methods in nesting order. + * @access public + */ + function getTestList() { + return $this->_test_stack; + } + + /** + * Accessor for total test size in number + * of test cases. Null until the first + * test is started. + * @return integer Total number of cases at start. + * @access public + */ + function getTestCaseCount() { + return $this->_size; + } + + /** + * Accessor for the number of test cases + * completed so far. + * @return integer Number of ended cases. + * @access public + */ + function getTestCaseProgress() { + return $this->_progress; + } + + /** + * Static check for running in the comand line. + * @return boolean True if CLI. + * @access public + * @static + */ + function inCli() { + return php_sapi_name() == 'cli'; + } +} + +/** + * For modifying the behaviour of the visual reporters. + * @package SimpleTest + * @subpackage UnitTester + */ +class SimpleReporterDecorator { + var $_reporter; + + /** + * Mediates between the reporter and the test case. + * @param SimpleScorer $reporter Reporter to receive events. + */ + function 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); + } + + /** + * Gets the formatter for variables and other small + * generic data items. + * @return SimpleDumper Formatter. + * @access public + */ + function getDumper() { + return $this->_reporter->getDumper(); + } + + /** + * 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 Exception $exception Exception to show. + * @access public + */ + function paintException($exception) { + $this->_reporter->paintException($exception); + } + + /** + * Prints the message for skipping tests. + * @param string $message Text of skip condition. + * @access public + */ + function paintSkip($message) { + $this->_reporter->paintSkip($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; + } + + /** + * Gets the formatter for variables and other small + * generic data items. + * @return SimpleDumper Formatter. + * @access public + */ + function getDumper() { + return new SimpleDumper(); + } + + /** + * Paints the start of a group test. + * @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 Exception $exception Exception to display. + * @access public + */ + function paintException($exception) { + for ($i = 0; $i < count($this->_reporters); $i++) { + $this->_reporters[$i]->paintException($exception); + } + } + + /** + * Prints the message for skipping tests. + * @param string $message Text of skip condition. + * @access public + */ + function paintSkip($message) { + for ($i = 0; $i < count($this->_reporters); $i++) { + $this->_reporters[$i]->paintSkip($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/contrib/simpletest/simpletest/selector.php b/contrib/simpletest/simpletest/selector.php new file mode 100644 index 00000000..de044b85 --- /dev/null +++ b/contrib/simpletest/simpletest/selector.php @@ -0,0 +1,137 @@ +_name = $name; + } + + function getName() { + return $this->_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/contrib/simpletest/simpletest/shell_tester.php b/contrib/simpletest/simpletest/shell_tester.php new file mode 100644 index 00000000..7b98869e --- /dev/null +++ b/contrib/simpletest/simpletest/shell_tester.php @@ -0,0 +1,333 @@ +_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 that 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(); + } + + /** + * 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) { + return $this->assert(new TrueExpectation(), $result, $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 = '%s') { + return $this->assert(new FalseExpectation(), $result, $message); + } + + /** + * 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; + } +} +?> diff --git a/contrib/simpletest/simpletest/simpletest.php b/contrib/simpletest/simpletest/simpletest.php new file mode 100644 index 00000000..bab2c1a6 --- /dev/null +++ b/contrib/simpletest/simpletest/simpletest.php @@ -0,0 +1,478 @@ += 0) { + require_once(dirname(__FILE__) . '/reflection_php5.php'); +} else { + require_once(dirname(__FILE__) . '/reflection_php4.php'); +} +require_once(dirname(__FILE__) . '/default_reporter.php'); +require_once(dirname(__FILE__) . '/compatibility.php'); +/**#@-*/ + +/** + * Registry and test context. Includes a few + * global options that I'm slowly getting rid of. + * @package SimpleTest + * @subpackage UnitTester + */ +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 just nice to have the ignore() + * calls at the top of the file before the actual declarations. + * @param array $classes Class names of interest. + * @static + * @access public + */ + function ignoreParentsIfIgnored($classes) { + $registry = &SimpleTest::_getRegistry(); + foreach ($classes as $class) { + if (SimpleTest::isIgnored($class)) { + $reflection = new SimpleReflection($class); + if ($parent = $reflection->getParent()) { + SimpleTest::ignore($parent); + } + } + } + } + + /** + * Puts the object to the global pool of 'preferred' objects + * which can be retrieved with SimpleTest :: preferred() method. + * Instances of the same class are overwritten. + * @param object $object Preferred object + * @static + * @access public + * @see preferred() + */ + function prefer(&$object) { + $registry = &SimpleTest::_getRegistry(); + $registry['Preferred'][] = &$object; + } + + /** + * Retrieves 'preferred' objects from global pool. Class filter + * can be applied in order to retrieve the object of the specific + * class + * @param array|string $classes Allowed classes or interfaces. + * @static + * @access public + * @return array|object|null + * @see prefer() + */ + function &preferred($classes) { + if (! is_array($classes)) { + $classes = array($classes); + } + $registry = &SimpleTest::_getRegistry(); + for ($i = count($registry['Preferred']) - 1; $i >= 0; $i--) { + foreach ($classes as $class) { + if (SimpleTestCompatibility::isA($registry['Preferred'][$i], $class)) { + return $registry['Preferred'][$i]; + } + } + } + return null; + } + + /** + * Test to see if a test case is in the ignore + * list. Quite obviously the ignore list should + * be a separate object and will be one day. + * This method is internal to SimpleTest. Don't + * use it. + * @param string $class Class name to test. + * @return boolean True if should not be run. + * @access public + * @static + */ + function isIgnored($class) { + $registry = &SimpleTest::_getRegistry(); + return isset($registry['IgnoreList'][strtolower($class)]); + } + + /** + * @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']; + } + + /** + * Accessor for global registry of options. + * @return hash All stored values. + * @access private + * @static + */ + function &_getRegistry() { + static $registry = false; + if (! $registry) { + $registry = SimpleTest::_getDefaults(); + } + return $registry; + } + + /** + * Accessor for the context of the current + * test run. + * @return SimpleTestContext Current test run. + * @access public + * @static + */ + function &getContext() { + static $context = false; + if (! $context) { + $context = new SimpleTestContext(); + } + return $context; + } + + /** + * Constant default values. + * @return hash All registry defaults. + * @access private + * @static + */ + function _getDefaults() { + return array( + 'StubBaseClass' => 'SimpleStub', + 'MockBaseClass' => 'SimpleMock', + 'IgnoreList' => array(), + 'DefaultProxy' => false, + 'DefaultProxyUsername' => false, + 'DefaultProxyPassword' => false, + 'Preferred' => array(new HtmlReporter(), new TextReporter(), new XmlReporter())); + } +} + +/** + * Container for all components for a specific + * test run. Makes things like error queues + * available to PHP event handlers, and also + * gets around some nasty reference issues in + * the mocks. + * @package SimpleTest + */ +class SimpleTestContext { + var $_test; + var $_reporter; + var $_resources; + + /** + * Clears down the current context. + * @access public + */ + function clear() { + $this->_resources = array(); + } + + /** + * Sets the current test case instance. This + * global instance can be used by the mock objects + * to send message to the test cases. + * @param SimpleTestCase $test Test case to register. + * @access public + */ + function setTest(&$test) { + $this->clear(); + $this->_test = &$test; + } + + /** + * Accessor for currently running test case. + * @return SimpleTestCase Current test. + * @access public + */ + function &getTest() { + return $this->_test; + } + + /** + * Sets the current reporter. This + * global instance can be used by the mock objects + * to send messages. + * @param SimpleReporter $reporter Reporter to register. + * @access public + */ + function setReporter(&$reporter) { + $this->clear(); + $this->_reporter = &$reporter; + } + + /** + * Accessor for current reporter. + * @return SimpleReporter Current reporter. + * @access public + */ + function &getReporter() { + return $this->_reporter; + } + + /** + * Accessor for the Singleton resource. + * @return object Global resource. + * @access public + * @static + */ + function &get($resource) { + if (! isset($this->_resources[$resource])) { + $this->_resources[$resource] = &new $resource(); + } + return $this->_resources[$resource]; + } +} + +/** + * Interrogates the stack trace to recover the + * failure point. + * @package SimpleTest + * @subpackage UnitTester + */ +class SimpleStackTrace { + var $_prefixes; + + /** + * Stashes the list of target prefixes. + * @param array $prefixes List of method prefixes + * to search for. + */ + function SimpleStackTrace($prefixes) { + $this->_prefixes = $prefixes; + } + + /** + * Extracts the last method name that was not within + * Simpletest itself. Captures a stack trace if none given. + * @param array $stack List of stack frames. + * @return string Snippet of test report with line + * number and file. + * @access public + */ + function traceMethod($stack = false) { + $stack = $stack ? $stack : $this->_captureTrace(); + foreach ($stack as $frame) { + if ($this->_frameLiesWithinSimpleTestFolder($frame)) { + continue; + } + if ($this->_frameMatchesPrefix($frame)) { + return ' at [' . $frame['file'] . ' line ' . $frame['line'] . ']'; + } + } + return ''; + } + + /** + * Test to see if error is generated by SimpleTest itself. + * @param array $frame PHP stack frame. + * @return boolean True if a SimpleTest file. + * @access private + */ + function _frameLiesWithinSimpleTestFolder($frame) { + if (isset($frame['file'])) { + $path = substr(SIMPLE_TEST, 0, -1); + if (strpos($frame['file'], $path) === 0) { + if (dirname($frame['file']) == $path) { + return true; + } + } + } + return false; + } + + /** + * Tries to determine if the method call is an assert, etc. + * @param array $frame PHP stack frame. + * @return boolean True if matches a target. + * @access private + */ + function _frameMatchesPrefix($frame) { + foreach ($this->_prefixes as $prefix) { + if (strncmp($frame['function'], $prefix, strlen($prefix)) == 0) { + return true; + } + } + return false; + } + + /** + * Grabs a current stack trace. + * @return array Fulle trace. + * @access private + */ + function _captureTrace() { + if (function_exists('debug_backtrace')) { + return array_reverse(debug_backtrace()); + } + return array(); + } +} + +/** + * @package SimpleTest + * @subpackage UnitTester + * @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(); + } +} +?> diff --git a/contrib/simpletest/simpletest/socket.php b/contrib/simpletest/simpletest/socket.php new file mode 100644 index 00000000..3ad5a9ff --- /dev/null +++ b/contrib/simpletest/simpletest/socket.php @@ -0,0 +1,216 @@ +_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/contrib/simpletest/simpletest/tag.php b/contrib/simpletest/simpletest/tag.php new file mode 100644 index 00000000..7bccae20 --- /dev/null +++ b/contrib/simpletest/simpletest/tag.php @@ -0,0 +1,1418 @@ +_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); + } +} + +/** + * Base url. + * @package SimpleTest + * @subpackage WebTester + */ +class SimpleBaseTag extends SimpleTag { + + /** + * Starts with a named tag with attributes only. + * @param hash $attributes Attribute names and + * string values. + */ + function SimpleBaseTag($attributes) { + $this->SimpleTag('base', $attributes); + } + + /** + * Base tag is not a block tag. + * @return boolean false + * @access public + */ + function expectEndTag() { + return false; + } +} + +/** + * 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/contrib/simpletest/simpletest/test.zip b/contrib/simpletest/simpletest/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..46bccfbc48b363ce23cb30ed092f655bb4a68525 GIT binary patch literal 67459 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 literal 0 HcmV?d00001 diff --git a/contrib/simpletest/simpletest/test_case.php b/contrib/simpletest/simpletest/test_case.php new file mode 100644 index 00000000..e5b22983 --- /dev/null +++ b/contrib/simpletest/simpletest/test_case.php @@ -0,0 +1,708 @@ += 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__) . DIRECTORY_SEPARATOR); +} +/**#@-*/ + +/** + * 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; + var $_should_skip = false; + + /** + * Sets up the test with no display. + * @param string $label If no test name is given then + * the class name is used. + * @access public + */ + function SimpleTestCase($label = false) { + if ($label) { + $this->_label = $label; + } + } + + /** + * Accessor for the test name for subclasses. + * @return string Name of the test. + * @access public + */ + function getLabel() { + return $this->_label ? $this->_label : get_class($this); + } + + /** + * This is a placeholder for skipping tests. In this + * method you place skipIf() and skipUnless() calls to + * set the skipping state. + * @access public + */ + function skip() { + } + + /** + * Will issue a message to the reporter and tell the test + * case to skip if the incoming flag is true. + * @param string $should_skip Condition causing the tests to be skipped. + * @param string $message Text of skip condition. + * @access public + */ + function skipIf($should_skip, $message = '%s') { + if ($should_skip && ! $this->_should_skip) { + $this->_should_skip = true; + $message = sprintf($message, 'Skipping [' . get_class($this) . ']'); + $this->_reporter->paintSkip($message . $this->getAssertionLine()); + } + } + + /** + * Will issue a message to the reporter and tell the test + * case to skip if the incoming flag is false. + * @param string $shouldnt_skip Condition causing the tests to be run. + * @param string $message Text of skip condition. + * @access public + */ + function skipUnless($shouldnt_skip, $message = false) { + $this->skipIf(! $shouldnt_skip, $message); + } + + /** + * Used to invoke the single tests. + * @return SimpleInvoker Individual test runner. + * @access public + */ + function &createInvoker() { + $invoker = &new SimpleErrorTrappingInvoker(new SimpleInvoker($this)); + if (version_compare(phpversion(), '5') >= 0) { + $invoker = &new SimpleExceptionTrappingInvoker($invoker); + } + return $invoker; + } + + /** + * Uses reflection to run every method within itself + * starting with the string "test" unless a method + * is specified. + * @param SimpleReporter $reporter Current test reporter. + * @return boolean True if all tests passed. + * @access public + */ + function run(&$reporter) { + $context = &SimpleTest::getContext(); + $context->setTest($this); + $context->setReporter($reporter); + $this->_reporter = &$reporter; + $started = false; + foreach ($this->getTests() as $method) { + if ($reporter->shouldInvoke($this->getLabel(), $method)) { + $this->skip(); + if ($this->_should_skip) { + break; + } + if (! $started) { + $reporter->paintCaseStart($this->getLabel()); + $started = true; + } + $invoker = &$this->_reporter->createInvoker($this->createInvoker()); + $invoker->before($method); + $invoker->invoke($method); + $invoker->after($method); + } + } + if ($started) { + $reporter->paintCaseEnd($this->getLabel()); + } + unset($this->_reporter); + return $reporter->getStatus(); + } + + /** + * Gets a list of test names. Normally that will + * be all internal methods that start with the + * name "test". This method should be overridden + * if you want a different rule. + * @return array List of test names. + * @access public + */ + function getTests() { + $methods = array(); + foreach (get_class_methods(get_class($this)) as $method) { + if ($this->_isTest($method)) { + $methods[] = $method; + } + } + return $methods; + } + + /** + * Tests to see if the method is a test that should + * be run. Currently any method that starts with 'test' + * is a candidate unless it is the constructor. + * @param string $method Method name to try. + * @return boolean True if test method. + * @access protected + */ + function _isTest($method) { + if (strtolower(substr($method, 0, 4)) == 'test') { + return ! 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); + } + $this->_reporter->paintMethodEnd($method); + } + + /** + * Sets up an observer for the test end. + * @param object $observer Must have atTestEnd() + * method. + * @access public + */ + function tell(&$observer) { + $this->_observers[] = &$observer; + } + + /** + * @deprecated + */ + function pass($message = "Pass") { + 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->paintException($exception); + } + + /** + * @deprecated + */ + function signal($type, &$payload) { + if (! isset($this->_reporter)) { + trigger_error('Can only make assertions within test methods'); + } + $this->_reporter->paintSignal($type, $payload); + } + + /** + * Runs an expectation directly, for extending the + * tests with new expectation classes. + * @param SimpleExpectation $expectation Expectation subclass. + * @param mixed $compare Value to compare. + * @param string $message Message to display. + * @return boolean True on pass + * @access public + */ + function assert(&$expectation, $compare, $message = '%s') { + if ($expectation->test($compare)) { + return $this->pass(sprintf( + $message, + $expectation->overlayMessage($compare, $this->_reporter->getDumper()))); + } else { + return $this->fail(sprintf( + $message, + $expectation->overlayMessage($compare, $this->_reporter->getDumper()))); + } + } + + /** + * @deprecated + */ + function assertExpectation(&$expectation, $compare, $message = '%s') { + return $this->assert($expectation, $compare, $message); + } + + /** + * Uses a stack trace to find the line of an assertion. + * @return string Line number of first assert* + * method embedded in format string. + * @access public + */ + function getAssertionLine() { + $trace = new SimpleStackTrace(array('assert', 'expect', 'pass', 'fail', 'skip')); + return $trace->traceMethod(); + } + + /** + * 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) { + $dumper = $this->_reporter->getDumper(); + $formatted = $dumper->dump($variable); + if ($message) { + $formatted = $message . "\n" . $formatted; + } + $this->_reporter->paintFormattedMessage($formatted); + return $variable; + } + + /** + * @deprecated + */ + function sendMessage($message) { + $this->_reporter->PaintMessage($message); + } + + /** + * Accessor for the number of subtests including myelf. + * @return integer Number of test cases. + * @access public + * @static + */ + function getSize() { + return 1; + } +} + +/** + * Helps to extract test cases automatically from a file. + */ +class SimpleFileLoader { + + /** + * Builds a test suite from a library of test cases. + * The new suite is composed into this one. + * @param string $test_file File name of library with + * test case classes. + * @return TestSuite The new test suite. + * @access public + */ + function &load($test_file) { + $existing_classes = get_declared_classes(); + $existing_globals = get_defined_vars(); + include_once($test_file); + $new_globals = get_defined_vars(); + $this->_makeFileVariablesGlobal($existing_globals, $new_globals); + $new_classes = array_diff(get_declared_classes(), $existing_classes); + if (empty($new_classes)) { + $new_classes = $this->_scrapeClassesFromFile($test_file); + } + $classes = $this->selectRunnableTests($new_classes); + $suite = &$this->createSuiteFromClasses($test_file, $classes); + return $suite; + } + + /** + * Imports new variables into the global namespace. + * @param hash $existing Variables before the file was loaded. + * @param hash $new Variables after the file was loaded. + * @access private + */ + function _makeFileVariablesGlobal($existing, $new) { + $globals = array_diff(array_keys($new), array_keys($existing)); + foreach ($globals as $global) { + $_GLOBALS[$global] = $new[$global]; + } + } + + /** + * Lookup classnames from file contents, in case the + * file may have been included before. + * Note: This is probably too clever by half. Figuring this + * out after a failed test case is going to be tricky for us, + * never mind the user. A test case should not be included + * twice anyway. + * @param string $test_file File name with classes. + * @access private + */ + function _scrapeClassesFromFile($test_file) { + preg_match_all('~^\s*class\s+(\w+)(\s+(extends|implements)\s+\w+)*\s*\{~mi', + file_get_contents($test_file), + $matches ); + return $matches[1]; + } + + /** + * Calculates the incoming test cases. Skips abstract + * and ignored classes. + * @param array $candidates Candidate classes. + * @return array New classes which are test + * cases that shouldn't be ignored. + * @access public + */ + function selectRunnableTests($candidates) { + $classes = array(); + foreach ($candidates as $class) { + if (TestSuite::getBaseTestCase($class)) { + $reflection = new SimpleReflection($class); + if ($reflection->isAbstract()) { + SimpleTest::ignore($class); + } else { + $classes[] = $class; + } + } + } + return $classes; + } + + /** + * Builds a test suite from a class list. + * @param string $title Title of new group. + * @param array $classes Test classes. + * @return TestSuite Group loaded with the new + * test cases. + * @access public + */ + function &createSuiteFromClasses($title, $classes) { + if (count($classes) == 0) { + $suite = &new BadTestSuite($title, "No runnable test cases in [$title]"); + return $suite; + } + SimpleTest::ignoreParentsIfIgnored($classes); + $suite = &new TestSuite($title); + foreach ($classes as $class) { + if (! SimpleTest::isIgnored($class)) { + $suite->addTestClass($class); + } + } + return $suite; + } +} + +/** + * This is a composite test class for combining + * test cases and other RunnableTest classes into + * a group test. + * @package SimpleTest + * @subpackage UnitTester + */ +class TestSuite { + var $_label; + var $_test_cases; + + /** + * Sets the name of the test suite. + * @param string $label Name sent at the start and end + * of the test. + * @access public + */ + function TestSuite($label = false) { + $this->_label = $label; + $this->_test_cases = array(); + } + + /** + * Accessor for the test name for subclasses. If the suite + * wraps a single test case the label defaults to the name of that test. + * @return string Name of the test. + * @access public + */ + function getLabel() { + if (! $this->_label) { + return ($this->getSize() == 1) ? + get_class($this->_test_cases[0]) : get_class($this); + } else { + return $this->_label; + } + } + + /** + * @deprecated + */ + function addTestCase(&$test_case) { + $this->_test_cases[] = &$test_case; + } + + /** + * @deprecated + */ + function addTestClass($class) { + if (TestSuite::getBaseTestCase($class) == 'testsuite') { + $this->_test_cases[] = &new $class(); + } else { + $this->_test_cases[] = $class; + } + } + + /** + * Adds a test into the suite by instance or class. The class will + * be instantiated if it's a test suite. + * @param SimpleTestCase $test_case Suite or individual test + * case implementing the + * runnable test interface. + * @access public + */ + function add(&$test_case) { + if (! is_string($test_case)) { + $this->_test_cases[] = &$test_case; + } elseif (TestSuite::getBaseTestCase($class) == 'testsuite') { + $this->_test_cases[] = &new $class(); + } else { + $this->_test_cases[] = $class; + } + } + + /** + * @deprecated + */ + function addTestFile($test_file) { + $this->addFile($test_file); + } + + /** + * Builds a test suite from a library of test cases. + * The new suite is composed into this one. + * @param string $test_file File name of library with + * test case classes. + * @access public + */ + function addFile($test_file) { + $extractor = new SimpleFileLoader(); + $this->add($extractor->load($test_file)); + } + + /** + * Delegates to a visiting collector to add test + * files. + * @param string $path Path to scan from. + * @param SimpleCollector $collector Directory scanner. + * @access public + */ + function collect($path, &$collector) { + $collector->collect($this, $path); + } + + /** + * Invokes run() on all of the held test cases, instantiating + * them if necessary. + * @param SimpleReporter $reporter Current test reporter. + * @access public + */ + function run(&$reporter) { + $reporter->paintGroupStart($this->getLabel(), $this->getSize()); + for ($i = 0, $count = count($this->_test_cases); $i < $count; $i++) { + if (is_string($this->_test_cases[$i])) { + $class = $this->_test_cases[$i]; + $test = &new $class(); + $test->run($reporter); + unset($test); + } else { + $this->_test_cases[$i]->run($reporter); + } + } + $reporter->paintGroupEnd($this->getLabel()); + return $reporter->getStatus(); + } + + /** + * Number of contained test cases. + * @return integer Total count of cases in the group. + * @access public + */ + function getSize() { + $count = 0; + foreach ($this->_test_cases as $case) { + if (is_string($case)) { + if (! SimpleTest::isIgnored($case)) { + $count++; + } + } else { + $count += $case->getSize(); + } + } + return $count; + } + + /** + * Test to see if a class is derived from the + * SimpleTestCase class. + * @param string $class Class name. + * @access public + * @static + */ + function getBaseTestCase($class) { + while ($class = get_parent_class($class)) { + $class = strtolower($class); + if ($class == 'simpletestcase' || $class == 'testsuite') { + return $class; + } + } + return false; + } +} + +/** + * @package SimpleTest + * @subpackage UnitTester + * @deprecated + */ +class GroupTest extends TestSuite { } + +/** + * This is a failing group test for when a test suite hasn't + * loaded properly. + * @package SimpleTest + * @subpackage UnitTester + */ +class BadTestSuite { + var $_label; + var $_error; + + /** + * Sets the name of the test suite and error message. + * @param string $label Name sent at the start and end + * of the test. + * @access public + */ + function BadTestSuite($label, $error) { + $this->_label = $label; + $this->_error = $error; + } + + /** + * Accessor for the test name for subclasses. + * @return string Name of the test. + * @access public + */ + function getLabel() { + return $this->_label; + } + + /** + * Sends a single error to the reporter. + * @param SimpleReporter $reporter Current test reporter. + * @access public + */ + function run(&$reporter) { + $reporter->paintGroupStart($this->getLabel(), $this->getSize()); + $reporter->paintFail('Bad TestSuite [' . $this->getLabel() . + '] with error [' . $this->_error . ']'); + $reporter->paintGroupEnd($this->getLabel()); + return $reporter->getStatus(); + } + + /** + * Number of contained test cases. Always zero. + * @return integer Total count of cases in the group. + * @access public + */ + function getSize() { + return 0; + } +} + +/** + * @package SimpleTest + * @subpackage UnitTester + * @deprecated + */ +class BadGroupTest extends BadTestSuite { } +?> diff --git a/contrib/simpletest/simpletest/unit_tester.php b/contrib/simpletest/simpletest/unit_tester.php new file mode 100644 index 00000000..8bb757d4 --- /dev/null +++ b/contrib/simpletest/simpletest/unit_tester.php @@ -0,0 +1,420 @@ +SimpleTestCase($label); + } + + /** + * Called from within the test methods to register + * passes and failures. + * @param boolean $result Pass on true. + * @param string $message Message to display describing + * the test state. + * @return boolean True on pass + * @access public + */ + function assertTrue($result, $message = false) { + return $this->assert(new TrueExpectation(), $result, $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 = '%s') { + return $this->assert(new FalseExpectation(), $result, $message); + } + + /** + * 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); + } + + /** + * @deprecated + */ + function swallowErrors() { + $context = &SimpleTest::getContext(); + $queue = &$context->get('SimpleErrorQueue'); + $queue->clear(); + } + + /** + * @deprecated + */ + function assertNoErrors($message = '%s') { + $context = &SimpleTest::getContext(); + $queue = &$context->get('SimpleErrorQueue'); + return $queue->assertNoErrors($message); + } + + /** + * @deprecated + */ + function assertError($expected = false, $message = '%s') { + $context = &SimpleTest::getContext(); + $queue = &$context->get('SimpleErrorQueue'); + return $queue->assertError($this->_coerceExpectation($expected), $message); + } + + /** + * Prepares for an error. If the error mismatches it + * passes through, otherwise it is swallowed. Any + * left over errors trigger failures. + * @param SimpleExpectation/string $expected The error to match. + * @param string $message Message on failure. + * @access public + */ + function expectError($expected = false, $message = '%s') { + $context = &SimpleTest::getContext(); + $queue = &$context->get('SimpleErrorQueue'); + $queue->expectError($this->_coerceExpectation($expected), $message); + } + + /** + * Prepares for an exception. If the error mismatches it + * passes through, otherwise it is swallowed. Any + * left over errors trigger failures. + * @param SimpleExpectation/Exception $expected The error to match. + * @param string $message Message on failure. + * @access public + */ + function expectException($expected = false, $message = '%s') { + $context = &SimpleTest::getContext(); + $queue = &$context->get('SimpleExceptionTrap'); + // :HACK: Directly substituting in seems to cause a segfault with + // Zend Optimizer on some systems + $line = $this->getAssertionLine(); + $queue->expectException($expected, $message . $line); + } + + /** + * 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 _coerceExpectation($expected) { + if ($expected == false) { + return new TrueExpectation(); + } + if (SimpleTestCompatibility::isA($expected, 'SimpleExpectation')) { + return $expected; + } + return new EqualExpectation( + is_string($expected) ? str_replace('%', '%%', $expected) : $expected); + } + + /** + * @deprecated + */ + function assertErrorPattern($pattern, $message = '%s') { + return $this->assertError(new PatternExpectation($pattern), $message); + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/url.php b/contrib/simpletest/simpletest/url.php new file mode 100644 index 00000000..0ea22040 --- /dev/null +++ b/contrib/simpletest/simpletest/url.php @@ -0,0 +1,528 @@ +_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() { + $path = $this->_path; + $scheme = $identity = $host = $encoded = $fragment = ''; + if ($this->_username && $this->_password) { + $identity = $this->_username . ':' . $this->_password . '@'; + } + if ($this->getHost()) { + $scheme = $this->getScheme() ? $this->getScheme() : 'http'; + $scheme .= "://"; + $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); + } + if ($this->getHost()) { + $scheme = $this->getScheme(); + $host = $this->getHost(); + $port = $this->getPort() ? ':' . $this->getPort() : ''; + $identity = $this->getIdentity() ? $this->getIdentity() . '@' : ''; + if (! $identity) { + $identity = $base->getIdentity() ? $base->getIdentity() . '@' : ''; + } + } else { + $scheme = $base->getScheme(); + $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/contrib/simpletest/simpletest/user_agent.php b/contrib/simpletest/simpletest/user_agent.php new file mode 100644 index 00000000..b3f6f057 --- /dev/null +++ b/contrib/simpletest/simpletest/user_agent.php @@ -0,0 +1,332 @@ +_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/contrib/simpletest/simpletest/web_tester.php b/contrib/simpletest/simpletest/web_tester.php new file mode 100644 index 00000000..40b16129 --- /dev/null +++ b/contrib/simpletest/simpletest/web_tester.php @@ -0,0 +1,1541 @@ +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 [" . + $dumper->describeValue($compare) . "] " . + $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->overlayMessage($compare, $this->_getDumper()); + } 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)); + } + + /** + * Checks for a click target. + * @param string $label Visible text or alt text. + * @return boolean True if click target. + * @access public + */ + function assertClickable($label, $message = '%s') { + return $this->assertTrue( + $this->_browser->isClickable($label), + sprintf($message, "Click target [$label] should exist")); + } + + /** + * 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)); + } + + /** + * Checks for a valid button label. + * @param string $label Visible text. + * @return boolean True if click target. + * @access public + */ + function assertSubmit($label, $message = '%s') { + return $this->assertTrue( + $this->_browser->isSubmit($label), + sprintf($message, "Submit button [$label] should exist")); + } + + /** + * 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)); + } + + /** + * Checks for a valid image with atht alt text or title. + * @param string $label Visible text. + * @return boolean True if click target. + * @access public + */ + function assertImage($label, $message = '%s') { + return $this->assertTrue( + $this->_browser->isImage($label), + sprintf($message, "Image with text [$label] should exist")); + } + + /** + * 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)); + } + + /** + * Tests for the presence of a link label. Match is + * case insensitive with normalised space. + * @param string $label Text between the anchor tags. + * @param mixed $expected Expected URL or expectation object. + * @param string $message Message to display. Default + * can be embedded with %s. + * @return boolean True if link present. + * @access public + */ + function assertLink($label, $expected = true, $message = '%s') { + $url = $this->_browser->getLink($label); + if ($expected === true || ($expected !== true && $url === false)) { + return $this->assertTrue($url !== false, sprintf($message, "Link [$label] should exist")); + } + if (! SimpleExpectation::isExpectation($expected)) { + $expected = new IdenticalExpectation($expected); + } + return $this->assert($expected, $url->asString(), sprintf($message, "Link [$label] should match")); + } + + /** + * 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->assertTrue( + $this->_browser->getLink($label) === false, + sprintf($message, "Link [$label] should not exist")); + } + + /** + * Tests for the presence of a link id attribute. + * @param string $id Id attribute value. + * @param mixed $expected Expected URL or expectation object. + * @param string $message Message to display. Default + * can be embedded with %s. + * @return boolean True if link present. + * @access public + */ + function assertLinkById($id, $expected = true, $message = '%s') { + $url = $this->_browser->getLinkById($id); + if ($expected === true) { + return $this->assertTrue($url !== false, sprintf($message, "Link ID [$id] should exist")); + } + if (! SimpleExpectation::isExpectation($expected)) { + $expected = new IdenticalExpectation($expected); + } + return $this->assert($expected, $url->asString(), sprintf($message, "Link ID [$id] should match")); + } + + /** + * 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->assertTrue( + $this->_browser->getLinkById($id) === false, + 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, $position=false) { + return $this->_browser->setField($label, $value, $position); + } + + /** + * 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, $position=false) { + return $this->_browser->setFieldByName($name, $value, $position); + } + + /** + * Sets all form fields with that id. + * @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/SimpleExpectation $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]")); + } + + /** + * 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) { + return $this->assert(new TrueExpectation(), $result, $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 = '%s') { + return $this->assert(new FalseExpectation(), $result, $message); + } + + /** + * 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); + } + + /** + * Uses a stack trace to find the line of an assertion. + * @return string Line number of first assert* + * method embedded in format string. + * @access public + */ + function getAssertionLine() { + $trace = new SimpleStackTrace(array('assert', 'click', 'pass', 'fail')); + return $trace->traceMethod(); + } +} +?> \ No newline at end of file diff --git a/contrib/simpletest/simpletest/xml.php b/contrib/simpletest/simpletest/xml.php new file mode 100644 index 00000000..1666cb93 --- /dev/null +++ b/contrib/simpletest/simpletest/xml.php @@ -0,0 +1,647 @@ +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('&', '<', '>', '"', '''), + $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) . + "_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 "_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) . + "_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 "_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) . + "_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 "_namespace . "test>\n"; + parent::paintMethodEnd($test_name); + } + + /** + * Paints pass as XML. + * @param string $message Message to encode. + * @access public + */ + function paintPass($message) { + parent::paintPass($message); + print $this->_getIndent(1); + print "<" . $this->_namespace . "pass>"; + print $this->toParsedXml($message); + print "_namespace . "pass>\n"; + } + + /** + * Paints failure as XML. + * @param string $message Message to encode. + * @access public + */ + function paintFail($message) { + parent::paintFail($message); + print $this->_getIndent(1); + print "<" . $this->_namespace . "fail>"; + print $this->toParsedXml($message); + print "_namespace . "fail>\n"; + } + + /** + * Paints error as XML. + * @param string $message Message to encode. + * @access public + */ + function paintError($message) { + parent::paintError($message); + print $this->_getIndent(1); + print "<" . $this->_namespace . "exception>"; + print $this->toParsedXml($message); + print "_namespace . "exception>\n"; + } + + /** + * Paints exception as XML. + * @param Exception $exception Exception to encode. + * @access public + */ + function paintException($exception) { + parent::paintException($exception); + print $this->_getIndent(1); + print "<" . $this->_namespace . "exception>"; + $message = 'Unexpected exception of type [' . get_class($exception) . + '] with message ['. $exception->getMessage() . + '] in ['. $exception->getFile() . + ' line ' . $exception->getLine() . ']'; + print $this->toParsedXml($message); + print "_namespace . "exception>\n"; + } + + /** + * Paints the skipping message and tag. + * @param string $message Text to display in skip tag. + * @access public + */ + function paintSkip($message) { + parent::paintSkip($message); + print $this->_getIndent(1); + print "<" . $this->_namespace . "skip>"; + print $this->toParsedXml($message); + print "_namespace . "skip>\n"; + } + + /** + * Paints a simple supplementary message. + * @param string $message Text to display. + * @access public + */ + function paintMessage($message) { + parent::paintMessage($message); + print $this->_getIndent(1); + print "<" . $this->_namespace . "message>"; + print $this->toParsedXml($message); + print "_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 ""; + print "_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 ""; + print "_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 "_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 "_namespace . "run>\n"; + } +} + +/** + * Accumulator for incoming tag. Holds the + * incoming test structure information for + * later dispatch to the reporter. + * @package SimpleTest + * @subpackage UnitTester + */ +class NestingXmlTag { + var $_name; + var $_attributes; + + /** + * Sets the basic test information except + * the name. + * @param hash $attributes Name value pairs. + * @access public + */ + function NestingXmlTag($attributes) { + $this->_name = false; + $this->_attributes = $attributes; + } + + /** + * Sets the test case/method name. + * @param string $name Name of test. + * @access public + */ + function setName($name) { + $this->_name = $name; + } + + /** + * Accessor for name. + * @return string Name of test. + * @access public + */ + function getName() { + return $this->_name; + } + + /** + * Accessor for attributes. + * @return hash All attributes. + * @access protected + */ + function _getAttributes() { + return $this->_attributes; + } +} + +/** + * Accumulator for incoming method tag. Holds the + * incoming test structure information for + * later dispatch to the reporter. + * @package SimpleTest + * @subpackage UnitTester + */ +class NestingMethodTag extends NestingXmlTag { + + /** + * Sets the basic test information except + * the name. + * @param hash $attributes Name value pairs. + * @access public + */ + function NestingMethodTag($attributes) { + $this->NestingXmlTag($attributes); + } + + /** + * Signals the appropriate start event on the + * listener. + * @param SimpleReporter $listener Target for events. + * @access public + */ + function paintStart(&$listener) { + $listener->paintMethodStart($this->getName()); + } + + /** + * Signals the appropriate end event on the + * listener. + * @param SimpleReporter $listener Target for events. + * @access public + */ + function paintEnd(&$listener) { + $listener->paintMethodEnd($this->getName()); + } +} + +/** + * Accumulator for incoming case tag. Holds the + * incoming test structure information for + * later dispatch to the reporter. + * @package SimpleTest + * @subpackage UnitTester + */ +class NestingCaseTag extends NestingXmlTag { + + /** + * Sets the basic test information except + * the name. + * @param hash $attributes Name value pairs. + * @access public + */ + function NestingCaseTag($attributes) { + $this->NestingXmlTag($attributes); + } + + /** + * Signals the appropriate start event on the + * listener. + * @param SimpleReporter $listener Target for events. + * @access public + */ + function paintStart(&$listener) { + $listener->paintCaseStart($this->getName()); + } + + /** + * Signals the appropriate end event on the + * listener. + * @param SimpleReporter $listener Target for events. + * @access public + */ + function paintEnd(&$listener) { + $listener->paintCaseEnd($this->getName()); + } +} + +/** + * Accumulator for incoming group tag. Holds the + * incoming test structure information for + * later dispatch to the reporter. + * @package SimpleTest + * @subpackage UnitTester + */ +class NestingGroupTag extends NestingXmlTag { + + /** + * Sets the basic test information except + * the name. + * @param hash $attributes Name value pairs. + * @access public + */ + function NestingGroupTag($attributes) { + $this->NestingXmlTag($attributes); + } + + /** + * Signals the appropriate start event on the + * listener. + * @param SimpleReporter $listener Target for events. + * @access public + */ + function paintStart(&$listener) { + $listener->paintGroupStart($this->getName(), $this->getSize()); + } + + /** + * Signals the appropriate end event on the + * listener. + * @param SimpleReporter $listener Target for events. + * @access public + */ + function paintEnd(&$listener) { + $listener->paintGroupEnd($this->getName()); + } + + /** + * The size in the attributes. + * @return integer Value of size attribute or zero. + * @access public + */ + function getSize() { + $attributes = $this->_getAttributes(); + if (isset($attributes['SIZE'])) { + return (integer)$attributes['SIZE']; + } + return 0; + } +} + +/** + * Parser for importing the output of the XmlReporter. + * Dispatches that output to another reporter. + * @package SimpleTest + * @subpackage UnitTester + */ +class SimpleTestXmlParser { + var $_listener; + var $_expat; + var $_tag_stack; + var $_in_content_tag; + var $_content; + var $_attributes; + + /** + * Loads a listener with the SimpleReporter + * interface. + * @param SimpleReporter $listener Listener of tag events. + * @access public + */ + function SimpleTestXmlParser(&$listener) { + $this->_listener = &$listener; + $this->_expat = &$this->_createParser(); + $this->_tag_stack = array(); + $this->_in_content_tag = false; + $this->_content = ''; + $this->_attributes = array(); + } + + /** + * Parses a block of XML sending the results to + * the listener. + * @param string $chunk Block of text to read. + * @return boolean True if valid XML. + * @access public + */ + function parse($chunk) { + if (! xml_parse($this->_expat, $chunk)) { + trigger_error('XML parse error with ' . + xml_error_string(xml_get_error_code($this->_expat))); + return false; + } + return true; + } + + /** + * Sets up expat as the XML parser. + * @return resource Expat handle. + * @access protected + */ + function &_createParser() { + $expat = xml_parser_create(); + xml_set_object($expat, $this); + xml_set_element_handler($expat, '_startElement', '_endElement'); + xml_set_character_data_handler($expat, '_addContent'); + xml_set_default_handler($expat, '_default'); + return $expat; + } + + /** + * Opens a new test nesting level. + * @return NestedXmlTag The group, case or method tag + * to start. + * @access private + */ + function _pushNestingTag($nested) { + array_unshift($this->_tag_stack, $nested); + } + + /** + * Accessor for current test structure tag. + * @return NestedXmlTag The group, case or method tag + * being parsed. + * @access private + */ + function &_getCurrentNestingTag() { + return $this->_tag_stack[0]; + } + + /** + * Ends a nesting tag. + * @return NestedXmlTag The group, case or method tag + * just finished. + * @access private + */ + function _popNestingTag() { + return array_shift($this->_tag_stack); + } + + /** + * Test if tag is a leaf node with only text content. + * @param string $tag XML tag name. + * @return @boolean True if leaf, false if nesting. + * @private + */ + function _isLeaf($tag) { + return in_array($tag, array( + 'NAME', 'PASS', 'FAIL', 'EXCEPTION', 'SKIP', 'MESSAGE', 'FORMATTED', 'SIGNAL')); + } + + /** + * Handler for start of event element. + * @param resource $expat Parser handle. + * @param string $tag Element name. + * @param hash $attributes Name value pairs. + * Attributes without content + * are marked as true. + * @access protected + */ + function _startElement($expat, $tag, $attributes) { + $this->_attributes = $attributes; + if ($tag == 'GROUP') { + $this->_pushNestingTag(new NestingGroupTag($attributes)); + } elseif ($tag == 'CASE') { + $this->_pushNestingTag(new NestingCaseTag($attributes)); + } elseif ($tag == 'TEST') { + $this->_pushNestingTag(new NestingMethodTag($attributes)); + } elseif ($this->_isLeaf($tag)) { + $this->_in_content_tag = true; + $this->_content = ''; + } + } + + /** + * End of element event. + * @param resource $expat Parser handle. + * @param string $tag Element name. + * @access protected + */ + function _endElement($expat, $tag) { + $this->_in_content_tag = false; + if (in_array($tag, array('GROUP', 'CASE', 'TEST'))) { + $nesting_tag = $this->_popNestingTag(); + $nesting_tag->paintEnd($this->_listener); + } elseif ($tag == 'NAME') { + $nesting_tag = &$this->_getCurrentNestingTag(); + $nesting_tag->setName($this->_content); + $nesting_tag->paintStart($this->_listener); + } elseif ($tag == 'PASS') { + $this->_listener->paintPass($this->_content); + } elseif ($tag == 'FAIL') { + $this->_listener->paintFail($this->_content); + } elseif ($tag == 'EXCEPTION') { + $this->_listener->paintError($this->_content); + } elseif ($tag == 'SKIP') { + $this->_listener->paintSkip($this->_content); + } elseif ($tag == 'SIGNAL') { + $this->_listener->paintSignal( + $this->_attributes['TYPE'], + unserialize($this->_content)); + } elseif ($tag == 'MESSAGE') { + $this->_listener->paintMessage($this->_content); + } elseif ($tag == 'FORMATTED') { + $this->_listener->paintFormattedMessage($this->_content); + } + } + + /** + * Content between start and end elements. + * @param resource $expat Parser handle. + * @param string $text Usually output messages. + * @access protected + */ + function _addContent($expat, $text) { + if ($this->_in_content_tag) { + $this->_content .= $text; + } + return true; + } + + /** + * XML and Doctype handler. Discards all such content. + * @param resource $expat Parser handle. + * @param string $default Text of default content. + * @access protected + */ + function _default($expat, $default) { + } +} +?> diff --git a/contrib/simpletest/theme.php b/contrib/simpletest/theme.php new file mode 100644 index 00000000..c8879bd7 --- /dev/null +++ b/contrib/simpletest/theme.php @@ -0,0 +1,52 @@ +page = $page; + } + + function paintHeader($test_name) { + // nowt + //parent::paintHeader($test_name); + } + + function paintFooter($test_name) { + //parent::paintFooter($test_name); + $html = "". + $this->getPassCount() . " passes, " . + $this->getFailCount() . " failures" . + "
Passed modules: " . $this->clear_modules; + $this->page->add_block(new Block("Results", $html, "main", 40)); + } + + function paintGroupStart($name, $size) { + parent::paintGroupStart($name, $size); + $this->current_html = ""; + } + + function paintGroupEnd($name) { + $name = substr($name, 4, strlen($name)-13); + parent::paintGroupEnd($name); + if($this->current_html == "") { + $this->clear_modules .= "$name, "; + } + else { + $this->page->add_block(new Block($name, $this->current_html, "main", 50)); + } + } + + function paintFail($message) { + //parent::paintFail($message); + $this->_fails++; // manually do the grandparent behaviour + + $message = str_replace(getcwd(), "...", $message); + $this->current_html .= "

Fail: $message"; + } +} +?>