";
-
- echo ' Testing updatingNodeAttribute with no insert (should be ok and change the item "links" into "zelda" haha) ';
- if($xmldbtest->updateNodeAttribute('table1', array('id', 'links'), array('id', 'zelda')))
- echo "ok
";
- else
- echo "ko
";
-
- echo ' Testing updateNodeValue via attribute (should be ok - inserting "booga!" into the item named "notes") ';
- if($xmldbtest->updateNodeValue('table1', array('id', 'notes'), null, 'booga!'))
- echo "ok
";
- else
- echo "ko
";
-
- echo ' Testing deleteNode via pk (should be ok - deleting the item clock into table1) ';
- if($xmldbtest->deleteNode('table1', 'clock', null))
- echo "ok
";
- else
- echo "ko
";
-
- }else{
- exit("can't load config.xml either");
- }
}
+$test = new TestOfLogging;
+$test->test();
+
+
diff --git a/log.php b/log.php
new file mode 100644
index 0000000..1591dcf
--- /dev/null
+++ b/log.php
@@ -0,0 +1,23 @@
+
+class Log {
+
+ protected $logfile;
+
+ function __construct($filename) {
+ $file = $filename;
+ $this->logfile = fopen($file, 'a+');
+ $this->message('Starting log');
+ }
+
+ function message($message) {
+ $message = '['. date("Y-m-d / H:i:s") . ']'.' - '.$message;
+ $message .= "\n";
+ return fwrite( $this->logfile, $message );
+ }
+
+ function __destruct(){
+ $this->message('Finishing log');
+ return fclose( $this->logfile );
+ }
+}
+
diff --git a/simpletest/HELP_MY_TESTS_DONT_WORK_ANYMORE b/simpletest/HELP_MY_TESTS_DONT_WORK_ANYMORE
new file mode 100644
index 0000000..8ac9cf2
--- /dev/null
+++ b/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/simpletest/LICENSE b/simpletest/LICENSE
new file mode 100644
index 0000000..09f465a
--- /dev/null
+++ b/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/simpletest/README b/simpletest/README
new file mode 100644
index 0000000..c52e3f7
--- /dev/null
+++ b/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/simpletest/VERSION b/simpletest/VERSION
new file mode 100644
index 0000000..7f20734
--- /dev/null
+++ b/simpletest/VERSION
@@ -0,0 +1 @@
+1.0.1
\ No newline at end of file
diff --git a/simpletest/authentication.php b/simpletest/authentication.php
new file mode 100644
index 0000000..c56d11b
--- /dev/null
+++ b/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/simpletest/autorun.php b/simpletest/autorun.php
new file mode 100644
index 0000000..7d97d2d
--- /dev/null
+++ b/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/simpletest/browser.php b/simpletest/browser.php
new file mode 100644
index 0000000..e2a1fe1
--- /dev/null
+++ b/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/simpletest/collector.php b/simpletest/collector.php
new file mode 100644
index 0000000..5b8255d
--- /dev/null
+++ b/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/simpletest/compatibility.php b/simpletest/compatibility.php
new file mode 100644
index 0000000..4e0f78a
--- /dev/null
+++ b/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/simpletest/cookies.php b/simpletest/cookies.php
new file mode 100644
index 0000000..ed1c025
--- /dev/null
+++ b/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/simpletest/default_reporter.php b/simpletest/default_reporter.php
new file mode 100644
index 0000000..bd4c6a1
--- /dev/null
+++ b/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/simpletest/detached.php b/simpletest/detached.php
new file mode 100644
index 0000000..e323d8c
--- /dev/null
+++ b/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/simpletest/docs/en/authentication_documentation.html b/simpletest/docs/en/authentication_documentation.html
new file mode 100644
index 0000000..8da2a7f
--- /dev/null
+++ b/simpletest/docs/en/authentication_documentation.html
@@ -0,0 +1,355 @@
+
+
+
+SimpleTest documentation for testing log-in and authentication
+
+
+
+
+ One of the trickiest, and yet most important, areas
+ of testing web sites is the security.
+ Testing these schemes is one of the core goals of
+ the SimpleTest web tester.
+
+ If you fetch a page protected by basic authentication then
+ rather than receiving content, you will instead get a 401
+ header.
+ We can illustrate this with this test...
+
1/1 test cases complete.
+ 0 passes, 0 fails and 0 exceptions.
+
+ We are trying to get away from visual inspection though, and so SimpleTest
+ allows to make automated assertions against the challenge.
+ Here is a thorough test of our header...
+
+ Any one of these tests would normally do on it's own depending
+ on the amount of detail you want to see.
+
+
+ One theme that runs through SimpleTest is the ability to use
+ SimpleExpectation objects wherever a simple
+ match is not enough.
+ If you want only an approximate match to the realm for
+ example, you can do this...
+
+ Most of the time we are not interested in testing the
+ authentication itself, but want to get past it to test
+ the pages underneath.
+ As soon as the challenge has been issued we can reply with
+ an authentication response...
+
+ The username and password will now be sent with every
+ subsequent request to that directory and subdirectories.
+ You will have to authenticate again if you step outside
+ the authenticated directory, but SimpleTest is smart enough
+ to merge subdirectories into a common realm.
+
+
+ You can shortcut this step further by encoding the log in
+ details straight into the URL...
+
+ If your username or password has special characters, then you
+ will have to URL encode them or the request will not be parsed
+ correctly.
+ Also this header will not be sent on subsequent requests if
+ you request a page with a fully qualified URL.
+ If you navigate with relative URLs though, the authentication
+ information will be preserved.
+
+
+ Only basic authentication is currently supported and this is
+ only really secure in tandem with HTTPS connections.
+ This is usually enough to protect test server from prying eyes,
+ however.
+ Digest authentication and NTLM authentication may be added
+ in the future.
+
+ Basic authentication doesn't give enough control over the
+ user interface for web developers.
+ More likely this functionality will be coded directly into
+ the web architecture using cookies and complicated timeouts.
+
+ Let's suppose that in fetching this page a cookie has been
+ set with a session ID.
+ We are not going to fill the form in yet, just test that
+ we are tracking the user.
+ Here is the test...
+
+ All we are doing is confirming that the cookie is set.
+ As the value is likely to be rather cryptic it's not
+ really worth testing this with...
+
+class LogInTest extends WebTestCase {
+ function testSessionCookieIsCorrectPattern() {
+ $this->get('http://www.my-site.com/login.php');
+ $this->assertCookie('SID', new PatternExpectation('/[a-f0-9]{32}/i'));
+ }
+}
+
+ The rest of the test would be the same as any other form,
+ but we might want to confirm that we still have the same
+ cookie after log-in as before we entered.
+ We wouldn't want to lose track of this after all.
+ Here is a possible test for this...
+
+ If you are testing an authentication system a critical piece
+ of behaviour is what happens when a user logs back in.
+ We would like to simulate closing and reopening a browser...
+
+ The WebTestCase::restart() method will
+ preserve cookies that have unexpired timeouts, but throw away
+ those that are temporary or expired.
+ You can optionally specify the time and date that the restart
+ happened.
+
+
+ Expiring cookies can be a problem.
+ After all, if you have a cookie that expires after an hour,
+ you don't want to stall the test for an hour while the
+ cookie passes it's timeout.
+
+
+ To push the cookies over the hour limit you can age them
+ before you restart the session...
+
+ SimpleTest's web browser component can be used not just
+ outside of the WebTestCase class, but also
+ independently of the SimpleTest framework itself.
+
+ You can use the web browser in PHP scripts to confirm
+ services are up and running, or to extract information
+ from them at a regular basis.
+ For example, here is a small script to extract the current number of
+ open PHP 5 bugs from the PHP web site...
+
+ There are simpler methods to do this particular example in PHP
+ of course.
+ For example you can just use the PHP file()
+ command against what here is a pretty fixed page.
+ However, using the web browser for scripts allows authentication,
+ correct handling of cookies, automatic loading of frames, redirects,
+ form submission and the ability to examine the page headers.
+ Such methods are fragile against a site that is constantly
+ evolving and you would want a more direct way of accessing
+ data in a permanent set up, but for simple tasks this can provide
+ a very rapid solution.
+
+
+ All of the navigation methods used in the
+ WebTestCase
+ are present in the SimpleBrowser class, but
+ the assertions are replaced with simpler accessors.
+ Here is a full list of the page navigation methods...
+
+
+
addHeader($header)
+
Adds a header to every fetch
+
+
+
useProxy($proxy, $username, $password)
+
Use this proxy from now on
+
+
+
head($url, $parameters)
+
Perform a HEAD request
+
+
+
get($url, $parameters)
+
Fetch a page with GET
+
+
+
post($url, $parameters)
+
Fetch a page with POST
+
+
+
clickLink($label)
+
Follows a link by label
+
+
+
clickLinkById($id)
+
Follows a link by attribute
+
+
+
getUrl()
+
Current URL of page or frame
+
+
+
getTitle()
+
Page title
+
+
+
getContent()
+
Raw page or frame
+
+
+
getContentAsText()
+
HTML removed except for alt text
+
+
+
retry()
+
Repeat the last request
+
+
+
back()
+
Use the browser back button
+
+
+
forward()
+
Use the browser forward button
+
+
+
authenticate($username, $password)
+
Retry page or frame after a 401 response
+
+
+
restart($date)
+
Restarts the browser for a new session
+
+
+
ageCookies($interval)
+
Ages the cookies by the specified time
+
+
+
setCookie($name, $value)
+
Sets an additional cookie
+
+
+
getCookieValue($host, $path, $name)
+
Reads the most specific cookie
+
+
+
getCurrentCookieValue($name)
+
Reads cookie for the current context
+
+
+ The methods SimpleBrowser::useProxy() and
+ SimpleBrowser::addHeader() are special.
+ Once called they continue to apply to all subsequent fetches.
+
+
Clicks an input tag of type image by title or alt text
+
+
+
clickImageByName($name, $x, $y)
+
Clicks an input tag of type image by name
+
+
+
clickImageById($id, $x, $y)
+
Clicks an input tag of type image by ID attribute
+
+
+
submitFormById($id)
+
Submits by the form tag attribute
+
+
+ At the moment there aren't any methods to list available forms
+ and fields.
+ This will probably be added to later versions of SimpleTest.
+
+
+ Within a page, individual frames can be selected.
+ If no selection is made then all the frames are merged together
+ in one large conceptual page.
+ The content of the current page will be a concatenation of all of the
+ frames in the order that they were specified in the "frameset"
+ tags.
+
+
+
getFrames()
+
A dump of the current frame structure
+
+
+
getFrameFocus()
+
Current frame label or index
+
+
+
setFrameFocusByIndex($choice)
+
Select a frame numbered from 1
+
+
+
setFrameFocus($name)
+
Select frame by label
+
+
+
clearFrameFocus()
+
Treat all the frames as a single page
+
+
+ When focused on a single frame, the content will come from
+ that frame only.
+ This includes links to click and forms to submit.
+
+
+
+ All of this functionality is great when we actually manage to fetch pages,
+ but that doesn't always happen.
+ To help figure out what went wrong, the browser has some methods to
+ aid in debugging...
+
+
+
setConnectionTimeout($timeout)
+
Close the socket on overrun
+
+
+
getRequest()
+
Raw request header of page or frame
+
+
+
getHeaders()
+
Raw response header of page or frame
+
+
+
getTransportError()
+
Any socket level errors in the last fetch
+
+
+
getResponseCode()
+
HTTP response of page or frame
+
+
+
getMimeType()
+
Mime type of page or frame
+
+
+
getAuthentication()
+
Authentication type in 401 challenge header
+
+
+
getRealm()
+
Authentication realm in 401 challenge header
+
+
+
setMaximumRedirects($max)
+
Number of redirects before page is loaded anyway
+
+
+
setMaximumNestedFrames($max)
+
Protection against recursive framesets
+
+
+
ignoreFrames()
+
Disables frames support
+
+
+
useFrames()
+
Enables frames support
+
+
+
ignoreCookies()
+
Disables sending and receiving of cookies
+
+
+
useCookies()
+
Enables cookie support
+
+
+ The methods SimpleBrowser::setConnectionTimeout()
+ SimpleBrowser::setMaximumRedirects(),
+ SimpleBrowser::setMaximumNestedFrames(),
+ SimpleBrowser::ignoreFrames(),
+ SimpleBrowser::useFrames(),
+ SimpleBrowser::ignoreCookies() and
+ SimpleBrowser::useCokies() continue to apply
+ to every subsequent request.
+ The other methods are frames aware.
+ This means that if you have an individual frame that is not
+ loading, navigate to it using SimpleBrowser::setFrameFocus()
+ and you can then use SimpleBrowser::getRequest(), etc to
+ see what happened.
+
+
+
+ Anything that could be done in a
+ WebTestCase can
+ now be done in a UnitTestCase.
+ This means that we can freely mix domain object testing with the
+ web interface...
+
+ While this may be a useful temporary expediency, I am not a fan
+ of this type of testing.
+ The testing has cut across application layers, make it twice as
+ likely it will need refactoring when the code changes.
+
+
+ A more useful case of where using the browser directly can be helpful
+ is where the WebTestCase cannot cope.
+ An example is where two browsers are needed at the same time.
+
+
+ For example, say we want to disallow multiple simultaneous
+ usage of a site with the same username.
+ This test case will do the job...
+
+ The default behaviour of the
+ mock objects
+ in
+ SimpleTest
+ is either an identical match on the argument or to allow any argument at all.
+ For almost all tests this is sufficient.
+ Sometimes, though, you want to weaken a test case.
+
+
+ One place where a test can be too tightly coupled is with
+ text matching.
+ Suppose we have a component that outputs a helpful error
+ message when something goes wrong.
+ You want to test that the correct error was sent, but the actual
+ text may be rather long.
+ If you test for the text exactly, then every time the exact wording
+ of the message changes, you will have to go back and edit the test suite.
+
+
+ For example, suppose we have a news service that has failed
+ to connect to its remote source.
+
+class NewsService {
+ ...
+ function publish(&$writer) {
+ if (! $this->isConnected()) {
+ $writer->write('Cannot connect to news service "' .
+ $this->_name . '" at this time. ' .
+ 'Please try again later.');
+ }
+ ...
+ }
+}
+
+ Here it is sending its content to a
+ Writer class.
+ We could test this behaviour with a
+ MockWriter like so...
+
+class TestOfNewsService extends UnitTestCase {
+ ...
+ function testConnectionFailure() {
+ $writer = &new MockWriter();
+ $writer->expectOnce('write', array(
+ 'Cannot connect to news service ' .
+ '"BBC News" at this time. ' .
+ 'Please try again later.'));
+
+ $service = &new NewsService('BBC News');
+ $service->publish($writer);
+ }
+}
+
+ This is a good example of a brittle test.
+ If we decide to add additional instructions, such as
+ suggesting an alternative news source, we will break
+ our tests even though no underlying functionality
+ has been altered.
+
+
+ To get around this, we would like to do a regular expression
+ test rather than an exact match.
+ We can actually do this with...
+
+ Instead of passing in the expected parameter to the
+ MockWriter we pass an
+ expectation class called
+ WantedPatternExpectation.
+ The mock object is smart enough to recognise this as special
+ and to treat it differently.
+ Rather than simply comparing the incoming argument to this
+ object, it uses the expectation object itself to
+ perform the test.
+
+
+ The WantedPatternExpectation takes
+ the regular expression to match in its constructor.
+ Whenever a comparison is made by the MockWriter
+ against this expectation class, it will do a
+ preg_match() with this pattern.
+ With our test case above, as long as "cannot connect"
+ appears in the text of the string, the mock will issue a pass
+ to the unit tester.
+ The rest of the text does not matter.
+
+
+ The possible expectation classes are...
+
+
+
AnythingExpectation
+
Will always match
+
+
+
EqualExpectation
+
An equality, rather than the stronger identity comparison
+
+
+
NotEqualExpectation
+
An inequality comparison
+
+
+
IndenticalExpectation
+
The default mock object check which must match exactly
+
+
+
NotIndenticalExpectation
+
Inverts the mock object logic
+
+
+
WithinMarginExpectation
+
Compares a value to within a margin
+
+
+
OutsideMarginExpectation
+
Checks that a value is out side the margin
+
+
+
PatternExpectation
+
Uses a Perl Regex to match a string
+
+
+
NoPatternExpectation
+
Passes only if failing a Perl Regex
+
+
+
IsAExpectation
+
Checks the type or class name only
+
+
+
NotAExpectation
+
Opposite of the IsAExpectation
+
+
+
+
MethodExistsExpectation
+
Checks a method is available on an object
+
+
+ Most take the expected value in the constructor.
+ The exceptions are the pattern matchers, which take a regular expression,
+ and the IsAExpectation and NotAExpectation which takes a type
+ or class name as a string.
+
+
+ This is different from the previous version in that the string
+ "14" as a parameter will also pass.
+ Sometimes the additional type checks of SimpleTest are too restrictive.
+
+ The expectation classes can be used not just for sending assertions
+ from mock objects, but also for selecting behaviour for the
+ mock objects.
+ Anywhere a list of arguments is given, a list of expectation objects
+ can be inserted instead.
+
+
+ Suppose we want a mock authorisation server to simulate a successful login,
+ but only if it receives a valid session object.
+ We can do this as follows...
+
+Mock::generate('Authorisation');
+
+$authorisation = new MockAuthorisation();
+$authorisation->setReturnValue(
+ 'isAllowed',
+ true,
+ array(new IsAExpectation('Session', 'Must be a session')));
+$authorisation->setReturnValue('isAllowed', false);
+
+ We have set the default mock behaviour to return false when
+ isAllowed is called.
+ When we call the method with a single parameter that
+ is a Session object, it will return true.
+ We have also added a second parameter as a message.
+ This will be displayed as part of the mock object
+ failure message if this expectation is the cause of
+ a failure.
+
+
+ This kind of sophistication is rarely useful, but is included for
+ completeness.
+
+ The expectation classes have a very simple structure.
+ So simple that it is easy to create your own versions for
+ commonly used test logic.
+
+
+ As an example here is the creation of a class to test for
+ valid IP addresses.
+ In order to work correctly with the stubs and mocks the new
+ expectation class should extend
+ SimpleExpectation...
+
+class ValidIp extends SimpleExpectation {
+
+ function test($ip) {
+ return (ip2long($ip) != -1);
+ }
+
+ function testMessage($ip) {
+ return "Address [$ip] should be a valid IP address";
+ }
+}
+
+ There are only two methods to implement.
+ The test() method should
+ evaluate to true if the expectation is to pass, and
+ false otherwise.
+ The testMessage() method
+ should simply return some helpful text explaining the test
+ that was carried out.
+
+
+ This class can now be used in place of the earlier expectation
+ classes.
+
+ The SimpleTest unit testing framework
+ also uses the expectation classes internally for the
+ UnitTestCase class.
+ We can also take advantage of these mechanisms to reuse our
+ homebrew expectation classes within the test suites directly.
+
+
+ The most crude way of doing this is to use the
+ SimpleTest::assert() method to
+ test against it directly...
+
+ This is a little untidy compared with our usual
+ assert...() syntax.
+
+
+ For such a simple case we would normally create a
+ separate assertion method on our test case rather
+ than bother using the expectation class.
+ If we pretend that our expectation is a little more
+ complicated for a moment, so that we want to reuse it,
+ we get...
+
+ It is unlikely we would ever need this degree of control
+ over the testing machinery.
+ It is rare to need the expectations for more than pattern
+ matching.
+ Also, complex expectation classes could make the tests
+ harder to read and debug.
+ These mechanisms are really of most use to authors of systems
+ that will extend the test framework to create their own tool set.
+
+
+
+ When a page is fetched by the WebTestCase
+ using get() or
+ post() the page content is
+ automatically parsed.
+ This results in any form controls that are inside <form> tags
+ being available from within the test case.
+ For example, if we have this snippet of HTML...
+
+ We can navigate to this code, via the
+ LastCraft
+ site, with the following test...
+
+class SimpleFormTests extends WebTestCase {
+
+ function testDefaultValue() {
+ $this->get('http://www.lastcraft.com/form_testing_documentation.php');
+ $this->assertField('a', 'A default');
+ }
+}
+
+ Immediately after loading the page all of the HTML controls are set at
+ their default values just as they would appear in the web browser.
+ The assertion tests that a HTML widget exists in the page with the
+ name "a" and that it is currently set to the value
+ "A default".
+ As usual, we could use a pattern expectation instead if a fixed
+ string.
+
+
+ We could submit the form straight away, but first we'll change
+ the value of the text field and only then submit it...
+
+ Because we didn't specify a method attribute on the form tag, and
+ didn't specify an action either, the test case will follow
+ the usual browser behaviour of submitting the form data as a GET
+ request back to the same location.
+ SimpleTest tries to emulate typical browser behaviour as much as possible,
+ rather than attempting to catch missing attributes on tags.
+ This is because the target of the testing framework is the PHP application
+ logic, not syntax or other errors in the HTML code.
+ For HTML errors, other tools such as
+ HTMLTidy should be used.
+
+
+ If a field is not present in any form, or if an option is unavailable,
+ then WebTestCase::setField() will return
+ false.
+ For example, suppose we wish to verify that a "Superuser"
+ option is not present in this form...
+
+<strong>Select type of user to add:</strong>
+<select name="type">
+ <option>Subscriber</option>
+ <option>Author</option>
+ <option>Administrator</option>
+</select>
+
+ The selection will not be changed on a failure to set
+ a widget value.
+
+
+ Here is the full list of widgets currently supported...
+
+
Text fields, including hidden and password fields.
+
Submit buttons including the button tag, although not yet reset buttons
+
Text area. This includes text wrapping behaviour.
+
Checkboxes, including multiple checkboxes in the same form.
+
Drop down selections, including multiple selects.
+
Radio buttons.
+
Images.
+
+
+
+ The browser emulation offered by SimpleTest mimics
+ the actions which can be perform by a user on a
+ standard HTML page. Javascript is not supported, and
+ it's unlikely that support will be added any time
+ soon.
+
+
+ Of particular note is that the Javascript idiom of
+ passing form results by setting a hidden field cannot
+ be performed using the normal SimpleTest
+ commands. See below for a way to test such forms.
+
+ SimpleTest can cope with two types of multivalue controls: Multiple
+ selection drop downs, and multiple checkboxes with the same name
+ within a form.
+ The multivalue nature of these means that setting and testing
+ are slightly different.
+ Using checkboxes as an example...
+
+ Instead of setting the field to a single value, we give it a list
+ of values.
+ We do the same when testing expected values.
+ We can then write other test code to confirm the effect of this, perhaps
+ by logging in as that user and attempting an update.
+
+
+
+ If you want to test a form which relies on javascript to set a hidden
+ field, you can't just call setField().
+ The following code will not work:
+
+class SimpleFormTests extends WebTestCase {
+ function testMyJavascriptForm() {
+ // This does *not* work
+ $this->setField('a_hidden_field', '123');
+ $this->clickSubmit('OK');
+ }
+}
+
+ Instead, you need to pass the additional form parameters to the
+ clickSubmit() method:
+
+class SimpleFormTests extends WebTestCase {
+ function testMyJavascriptForm() {
+ // Pass the hidden field value as an additional POST variable
+ $this->clickSubmit('OK', array('a_hidden_field'=>'123'));
+ }
+
+}
+
+
+
+ Bear in mind that in doing this you're effectively stubbing out a
+ part of your software (the javascript code in the form), and
+ perhaps you might be better off using something like
+ Selenium to ensure a complete
+ acceptance test.
+
+ If you want to test a form handler, but have not yet written
+ or do not have access to the form itself, you can create a
+ form submission by hand.
+
+ As many cases as needed can appear in a single file.
+ They should include any code they need, such as the library
+ being tested, but none of the simple test libraries.
+
+
+ If you have extended any test cases, you can include them
+ as well. In PHP 4...
+
+ The FileTester class does
+ not contain any actual tests, but is a base class for other
+ test cases.
+ For this reason we use the
+ SimpleTestOptions::ignore() directive
+ to tell the upcoming group test to ignore it.
+ This directive can appear anywhere in the file and works
+ when a whole file of test cases is loaded (see below).
+
+
+ If you are using PHP 5, you do not need this special directive at all.
+ Simply mark any test cases that should not be run as abstract...
+
+ We will call this sample file_test.php.
+ Next we create a group test file, called say my_group_test.php.
+ You will think of a better name I am sure.
+
+
+ We will add the test file using a safe method...
+
+ This instantiates the test case before the test suite is
+ run.
+ This could get a little expensive with a large number of test
+ cases, and can be surprising behaviour.
+
+
+ The main problem is that for every test case
+ that we add we will have
+ to require_once() the test code
+ file and manually instantiate each and every test case.
+
+ What happens here is that the TestSuite
+ class has done the require_once()
+ for us.
+ It then checks to see if any new test case classes
+ have been created by the new file and automatically adds
+ them to the group test.
+ Now all we have to do is add each new file.
+
+
+ No only that, but you can guarantee that the constructor is run
+ just before the first test method and, in PHP 5, the destructor
+ is run just after the last test method.
+
+
+ There are two things that could go wrong and which require care...
+
+
+ The file could already have been parsed by PHP, and so no
+ new classes will have been added. You should make
+ sure that the test cases are only included in this file
+ and no others (Note : with the new autorun
+ functionnality, this problem has now been solved).
+
+
+ New test case extension classes that get included will be
+ placed in the group test and run also.
+ You will need to add a SimpleTestOptions::ignore()
+ directive for these classes, or make sure that they are included
+ before the TestSuite::addTestFile()
+ line, or make sure that they are abstract classes.
+
+ The above method places all of the test cases into one large group.
+ For larger projects though this may not be flexible enough; you
+ may want to group the tests in all sorts of ways.
+
+
+ To get a more flexible group test we can subclass
+ TestSuite and then instantiate it as needed...
+
+ This effectively names the test in the constructor and then
+ adds our test cases and a single group below.
+ Of course we can add more than one group at this point.
+ We can now invoke the tests from a separate runner file...
+
+ ...or we can group them into even larger group tests.
+ We can even mix groups and test cases freely as long as
+ we are careful about double includes...
+
+ In the event of a double include, ony the first instance
+ of the test case will be run.
+
+
+ If we still wish to run the original group test, and we
+ don't want all of these little runner files, we can
+ put the test runner code around guard bars when we create
+ each group.
+
+ This approach requires the guard to be set when including
+ the group test file, but this is still less hassle than
+ lots of separate runner files.
+ You include the same guard on the top level tests to make sure
+ that run() will run once only
+ from the top level script that has been invoked.
+
+ If you already have unit tests for your code or are extending external
+ classes that have tests, it is unlikely that all of the test cases
+ are in SimpleTest format.
+ Fortunately it is possible to incorporate test cases from other
+ unit testers directly into SimpleTest group tests.
+
+
+ Say we have the following
+ PhpUnit
+ test case in the file config_test.php...
+
+ The PEAR test cases can be freely mixed with SimpleTest
+ ones even in the same test file,
+ but you cannot use SimpleTest assertions in the legacy
+ test case versions.
+ This is done as a check that you are not accidently making
+ your test cases completely dependent on SimpleTest.
+ You may want to do a PEAR release of your library for example,
+ which would mean shipping it with valid PEAR::PhpUnit test
+ cases.
+
+
+
+ The following assumes that you are familiar with the concept
+ of unit testing as well as the PHP web development language.
+ It is a guide for the impatient new user of
+ SimpleTest.
+ For fuller documentation, especially if you are new
+ to unit testing see the ongoing
+ documentation, and for
+ example test cases see the
+ unit testing tutorial.
+
+ Amongst software testing tools, a unit tester is the one
+ closest to the developer.
+ In the context of agile development the test code sits right
+ next to the source code as both are written simultaneously.
+ In this context SimpleTest aims to be a complete PHP developer
+ test solution and is called "Simple" because it
+ should be easy to use and extend.
+ It wasn't a good choice of name really.
+ It includes all of the typical functions you would expect from
+ JUnit and the
+ PHPUnit
+ ports, and includes
+ mock objects.
+
+
+ What makes this tool immediately useful to the PHP developer is the internal
+ web browser.
+ This allows tests that navigate web sites, fill in forms and test pages.
+ Being able to write these test in PHP means that it is easy to write
+ integrated tests.
+ An example might be confirming that a user was written to a database
+ after a signing up through the web site.
+
+
+ The quickest way to demonstrate SimpleTest is with an example.
+
+
+ Let us suppose we are testing a simple file logging class called
+ Log in classes/log.php.
+ We start by creating a test script which we will call
+ tests/log_test.php and populate it as follows...
+
+ Here the simpletest folder is either local or in the path.
+ You would have to edit these locations depending on where you
+ unpacked the toolset.
+ The "autorun.php" file does more than just include the
+ SimpleTest files, it also runs our test for us.
+
+
+ The TestOfLogging is our first test case and it's
+ currently empty.
+ Each test case is a class that extends one of the SimpleTet base classes
+ and we can have as many of these in the file as we want.
+
+
+ With three lines of scaffolding, and our Log class
+ include, we have a test suite.
+ No tests though.
+
+
+ For our first test, we'll assume that the Log class
+ takes the file name to write to in the constructor, and we have
+ a temporary folder in which to place this file...
+
+<?php
+require_once('simpletest/autorun.php');
+require_once('../classes/log.php');
+
+class TestOfLogging extends UnitTestCase {
+ function testLogCreatesNewFileOnFirstMessage() {
+ @unlink('/temp/test.log');
+ $log = new Log('/temp/test.log');
+ $this->assertFalse(file_exists('/temp/test.log'));
+ $log->message('Should write this to a file');
+ $this->assertTrue(file_exists('/temp/test.log'));
+ }
+}
+?>
+
+ When a test case runs, it will search for any method that
+ starts with the string "test"
+ and execute that method.
+ If the method starts "test", it's a test.
+ Note the very long name testLogCreatesNewFileOnFirstMessage().
+ This is considered good style and makes the test output more readable.
+
+
+ We would normally have more than one test method in a test case,
+ but that's for later.
+
+
+ Assertions within the test methods trigger messages to the
+ test framework which displays the result immediately.
+ This immediate response is important, not just in the event
+ of the code causing a crash, but also so that
+ print statements can display
+ their debugging content right next to the assertion concerned.
+
+
+ To see these results we have to actually run the tests.
+ No other code is necessary - we can just open the page
+ with our browser.
+
+ Fatal error: Failed opening required '../classes/log.php' (include_path='') in /home/marcus/projects/lastcraft/tutorial_tests/Log/tests/log_test.php on line 7
+
+ it means you're missing the classes/Log.php file that could look like...
+
+<?php
+class Log {
+ function Log($file_path) {
+ }
+
+ function message() {
+ }
+}
+?>
+
+ It's fun to write the code after the test.
+ More than fun even -
+ this system is called "Test Driven Development".
+
+
+ It is unlikely in a real application that we will only ever run
+ one test case.
+ This means that we need a way of grouping cases into a test
+ script that can, if need be, run every test for the application.
+
+
+ Our first step is to create a new file called tests/all_tests.php
+ and insert the following code...
+
+ The "autorun" include allows our upcoming test suite
+ to be run just by invoking this script.
+
+
+ The TestSuite subclass must chain it's constructor.
+ This limitation will be removed in future versions.
+
+
+ The method TestSuite::addFile()
+ will include the test case file and read any new classes
+ that are descended from SimpleTestCase.
+ UnitTestCase is just one example of a class derived from
+ SimpleTestCase, and you can create your own.
+ TestSuite::addFile() can include other test suites.
+
+
+ The class will not be instantiated yet.
+ When the test suite runs it will construct each instance once
+ it reaches that test, then destroy it straight after.
+ This means that the constructor is run just before each run
+ of that test case, and the destructor is run before the next test case starts.
+
+
+ It is common to group test case code into superclasses which are not
+ supposed to run, but become the base classes of other tests.
+ For "autorun" to work properly the test case file should not blindly run
+ any other test case extensions that do not actually run tests.
+ This could result in extra test cases being counted during the test
+ run.
+ Hardly a major problem, but to avoid this inconvenience simply mark your
+ base class as abstract.
+ SimpleTest won't run abstract classes.
+ If you are still using PHP4, then
+ a SimpleTestOptions::ignore() directive
+ somewhere in the test case file will have the same effect.
+
+
+ Also, the test case file should not have been included
+ elsewhere or no cases will be added to this group test.
+ This would be a more serious error as if the test case classes are
+ already loaded by PHP the TestSuite::addFile()
+ method will not detect them.
+
+
+ To display the results it is necessary only to invoke
+ tests/all_tests.php from the web server or the command line.
+
+ Let's move further into the future and do something really complicated.
+
+
+ Assume that our logging class is tested and completed.
+ Assume also that we are testing another class that is
+ required to write log messages, say a
+ SessionPool.
+ We want to test a method that will probably end up looking
+ like this...
+
+ We'll explain the setUp() and tearDown()
+ methods later.
+
+
+ This test case design is not all bad, but it could be improved.
+ We are spending time fiddling with log files which are
+ not part of our test.
+ We have created close ties with the Log class and
+ this test.
+ What if we don't use files any more, but use ths
+ syslog library instead?
+ It means that our TestOfSessionLogging test will
+ fail, even thouh it's not testing Logging.
+
+
+ It's fragile in smaller ways too.
+ Did you notice the extra carriage return in the message?
+ Was that added by the logger?
+ What if it also added a time stamp or other data?
+
+
+ The only part that we really want to test is that a particular
+ message was sent to the logger.
+ We can reduce coupling if we pass in a fake logging class
+ that simply records the message calls for testing, but
+ takes no action.
+ It would have to look exactly like our original though.
+
+
+ If the fake object doesn't write to a file then we save on deleting
+ the file before and after each test. We could save even more
+ test code if the fake object would kindly run the assertion for us.
+
+
+ Too good to be true?
+ We can create such an object easily...
+
+ The Mock::generate() call code generated a new class
+ called MockLog.
+ This looks like an identical clone, except that we can wire test code
+ to it.
+ That's what expectOnce() does.
+ It says that if message() is ever called on me, it had
+ better be with the parameter "User fred logged in.".
+
+
+ The test will be triggered when the call to
+ message() is invoked on the
+ MockLog object by SessionPool::logIn() code.
+ The mock call will trigger a parameter comparison and then send the
+ resulting pass or fail event to the test display.
+ Wildcards can be included here too, so you don't have to test every parameter of
+ a call when you only want to test one.
+
+
+ If the mock reaches the end of the test case without the
+ method being called, the expectOnce()
+ expectation will trigger a test failure.
+ In other words the mocks can detect the absence of
+ behaviour as well as the presence.
+
+
+ The mock objects in the SimpleTest suite can have arbitrary
+ return values set, sequences of returns, return values
+ selected according to the incoming arguments, sequences of
+ parameter expectations and limits on the number of times
+ a method is to be invoked.
+
+ One of the requirements of web sites is that they produce web
+ pages.
+ If you are building a project top-down and you want to fully
+ integrate testing along the way then you will want a way of
+ automatically navigating a site and examining output for
+ correctness.
+ This is the job of a web tester.
+
+
+ The web testing in SimpleTest is fairly primitive, as there is
+ no JavaScript.
+ Most other browser operations are simulated.
+
+
+ To give an idea here is a trivial example where a home
+ page is fetched, from which we navigate to an "about"
+ page and then test some client determined content.
+
+<?php
+require_once('simpletest/autorun.php');
+require_once('simpletest/web_tester.php');
+
+class TestOfAbout extends WebTestCase {
+ function testOurAboutPageGivesFreeReignToOurEgo() {
+ $this->get('http://test-server/index.php');
+ $this->click('About');
+ $this->assertTitle('About why we are so great');
+ $this->assertText('We are really great');
+ }
+}
+?>
+
+ With this code as an acceptance test, you can ensure that
+ the content always meets the specifications of both the
+ developers, and the other project stakeholders.
+
+
+ Mock objects have two roles during a test case: actor and critic.
+
+
+ The actor behaviour is to simulate objects that are difficult to
+ set up or time consuming to set up for a test.
+ The classic example is a database connection.
+ Setting up a test database at the start of each test would slow
+ testing to a crawl and would require the installation of the
+ database engine and test data on the test machine.
+ If we can simulate the connection and return data of our
+ choosing we not only win on the pragmatics of testing, but can
+ also feed our code spurious data to see how it responds.
+ We can simulate databases being down or other extremes
+ without having to create a broken database for real.
+ In other words, we get greater control of the test environment.
+
+
+ If mock objects only behaved as actors they would simply be
+ known as server stubs.
+ This was originally a pattern named by Robert Binder (Testing
+ object-oriented systems: models, patterns, and tools,
+ Addison-Wesley) in 1999.
+
+
+ A server stub is a simulation of an object or component.
+ It should exactly replace a component in a system for test
+ or prototyping purposes, but remain lightweight.
+ This allows tests to run more quickly, or if the simulated
+ class has not been written, to run at all.
+
+
+ However, the mock objects not only play a part (by supplying chosen
+ return values on demand) they are also sensitive to the
+ messages sent to them (via expectations).
+ By setting expected parameters for a method call they act
+ as a guard that the calls upon them are made correctly.
+ If expectations are not met they save us the effort of
+ writing a failed test assertion by performing that duty on our
+ behalf.
+
+
+ In the case of an imaginary database connection they can
+ test that the query, say SQL, was correctly formed by
+ the object that is using the connection.
+ Set them up with fairly tight expectations and you will
+ hardly need manual assertions at all.
+
+ In the same way that we create server stubs, all we need is an
+ existing class, say a database connection that looks like this...
+
+class DatabaseConnection {
+ function DatabaseConnection() {
+ }
+
+ function query() {
+ }
+
+ function selectQuery() {
+ }
+}
+
+ The class does not need to have been implemented yet.
+ To create a mock version of the class we need to include the
+ mock object library and run the generator...
+
+ Unlike the generated stubs the mock constructor needs a reference
+ to the test case so that it can dispatch passes and failures while
+ checking its expectations.
+ This means that mock objects can only be used within test cases.
+ Despite this their extra power means that stubs are hardly ever used
+ if mocks are available.
+
+
+ The mock version of a class has all the methods of the original,
+ so that operations like
+ $connection->query() are still
+ legal.
+ The return value will be null,
+ but we can change that with...
+
+$connection->setReturnValue('query', 37)
+
+ Now every time we call
+ $connection->query() we get
+ the result of 37.
+ We can set the return value to anything, say a hash of
+ imaginary database results or a list of persistent objects.
+ Parameters are irrelevant here, we always get the same
+ values back each time once they have been set up this way.
+ That may not sound like a convincing replica of a
+ database connection, but for the half a dozen lines of
+ a test method it is usually all you need.
+
+
+ We can also add extra methods to the mock when generating it
+ and choose our own class name...
+
+ Here the mock will behave as if the setOptions()
+ existed in the original class.
+ This is handy if a class has used the PHP overload()
+ mechanism to add dynamic methods.
+ You can create a special mock to simulate this situation.
+
+
+ Things aren't always that simple though.
+ One common problem is iterators, where constantly returning
+ the same value could cause an endless loop in the object
+ being tested.
+ For these we need to set up sequences of values.
+ Let's say we have a simple iterator that looks like this...
+
+class Iterator {
+ function Iterator() {
+ }
+
+ function next() {
+ }
+}
+
+ This is about the simplest iterator you could have.
+ Assuming that this iterator only returns text until it
+ reaches the end, when it returns false, we can simulate it
+ with...
+
+ When next() is called on the
+ mock iterator it will first return "First string",
+ on the second call "Second string" will be returned
+ and on any other call false will
+ be returned.
+ The sequenced return values take precedence over the constant
+ return value.
+ The constant one is a kind of default if you like.
+
+
+ Another tricky situation is an overloaded
+ get() operation.
+ An example of this is an information holder with name/value pairs.
+ Say we have a configuration class like...
+
+class Configuration {
+ function Configuration() {
+ }
+
+ function getValue($key) {
+ }
+}
+
+ This is a classic situation for using mock objects as
+ actual configuration will vary from machine to machine,
+ hardly helping the reliability of our tests if we use it
+ directly.
+ The problem though is that all the data comes through the
+ getValue() method and yet
+ we want different results for different keys.
+ Luckily the mocks have a filter system...
+
+ The extra parameter is a list of arguments to attempt
+ to match.
+ In this case we are trying to match only one argument which
+ is the look up key.
+ Now when the mock object has the
+ getValue() method invoked
+ like this...
+
+$config->getValue('db_user')
+
+ ...it will return "admin".
+ It finds this by attempting to match the calling arguments
+ to its list of returns one after another until
+ a complete match is found.
+
+
+ You can set a default argument argument like so...
+
+ This is not the same as setting the return value without
+ any argument requirements like this...
+
+$config->setReturnValue('getValue', false);
+
+ In the first case it will accept any single argument,
+ but exactly one is required.
+ In the second case any number of arguments will do and
+ it acts as a catchall after all other matches.
+ Note that if we add further single parameter options after
+ the wildcard in the first case, they will be ignored as the wildcard
+ will match first.
+ With complex parameter lists the ordering could be important
+ or else desired matches could be masked by earlier wildcard
+ ones.
+ Declare the most specific matches first if you are not sure.
+
+
+ There are times when you want a specific object to be
+ dished out by the mock rather than a copy.
+ The PHP4 copy semantics force us to use a different method
+ for this.
+ You might be simulating a container for example...
+
+ With this arrangement you know that every time
+ $vector->get(12) is
+ called it will return the same
+ $thing each time.
+ This is compatible with PHP5 as well.
+
+
+ These three factors, timing, parameters and whether to copy,
+ can be combined orthogonally.
+ For example...
+
+ This will return the $stuff only on the third
+ call and only if two parameters were set the second of
+ which must be the integer 1.
+ That should cover most simple prototyping situations.
+
+
+ A final tricky case is one object creating another, known
+ as a factory pattern.
+ Suppose that on a successful query to our imaginary
+ database, a result set is returned as an iterator with
+ each call to next() giving
+ one row until false.
+ This sounds like a simulation nightmare, but in fact it can all
+ be mocked using the mechanics above.
+
+ Now only if our
+ $connection is called with the correct
+ query() will the
+ $result be returned that is
+ itself exhausted after the third call to next().
+ This should be enough
+ information for our UserFinder class,
+ the class actually
+ being tested here, to come up with goods.
+ A very precise test and not a real database in sight.
+
+
+
+ Although the server stubs approach insulates your tests from
+ real world disruption, it is only half the benefit.
+ You can have the class under test receiving the required
+ messages, but is your new class sending correct ones?
+ Testing this can get messy without a mock objects library.
+
+
+ By way of example, suppose we have a
+ SessionPool class that we
+ want to add logging to.
+ Rather than grow the original class into something more
+ complicated, we want to add this behaviour with a decorator (GOF).
+ The SessionPool code currently looks
+ like this...
+
+ Out of all of this, the only class we want to test here
+ is the LoggingSessionPool.
+ In particular we would like to check that the
+ findSession() method is
+ called with the correct session ID in the cookie and that
+ it sent the message "Starting session $cookie"
+ to the logger.
+
+
+ Despite the fact that we are testing only a few lines of
+ production code, here is what we would have to do in a
+ conventional test case:
+
+
Create a log object.
+
Set a directory to place the log file.
+
Set the directory permissions so we can write the log.
+
Create a SessionPool object.
+
Hand start a session, which probably does lot's of things.
+
Invoke findSession().
+
Read the new Session ID (hope there is an accessor!).
+
Raise a test assertion to confirm that the ID matches the cookie.
+
Read the last line of the log file.
+
Pattern match out the extra logging timestamps, etc.
+
Assert that the session message is contained in the text.
+
+ It is hardly surprising that developers hate writing tests
+ when they are this much drudgery.
+ To make things worse, every time the logging format changes or
+ the method of creating new sessions changes, we have to rewrite
+ parts of this test even though this test does not officially
+ test those parts of the system.
+ We are creating headaches for the writers of these other classes.
+
+
+ Instead, here is the complete test method using mock object magic...
+
+ We start by creating a dummy session.
+ We don't have to be too fussy about this as the check
+ for which session we want is done elsewhere.
+ We only need to check that it was the same one that came
+ from the session pool.
+
+
+ findSession() is a factory
+ method the simulation of which is described above.
+ The point of departure comes with the first
+ expectOnce() call.
+ This line states that whenever
+ findSession() is invoked on the
+ mock, it will test the incoming arguments.
+ If it receives the single argument of a string "abc"
+ then a test pass is sent to the unit tester, otherwise a fail is
+ generated.
+ This was the part where we checked that the right session was asked for.
+ The argument list follows the same format as the one for setting
+ return values.
+ You can have wildcards and sequences and the order of
+ evaluation is the same.
+
+
+ We use the same pattern to set up the mock logger.
+ We tell it that it should have
+ message() invoked
+ once only with the argument "Starting session abc".
+ By testing the calling arguments, rather than the logger output,
+ we insulate the test from any display changes in the logger.
+
+
+ We start to run our tests when we create the new
+ LoggingSessionPool and feed
+ it our preset mock objects.
+ Everything is now under our control.
+
+
+ This is still quite a bit of test code, but the code is very
+ strict.
+ If it still seems rather daunting there is a lot less of it
+ than if we tried this without mocks and this particular test,
+ interactions rather than output, is always more work to set
+ up.
+ More often you will be testing more complex situations without
+ needing this level or precision.
+ Also some of this can be refactored into a test case
+ setUp() method.
+
+
+ Here is the full list of expectations you can set on a mock object
+ in SimpleTest...
+
+
+
+
Expectation
+
Needs tally()
+
+
+
+
+
+
expect($method, $args)
+
No
+
+
+
expectAt($timing, $method, $args)
+
No
+
+
+
expectCallCount($method, $count)
+
Yes
+
+
+
expectMaximumCallCount($method, $count)
+
No
+
+
+
expectMinimumCallCount($method, $count)
+
Yes
+
+
+
expectNever($method)
+
No
+
+
+
expectOnce($method, $args)
+
Yes
+
+
+
expectAtLeastOnce($method, $args)
+
Yes
+
+
+
+ Where the parameters are...
+
+
$method
+
The method name, as a string, to apply the condition to.
+
$args
+
+ The arguments as a list. Wildcards can be included in the same
+ manner as for setReturn().
+ This argument is optional for expectOnce()
+ and expectAtLeastOnce().
+
+
$timing
+
+ The only point in time to test the condition.
+ The first call starts at zero.
+
+
$count
+
The number of calls expected.
+
+ The method expectMaximumCallCount()
+ is slightly different in that it will only ever generate a failure.
+ It is silent if the limit is never reached.
+
+
+ Also if you have juste one call in your test, make sure you're using
+ expectOnce.
+ Using $mocked->expectAt(0, 'method', 'args);
+ on its own will not be catched :
+ checking the arguments and the overall call count
+ are currently independant.
+
+
+ Like the assertions within test cases, all of the expectations
+ can take a message override as an extra parameter.
+ Also the original failure message can be embedded in the output
+ as "%s".
+
+ There are three approaches to creating mocks including the one
+ that SimpleTest employs.
+ Coding them by hand using a base class, generating them to
+ a file and dynamically generating them on the fly.
+
+
+ Mock objects generated with SimpleTest
+ are dynamic.
+ They are created at run time in memory, using
+ eval(), rather than written
+ out to a file.
+ This makes the mocks easy to create, a one liner,
+ especially compared with hand
+ crafting them in a parallel class hierarchy.
+ The problem is that the behaviour is usually set up in the tests
+ themselves.
+ If the original objects change the mock versions
+ that the tests rely on can get out of sync.
+ This can happen with the parallel hierarchy approach as well,
+ but is far more quickly detected.
+
+
+ The solution, of course, is to add some real integration
+ tests.
+ You don't need very many and the convenience gained
+ from the mocks more than outweighs the small amount of
+ extra testing.
+ You cannot trust code that was only tested with mocks.
+
+
+ If you are still determined to build static libraries of mocks
+ because you want to simulate very specific behaviour, you can
+ achieve the same effect using the SimpleTest class generator.
+ In your library file, say mocks/connection.php for a
+ database connection, create a mock and inherit to override
+ special methods or add presets...
+
+ The generate call tells the class generator to create
+ a class called BasicMockConnection
+ rather than the usual MockConnection.
+ We then inherit from this to get our version of
+ MockConnection.
+ By intercepting in this way we can add behaviour, here setting
+ the default value of query() to be false.
+ By using the default name we make sure that the mock class
+ generator will not recreate a different one when invoked elsewhere in the
+ tests.
+ It never creates a class if it already exists.
+ As long as the above file is included first then all tests
+ that generated MockConnection should
+ now be using our one instead.
+ If we don't get the order right and the mock library
+ creates one first then the class creation will simply fail.
+
+
+ Use this trick if you find you have a lot of common mock behaviour
+ or you are getting frequent integration problems at later
+ stages of testing.
+
+
+
diff --git a/simpletest/docs/en/overview.html b/simpletest/docs/en/overview.html
new file mode 100644
index 0000000..5bed89e
--- /dev/null
+++ b/simpletest/docs/en/overview.html
@@ -0,0 +1,486 @@
+
+
+
+
+ Overview and feature list for the SimpleTest PHP unit tester and web tester
+
+
+
+
+
+ The heart of SimpleTest is a testing framework built around
+ test case classes.
+ These are written as extensions of base test case classes,
+ each extended with methods that actually contain test code.
+ Top level test scripts then invoke the run()
+ methods on every one of these test cases in order.
+ Each test method is written to invoke various assertions that
+ the developer expects to be true such as
+ assertEqual().
+ If the expectation is correct, then a successful result is dispatched to the
+ observing test reporter, but any failure triggers an alert
+ and a description of the mismatch.
+
+ These tools are designed for the developer.
+ Tests are written in the PHP language itself more or less
+ as the application itself is built.
+ The advantage of using PHP itself as the testing language is that
+ there are no new languages to learn, testing can start straight away,
+ and the developer can test any part of the code.
+ Basically, all parts that can be accessed by the application code can also be
+ accessed by the test code, if they are in the same programming language.
+
+
+ The simplest type of test case is the
+ UnitTestCase.
+ This class of test case includes standard tests for equality,
+ references and pattern matching.
+ All these test the typical expectations of what you would
+ expect the result of a function or method to be.
+ This is by far the most common type of test in the daily
+ routine of development, making up about 95% of test cases.
+
+
+ The top level task of a web application though is not to
+ produce correct output from its methods and objects, but
+ to generate web pages.
+ The WebTestCase class tests web
+ pages.
+ It simulates a web browser requesting a page, complete with
+ cookies, proxies, secure connections, authentication, forms, frames and most
+ navigation elements.
+ With this type of test case, the developer can assert that
+ information is present in the page and that forms and
+ sessions are handled correctly.
+
+ The following is a very rough outline of past and future features
+ and their expected point of release.
+ I am afraid it is liable to change without warning, as meeting the
+ milestones rather depends on time available.
+ Green stuff has been coded, but not necessarily released yet.
+ If you have a pressing need for a green but unreleased feature
+ then you should check-out the code from Sourceforge SVN directly.
+
+
+
+
Feature
+
Description
+
Release
+
+
+
+
+
Unit test case
+
Core test case class and assertions
+
1.0
+
+
+
Html display
+
Simplest possible display
+
1.0
+
+
+
Autoloading of test cases
+
+ Reading a file with test cases and loading them into a
+ group test automatically
+
+
1.0
+
+
+
Mock objects
+
+ Objects capable of simulating other objects removing
+ test dependencies
+
+
1.0
+
+
+
Web test case
+
Allows link following and title tag matching
+
1.0
+
+
+
Partial mocks
+
+ Mocking parts of a class for testing less than a class
+ or for complex simulations
+
+
1.0
+
+
+
Web cookie handling
+
Correct handling of cookies when fetching pages
+
1.0
+
+
+
Following redirects
+
Page fetching automatically follows 300 redirects
+
1.0
+
+
+
Form parsing
+
Ability to submit simple forms and read default form values
+
1.0
+
+
+
Command line interface
+
Test display without the need of a web browser
+
1.0
+
+
+
Exposure of expectation classes
+
Can create precise tests with mocks as well as test cases
+
1.0
+
+
+
XML output and parsing
+
+ Allows multi host testing and the integration of acceptance
+ testing extensions
+
+
1.0
+
+
+
Browser component
+
+ Exposure of lower level web browser interface for more
+ detailed test cases
+
+
1.0
+
+
+
HTTP authentication
+
+ Fetching protected web pages with basic authentication
+ only
+
+
1.0
+
+
+
SSL support
+
Can connect to https: pages
+
1.0
+
+
+
Proxy support
+
Can connect via. common proxies
+
1.0
+
+
+
Frames support
+
Handling of frames in web test cases
+
1.0
+
+
+
File upload testing
+
Can simulate the input type file tag
+
1.0.1
+
+
+
Mocking interfaces
+
+ Can generate mock objects to interfaces as well as classes
+ and class interfaces are carried for type hints
+
+
1.0.1
+
+
+
Testing exceptions
+
Similar to testing PHP errors
+
1.0.1
+
+
+
HTML label support
+
Can access all controls using the visual label
+
1.0.1
+
+
+
Base tag support
+
Respects page base tag when clicking
+
1.0.1
+
+
+
PHP 5 E_STRICT compliant
+
PHP 5 only version that works with the E_STRICT error level
+
1.1
+
+
+
BDD style fixtures
+
Can import fixtures using a mixin like given() method
+
1.5
+
+
+
Reporting machinery enhancements
+
Improved message passing for better cooperation with IDEs
+
1.5
+
+
+
Fluent mock interface
+
More flexible and concise mock objects
+
1.6
+
+
+
Localisation
+
Messages abstracted and code generated
+
1.6
+
+
+
CSS selectors
+
HTML content can be examined using CSS selectors
+
1.7
+
+
+
HTML table assertions
+
Can match HTML or table elements to expectations
+
1.7
+
+
+
Unified acceptance testing model
+
Content searchable through selectors combined with expectations
+
1.7
+
+
+
DatabaseTestCase
+
SQL selectors and DB drivers
+
1.7
+
+
+
IFrame support
+
Reads IFrame content that can be refreshed
+
1.8
+
+
+
Alternate HTML parsers
+
Can detect compiled parsers for performance improvements
+
1.8
+
+
+
Integrated Selenium support
+
Easy to use built in Selenium driver and tutorial
+
1.9
+
+
+
Code coverage
+
Reports using the bundled tool when using XDebug
+
1.9
+
+
+
Deprecation of old methods
+
Simpler interface for SimpleTest2
+
2.0
+
+
+
Javascript suport
+
Use of PECL module to add Javascript to the native browser
+
3.0
+
+
+
+ PHP5 migraton will start straight after the version 1.0.1 series,
+ whereupon only PHP 5.1+ will be supported.
+ SimpleTest is currently compatible with PHP 5, but will not
+ make use of all of the new features until version 1.1.
+
+
+
+ Process is at least as important as tools.
+ The type of process that makes the heaviest use of a developer's
+ testing tool is of course
+ Extreme Programming.
+ This is one of the
+ Agile Methodologies
+ which combine various practices to "flatten the cost curve" of software development.
+ More extreme still is Test Driven Development,
+ where you very strictly adhere to the rule of no coding until you have a test.
+ If you're more of a planner, or believe that experience trumps evolution,
+ you may prefer the
+ RUP approach.
+ I haven't tried it, but even I can see that you will need test tools (see figure 9).
+
+
+ Most unit testers clone JUnit to some degree,
+ as far as the interface at least. There is a wealth of information on the
+ JUnit site including the
+ FAQ
+ which contains plenty of general advice on testing.
+ Once you get bitten by the bug you will certainly appreciate the phrase
+ test infected
+ coined by Eric Gamma.
+ If you are still reviewing which unit tester to use you can find pretty complete
+ lists from
+ Wikipedia,
+ Software testing FAQ,
+ and Open source testing.
+
+
+ There is still very little material on using mock objects, which is a shame
+ as unit testing without them is a lot more work.
+ The original mock objects paper
+ is very Java focused, but still worth a read.
+ The most authoritive sources are probably
+ the original mock objects site and
+ JMock.
+ Java centric, but tucked away in PDFs they contain some deep knowledge on using mocks from the
+ extended experience of the concept inventors.
+ As a new technology there are plenty of discussions and debate on how to use mocks,
+ often on Wikis such as
+ Extreme Tuesday
+ or www.mockobjects.com
+ or the original C2 Wiki.
+ Injecting mocks into a class is the main area of debate for which this
+ paper on IBM
+ makes a good starting point.
+
+
+ There are plenty of web testing tools, but the scriptable ones
+ are mostly are written in Java and
+ tutorials and advice are rather thin on the ground.
+ The only hope is to look at the documentation for
+ HTTPUnit,
+ HTMLUnit
+ or JWebUnit and hope for clues.
+ There are some XML driven test frameworks, but again most
+ require Java to run.
+
+
+ Most significant is a new generation of tools that run directly in the web browser
+ are now available.
+ These include
+ Selenium and
+ Watir.
+ They are non-trivial to set up and slow to run, but can essentially test anything.
+ As SimpleTest does not support JavaScript you would probably
+ have to look at these tools anyway if you have highly dynamic
+ pages.
+
+ A partial mock is simply a pattern to alleviate a specific problem
+ in testing with mock objects,
+ that of getting mock objects into tight corners.
+ It's quite a limited tool and possibly not even a good idea.
+ It is included with SimpleTest because I have found it useful
+ on more than one occasion and has saved a lot of work at that point.
+
+ When one object uses another it is very simple to just pass a mock
+ version in already set up with its expectations.
+ Things are rather tricker if one object creates another and the
+ creator is the one you want to test.
+ This means that the created object should be mocked, but we can
+ hardly tell our class under test to create a mock instead.
+ The tested class doesn't even know it is running inside a test
+ after all.
+
+
+ For example, suppose we are building a telnet client and it
+ needs to create a network socket to pass its messages.
+ The connection method might look something like...
+
+ We would really like to have a mock object version of the socket
+ here, what can we do?
+
+
+ The first solution is to pass the socket in as a parameter,
+ forcing the creation up a level.
+ Having the client handle this is actually a very good approach
+ if you can manage it and should lead to factoring the creation from
+ the doing.
+ In fact, this is one way in which testing with mock objects actually
+ forces you to code more tightly focused solutions.
+ They improve your programming.
+
+ It is pretty obvious though that one level is all you can go.
+ You would hardly want your top level application creating
+ every low level file, socket and database connection ever
+ needed.
+ It wouldn't know the constructor parameters anyway.
+
+
+ The next simplest compromise is to have the created object passed
+ in as an optional parameter...
+
+ The problem with this approach is its untidiness.
+ There is test code in the main class and parameters passed
+ in the test case that are never used.
+ This is a quick and dirty approach, but nevertheless effective
+ in most situations.
+
+
+ The next method is to pass in a factory object to do the creation...
+
+ This is probably the most highly factored answer as creation
+ is now moved into a small specialist class.
+ The networking factory can now be tested separately, but mocked
+ easily when we are testing the telnet class...
+
+ The downside is that we are adding a lot more classes to the
+ library.
+ Also we are passing a lot of factories around which will
+ make the code a little less intuitive.
+ The most flexible solution, but the most complex.
+
+
+ There is a way we can circumvent the problem without creating
+ any new application classes, but it involves creating a subclass
+ when we do the actual testing.
+ Firstly we move the socket creation into its own method...
+
+ Here I have passed the mock in the constructor, but a
+ setter would have done just as well.
+ Note that the mock was set into the object variable
+ before the constructor was chained.
+ This is necessary in case the constructor calls
+ connect().
+ Otherwise it could get a null value from
+ _createSocket().
+
+
+ After the completion of all of this extra work the
+ actual test case is fairly easy.
+ We just test our new class instead...
+
+ The new class is very simple of course.
+ It just sets up a return value, rather like a mock.
+ It would be nice if it also checked the incoming parameters
+ as well.
+ Just like a mock.
+ It seems we are likely to do this often, can
+ we automate the subclass creation?
+
+
+
+ Of course the answer is "yes" or I would have stopped writing
+ this by now!
+ The previous test case was a lot of work, but we can
+ generate the subclass using a similar approach to the mock objects.
+
+
+ Here is the partial mock version of the test...
+
+ The partial mock is a subclass of the original with
+ selected methods "knocked out" with test
+ versions.
+ The generatePartial() call
+ takes three parameters: the class to be subclassed,
+ the new test class name and a list of methods to mock.
+
+
+ Instantiating the resulting objects is slightly tricky.
+ The only constructor parameter of a partial mock is
+ the unit tester reference.
+ As with the normal mock objects this is needed for sending
+ test results in response to checked expectations.
+
+
+ The original constructor is not run yet.
+ This is necessary in case the constructor is going to
+ make use of the as yet unset mocked methods.
+ We set any return values at this point and then run the
+ constructor with its normal parameters.
+ This three step construction of "new", followed
+ by setting up the methods, followed by running the constructor
+ proper is what distinguishes the partial mock code.
+
+
+ Apart from construction, all of the mocked methods have
+ the same features as mock objects and all of the unmocked
+ methods behave as before.
+ We can set expectations very easily...
+
+ The mocked out methods don't have to be factory methods,
+ they could be any sort of method.
+ In this way partial mocks allow us to take control of any part of
+ a class except the constructor.
+ We could even go as far as to mock every method
+ except one we actually want to test.
+
+
+ This last situation is all rather hypothetical, as I haven't
+ tried it.
+ I am open to the possibility, but a little worried that
+ forcing object granularity may be better for the code quality.
+ I personally use partial mocks as a way of overriding creation
+ or for occasional testing of the TemplateMethod pattern.
+
+
+ It's all going to come down to the coding standards of your
+ project to decide which mechanism you use.
+
+ SimpleTest pretty much follows the MVC pattern
+ (Model-View-Controller).
+ The reporter classes are the view and the model is your
+ test cases and their hiearchy.
+ The controller is mostly hidden from the user of
+ SimpleTest unless you want to change how the test cases
+ are actually run, in which case it is possible to
+ override the runner objects from within the test case.
+ As usual with MVC, the controller is mostly undefined
+ and there are other places to control the test run.
+
+ The default test display is minimal in the extreme.
+ It reports success and failure with the conventional red and
+ green bars and shows a breadcrumb trail of test groups
+ for every failed assertion.
+ Here's a fail...
+
+
File test
+ Fail: createnewfile->True assertion failed.
+
1/1 test cases complete.
+ 0 passes, 1 fails and 0 exceptions.
+
+ And here all tests passed...
+
+
File test
+
1/1 test cases complete.
+ 1 passes, 0 fails and 0 exceptions.
+
+ The good news is that there are several points in the display
+ hiearchy for subclassing.
+
+
+ For web page based displays there is the
+ HtmlReporter class with the following
+ signature...
+
+class HtmlReporter extends SimpleReporter {
+ public HtmlReporter($encoding) { ... }
+ public makeDry(boolean $is_dry) { ... }
+ public void paintHeader(string $test_name) { ... }
+ public void sendNoCacheHeaders() { ... }
+ public void paintFooter(string $test_name) { ... }
+ public void paintGroupStart(string $test_name, integer $size) { ... }
+ public void paintGroupEnd(string $test_name) { ... }
+ public void paintCaseStart(string $test_name) { ... }
+ public void paintCaseEnd(string $test_name) { ... }
+ public void paintMethodStart(string $test_name) { ... }
+ public void paintMethodEnd(string $test_name) { ... }
+ public void paintFail(string $message) { ... }
+ public void paintPass(string $message) { ... }
+ public void paintError(string $message) { ... }
+ public void paintException(string $message) { ... }
+ public void paintMessage(string $message) { ... }
+ public void paintFormattedMessage(string $message) { ... }
+ protected string _getCss() { ... }
+ public array getTestList() { ... }
+ public integer getPassCount() { ... }
+ public integer getFailCount() { ... }
+ public integer getExceptionCount() { ... }
+ public integer getTestCaseCount() { ... }
+ public integer getTestCaseProgress() { ... }
+}
+
+ Here is what some of these methods mean. First the display methods
+ that you will probably want to override...
+
+
+ HtmlReporter(string $encoding)
+ is the constructor.
+ Note that the unit test sets up the link to the display
+ rather than the other way around.
+ The display is a mostly passive receiver of test events.
+ This allows easy adaption of the display for other test
+ systems beside unit tests, such as monitoring servers.
+ The encoding is the character encoding you wish to
+ display the test output in.
+ In order to correctly render debug output when
+ using the web tester, this should match the encoding
+ of the site you are trying to test.
+ The available character set strings are described in
+ the PHP html_entities()
+ function.
+
+
+ void paintHeader(string $test_name)
+ is called once at the very start of the test when the first
+ start event arrives.
+ The first start event is usually delivered by the top level group
+ test and so this is where $test_name
+ comes from.
+ It paints the page titles, CSS, body tag, etc.
+ It returns nothing (void).
+
+
+ void paintFooter(string $test_name)
+ Called at the very end of the test to close any tags opened
+ by the page header.
+ By default it also displays the red/green bar and the final
+ count of results.
+ Actually the end of the test happens when a test end event
+ comes in with the same name as the one that started it all
+ at the same level.
+ The tests nest you see.
+ Closing the last test finishes the display.
+
+
+ void paintMethodStart(string $test_name)
+ is called at the start of each test method.
+ The name normally comes from method name.
+ The other test start events behave the same way except
+ that the group test one tells the reporter how large
+ it is in number of held test cases.
+ This is so that the reporter can display a progress bar
+ as the runner churns through the test cases.
+
+
+ void paintMethodEnd(string $test_name)
+ backs out of the test started with the same name.
+
+
+ void paintFail(string $message)
+ paints a failure.
+ By default it just displays the word fail, a breadcrumbs trail
+ showing the current test nesting and the message issued by
+ the assertion.
+
+
+ void paintPass(string $message)
+ by default does nothing.
+
+
+ string _getCss()
+ Returns the CSS styles as a string for the page header
+ method.
+ Additional styles have to be appended here if you are
+ not overriding the page header.
+ You will want to use this method in an overriden page header
+ if you want to include the original CSS.
+
+
+ There are also some accessors to get information on the current
+ state of the test suite.
+ Use these to enrich the display...
+
+
+ array getTestList()
+ is the first convenience method for subclasses.
+ Lists the current nesting of the tests as a list
+ of test names.
+ The first, most deeply nested test, is first in the
+ list and the current test method will be last.
+
+
+ integer getPassCount()
+ returns the number of passes chalked up so far.
+ Needed for the display at the end.
+
+
+ integer getFailCount()
+ is likewise the number of fails so far.
+
+
+ integer getExceptionCount()
+ is likewise the number of errors so far.
+
+
+ integer getTestCaseCount()
+ is the total number of test cases in the test run.
+ This includes the grouping tests themselves.
+
+
+ integer getTestCaseProgress()
+ is the number of test cases completed so far.
+
+
+ One simple modification is to get the HtmlReporter to display
+ the passes as well as the failures and errors...
+
+ One method that was glossed over was the makeDry()
+ method.
+ If you run this method, with no parameters, on the reporter
+ before the test suite is run no actual test methods
+ will be called.
+ You will still get the events of entering and leaving the
+ test methods and test cases, but no passes or failures etc,
+ because the test code will not actually be executed.
+
+
+ The reason for this is to allow for more sophistcated
+ GUI displays that allow the selection of individual test
+ cases.
+ In order to build a list of possible tests they need a
+ report on the test structure for drawing, say a tree view
+ of the test suite.
+ With a reporter set to dry run that just sends drawing events
+ this is easily accomplished.
+
+ Rather than simply modifying the existing display, you might want to
+ produce a whole new HTML look, or even generate text or XML.
+ Rather than override every method in
+ HtmlReporter we can take one
+ step up the class hiearchy to SimpleReporter
+ in the simple_test.php source file.
+
+
+ A do nothing display, a blank canvas for your own creation, would
+ be...
+
+ SimpleTest also ships with a minimal command line reporter.
+ The interface mimics JUnit to some extent, but paints the
+ failure messages as they arrive.
+ To use the command line reporter simply substitute it
+ for the HTML version...
+
+File test
+1) True assertion failed.
+ in createnewfile
+FAILURES!!!
+Test cases run: 1/1, Failures: 1, Exceptions: 0
+
+
+
+ One of the main reasons for using a command line driven
+ test suite is of using the tester as part of some automated
+ process.
+ To function properly in shell scripts the test script should
+ return a non-zero exit code on failure.
+ If a test suite fails the value false
+ is returned from the SimpleTest::run()
+ method.
+ We can use that result to exit the script with the desired return
+ code...
+
+ Of course we don't really want to create two test scripts,
+ a command line one and a web browser one, for each test suite.
+ The command line reporter includes a method to sniff out the
+ run time environment...
+
+ You can make use of this format with the parser
+ supplied as part of SimpleTest itself.
+ This is called SimpleTestXmlParser and
+ resides in xml.php within the SimpleTest package...
+
+ The $test_output should be the XML format
+ from the XML reporter, and could come from say a command
+ line run of a test case.
+ The parser sends events to the reporter just like any
+ other test run.
+ There are some odd occasions where this is actually useful.
+
+
+ A problem with large test suites is thet they can exhaust
+ the default 8Mb memory limit on a PHP process.
+ By having the test groups output in XML and run in
+ separate processes, the output can be reparsed to
+ aggregate the results into a much smaller footprint top level
+ test.
+
+
+ Because the XML output can come from anywhere, this opens
+ up the possibility of aggregating test runs from remote
+ servers.
+ A test case already exists to do this within the SimpleTest
+ framework, but it is currently experimental...
+
+ The RemoteTestCase takes the actual location
+ of the test runner, basically a web page in XML format.
+ It also takes the URL of a reporter set to do a dry run.
+ This is so that progress can be reported upward correctly.
+ The RemoteTestCase can be added to test suites
+ just like any other group test.
+
+
+
+ The core system is a regression testing framework built around
+ test cases.
+ A sample test case looks like this...
+
+class FileTestCase extends UnitTestCase {
+}
+
+ Actual tests are added as methods in the test case whose names
+ by default start with the string "test" and
+ when the test case is invoked all such methods are run in
+ the order that PHP introspection finds them.
+ As many test methods can be added as needed.
+
+
+ The constructor is optional and usually omitted.
+ Without a name, the class name is taken as the name of the test case.
+
+
+ Our only test method at the moment is testCreation()
+ where we check that a file has been created by our
+ Writer object.
+ We could have put the unlink()
+ code into this method as well, but by placing it in
+ setUp() and
+ tearDown() we can use it with
+ other test methods that we add.
+
+
+ The setUp() method is run
+ just before each and every test method.
+ tearDown() is run just after
+ each and every test method.
+
+
+ You can place some test case set up into the constructor to
+ be run once for all the methods in the test case, but
+ you risk test inteference that way.
+ This way is slightly slower, but it is safer.
+ Note that if you come from a JUnit background this will not
+ be the behaviour you are used to.
+ JUnit surprisingly reinstantiates the test case for each test
+ method to prevent such interference.
+ SimpleTest requires the end user to use setUp(), but
+ supplies additional hooks for library writers.
+
+
+ The means of reporting test results (see below) are by a
+ visiting display class
+ that is notified by various assert...()
+ methods.
+ Here is the full list for the UnitTestCase
+ class, the default for SimpleTest...
+
+ All assertion methods can take an optional description as a
+ last parameter.
+ This is to label the displayed result with.
+ If omitted a default message is sent instead, which is usually
+ sufficient.
+ This default message can still be embedded in your own message
+ if you include "%s" within the string.
+ All the assertions return true on a pass or false on failure.
+
+
+ Some examples...
+
+$variable = null;
+$this->assertNull($variable, 'Should be cleared');
+
+ ...will pass and normally show no message.
+ If you have
+ set up the tester to display passes
+ as well then the message will be displayed as is.
+
+$this->assertIdentical(0, false, 'Zero is not false [%s]');
+
+ This will fail as it performs a type
+ check, as well as a comparison, between the two values.
+ The "%s" part is replaced by the default
+ error message that would have been shown if we had not
+ supplied our own.
+
+ The next error check tests not only the existence of the error,
+ but also the text which, here matches so another pass.
+ If any unchecked errors are left at the end of a test method then
+ an exception will be reported in the test.
+
+
+ Note that SimpleTest cannot catch compile time PHP errors.
+
+
+ The test cases also have some convenience methods for debugging
+ code or extending the suite...
+
+
+
setUp()
+
Runs this before each test method
+
+
+
tearDown()
+
Runs this after each test method
+
+
+
pass()
+
Sends a test pass
+
+
+
fail()
+
Sends a test failure
+
+
+
error()
+
Sends an exception event
+
+
+
signal($type, $payload)
+
Sends a user defined message to the test reporter
+
+
+
dump($var)
+
Does a formatted print_r() for quick and dirty debugging
+ If you want a test case that does not have all of the
+ UnitTestCase assertions,
+ only your own and a few basics,
+ you need to extend the SimpleTestCase
+ class instead.
+ It is found in simple_test.php rather than
+ unit_tester.php.
+ See later if you
+ want to incorporate other unit tester's
+ test cases in your test suites.
+
+ You won't often run single test cases except when bashing
+ away at a module that is having difficulty, and you don't
+ want to upset the main test suite.
+ With autorun no particular scaffolding is needed,
+ just launch your particular test file and you're ready to go.
+
+
+ You can even decide which reporter (for example,
+ TextReporter or HtmlReporter)
+ you prefer for a specific file when launched on its own...
+
+ Testing classes is all very well, but PHP is predominately
+ a language for creating functionality within web pages.
+ How do we test the front end presentation role of our PHP
+ applications?
+ Well the web pages are just text, so we should be able to
+ examine them just like any other test data.
+
+
+ This leads to a tricky issue.
+ If we test at too low a level, testing for matching tags
+ in the page with pattern matching for example, our tests will
+ be brittle.
+ The slightest change in layout could break a large number of
+ tests.
+ If we test at too high a level, say using mock versions of a
+ template engine, then we lose the ability to automate some classes
+ of test.
+ For example, the interaction of forms and navigation will
+ have to be tested manually.
+ These types of test are extremely repetitive and error prone.
+
+
+ SimpleTest includes a special form of test case for the testing
+ of web page actions.
+ The WebTestCase includes facilities
+ for navigation, content and cookie checks and form handling.
+ Usage of these test cases is similar to the
+ UnitTestCase...
+
+class TestOfLastcraft extends WebTestCase {
+}
+
+ Here we are about to test the
+ Last Craft site itself.
+ If this test case is in a file called lastcraft_test.php
+ then it can be loaded in a runner script just like unit tests...
+
+ The get() method will
+ return true only if page content was successfully
+ loaded.
+ It is a simple, but crude way to check that a web page
+ was actually delivered by the web server.
+ However that content may be a 404 response and yet
+ our get() method will still return true.
+
+
+ Assuming that the web server for the Last Craft site is up
+ (sadly not always the case), we should see...
+
+ To confirm that the page we think we are on is actually the
+ page we are on, we need to verify the page content.
+
+class TestOfLastcraft extends WebTestCase {
+
+ function testHomepage() {
+ $this->get('http://www.lastcraft.com/');
+ $this->assertText('Why the last craft');
+ }
+}
+
+ The page from the last fetch is held in a buffer in
+ the test case, so there is no need to refer to it directly.
+ The pattern match is always made against the buffer.
+
+
+ Here is the list of possible content assertions...
+
+
+
assertTitle($title)
+
Pass if title is an exact match
+
+
+
assertText($text)
+
Pass if matches visible and "alt" text
+
+
+
assertNoText($text)
+
Pass if doesn't match visible and "alt" text
+
+
+
assertPattern($pattern)
+
A Perl pattern match against the page content
+
+
+
assertNoPattern($pattern)
+
A Perl pattern match to not find content
+
+
+
assertLink($label)
+
Pass if a link with this text is present
+
+
+
assertNoLink($label)
+
Pass if no link with this text is present
+
+
+
assertLinkById($id)
+
Pass if a link with this id attribute is present
+
+
+
assertNoLinkById($id)
+
Pass if no link with this id attribute is present
+
+
+
assertField($name, $value)
+
Pass if an input tag with this name has this value
+
+
+
assertFieldById($id, $value)
+
Pass if an input tag with this id has this value
+
+
+
assertResponse($codes)
+
Pass if HTTP response matches this list
+
+
+
assertMime($types)
+
Pass if MIME type is in this list
+
+
+
assertAuthentication($protocol)
+
Pass if the current challenge is this protocol
+
+
+
assertNoAuthentication()
+
Pass if there is no current challenge
+
+
+
assertRealm($name)
+
Pass if the current challenge realm matches
+
+
+
assertHeader($header, $content)
+
Pass if a header was fetched matching this value
+
+
+
assertNoHeader($header)
+
Pass if a header was not fetched
+
+
+
assertCookie($name, $value)
+
Pass if there is currently a matching cookie
+
+
+
assertNoCookie($name)
+
Pass if there is currently no cookie of this name
+
+
+ As usual with the SimpleTest assertions, they all return
+ false on failure and true on pass.
+ They also allow an optional test message and you can embed
+ the original test message inside using "%s" inside
+ your custom message.
+
+
+ So now we could instead test against the title tag with...
+
+$this->assertTitle('The Last Craft? Web developer tutorials on PHP, Extreme programming and Object Oriented development');
+
+ ...or, if that is too long and fragile...
+
+$this->assertTitle(new PatternExpectation('/The Last Craft/'));
+
+ As well as the simple HTML content checks we can check
+ that the MIME type is in a list of allowed types with...
+
+ More interesting is checking the HTTP response code.
+ Like the MIME type, we can assert that the response code
+ is in a list of allowed values...
+
+ Here we are checking that the fetch is successful by
+ allowing only a 200 HTTP response.
+ This test will pass, but it is not actually correct to do so.
+ There is no page, instead the server issues a redirect.
+ The WebTestCase will
+ automatically follow up to three such redirects.
+ The tests are more robust this way and we are usually
+ interested in the interaction with the pages rather
+ than their delivery.
+ If the redirects are of interest then this ability must
+ be disabled...
+
+ Users don't often navigate sites by typing in URLs, but by
+ clicking links and buttons.
+ Here we confirm that the contact details can be reached
+ from the home page...
+
+ If the target is a button rather than an anchor tag, then
+ clickSubmit() can be used
+ with the button title...
+
+$this->clickSubmit('Go!');
+
+ If you are not sure or don't care, the usual case, then just
+ use the click() method...
+
+$this->click('Go!');
+
+
+
+ The list of navigation methods is...
+
+
+
getUrl()
+
The current location
+
+
+
get($url, $parameters)
+
Send a GET request with these parameters
+
+
+
post($url, $parameters)
+
Send a POST request with these parameters
+
+
+
head($url, $parameters)
+
Send a HEAD request without replacing the page content
+
+
+
retry()
+
Reload the last request
+
+
+
back()
+
Like the browser back button
+
+
+
forward()
+
Like the browser forward button
+
+
+
authenticate($name, $password)
+
Retry after a challenge
+
+
+
restart()
+
Restarts the browser as if a new session
+
+
+
getCookie($name)
+
Gets the cookie value for the current context
+
+
+
ageCookies($interval)
+
Ages current cookies prior to a restart
+
+
+
clearFrameFocus()
+
Go back to treating all frames as one page
+
+
+
clickSubmit($label)
+
Click the first button with this label
+
+
+
clickSubmitByName($name)
+
Click the button with this name attribute
+
+
+
clickSubmitById($id)
+
Click the button with this ID attribute
+
+
+
clickImage($label, $x, $y)
+
Click an input tag of type image by title or alt text
+
+
+
clickImageByName($name, $x, $y)
+
Click an input tag of type image by name
+
+
+
clickImageById($id, $x, $y)
+
Click an input tag of type image by ID attribute
+
+
+
submitFormById($id)
+
Submit a form without the submit value
+
+
+
clickLink($label, $index)
+
Click an anchor by the visible label text
+
+
+
clickLinkById($id)
+
Click an anchor by the ID attribute
+
+
+
getFrameFocus()
+
The name of the currently selected frame
+
+
+
setFrameFocusByIndex($choice)
+
Focus on a frame counting from 1
+
+
+
setFrameFocus($name)
+
Focus on a frame by name
+
+
+
+
+ The parameters in the get(), post() or
+ head() methods are optional.
+ The HTTP HEAD fetch does not change the browser context, only loads
+ cookies.
+ This can be useful for when an image or stylesheet sets a cookie
+ for crafty robot blocking.
+
+
+ The retry(), back() and
+ forward() commands work as they would on
+ your web browser.
+ They use the history to retry pages.
+ This can be handy for checking the effect of hitting the
+ back button on your forms.
+
+
+ The frame methods need a little explanation.
+ By default a framed page is treated just like any other.
+ Content will be searced for throughout the entire frameset,
+ so clicking a link will work no matter which frame
+ the anchor tag is in.
+ You can override this behaviour by focusing on a single
+ frame.
+ If you do that, all searches and actions will apply to that
+ frame alone, such as authentication and retries.
+ If a link or button is not in a focused frame then it cannot
+ be clicked.
+
+
+ Testing navigation on fixed pages only tells you when you
+ have broken an entire script.
+ For highly dynamic pages, such as for bulletin boards, this can
+ be crucial for verifying the correctness of the application.
+ For most applications though, the really tricky logic is usually in
+ the handling of forms and sessions.
+ Fortunately SimpleTest includes
+ tools for testing web forms
+ as well.
+
+ Although SimpleTest does not have the goal of testing networking
+ problems, it does include some methods to modify and debug
+ the requests it makes.
+ Here is another method list...
+
+
+
getTransportError()
+
The last socket error
+
+
+
showRequest()
+
Dump the outgoing request
+
+
+
showHeaders()
+
Dump the incoming headers
+
+
+
showSource()
+
Dump the raw HTML page content
+
+
+
ignoreFrames()
+
Do not load framesets
+
+
+
setCookie($name, $value)
+
Set a cookie from now on
+
+
+
addHeader($header)
+
Always add this header to the request
+
+
+
setMaximumRedirects($max)
+
Stop after this many redirects
+
+
+
setConnectionTimeout($timeout)
+
Kill the connection after this time between bytes
+
+
+
useProxy($proxy, $name, $password)
+
Make requests via this proxy URL
+
+
+ These methods are principally for debugging.
+
+
+
+ Si vous testez un système d'authentification,
+ la reconnexion par un utilisateur est un point sensible.
+ Essayons de simuler ce qui se passe dans ce cas :
+
+ L'expiration des cookies peut être un problème.
+ Si vous avez un cookie qui doit expirer au bout d'une heure,
+ nous n'allons pas mettre le test en veille en attendant
+ que le cookie expire...
+
+ Bien que ça puisse être utile par convenance temporaire,
+ je ne suis pas fan de ce genre de test. Ce test s'applique
+ à plusieurs couches de l'application, ça implique qu'il est
+ plus que probable qu'il faudra le remanier lorsque le code changera.
+
+
+ Pour contourner ce problème, nous voudrions utiliser
+ un test avec une expression rationnelle plutôt
+ qu'une correspondance exacte. Nous pouvons y parvenir avec...
+
+ Nous pourrions retourner le formulaire tout de suite,
+ mais d'abord nous allons changer la valeur du champ texte.
+ Ce n'est qu'après que nous le transmettrons...
+
+<strong>Select type of user to add:</strong>
+<select name="type">
+ <option>Subscriber</option>
+ <option>Author</option>
+ <option>Administrator</option>
+</select>
+
+ N'oubliez pas que de la sorte, vous êtes effectivement en train
+ de court-circuitez une partie de votre application (le code Javascript
+ dans le formulaire) et que peut-être serait-il plus prudent
+ d'utiliser un outil comme
+ Selenium pour mettre sur pied
+ un test de recette complet.
+
+ Fatal error: Failed opening required '../classes/log.php' (include_path='') in /home/marcus/projects/lastcraft/tutorial_tests/Log/tests/log_test.php on line 7
+
+ c'est qu'il vous manque le fichier classes/Log.php
+ qui pourrait ressembler à :
+
+<?php
+class Log {
+ function Log($file_path) {
+ }
+
+ function message() {
+ }
+}
+?>
+
+ ...elle renverra "admin".
+ Elle le trouve en essayant de faire correspondre
+ les arguments entrants dans sa liste
+ d'arguments sortants les uns après les autres
+ jusqu'au moment où une correspondance exacte est atteinte.
+
+
+ Les arguments sous la forme d'une liste.
+ Les jokers peuvent être inclus de la même manière
+ qu'avec setReturn().
+ Cet argument est optionnel pour expectOnce()
+ et expectAtLeastOnce().
+
+ Le problème reste que nous ajoutons beaucoup de classes
+ à la bibliothèque. Et aussi que nous utilisons beaucoup
+ de fabriques ce qui rend notre code un peu moins intuitif.
+ La solution la plus flexible, mais aussi la plus complexe.
+
+
+ Les utilisateurs ne naviguent pas souvent en tapant les URLs,
+ mais surtout en cliquant sur des liens et des boutons.
+ Ici nous confirmons que les informations sur le contact
+ peuvent être atteintes depuis la page d'accueil...
+
\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/simpletest/scorer.php b/simpletest/scorer.php
new file mode 100644
index 0000000..cc1331b
--- /dev/null
+++ b/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/simpletest/selector.php b/simpletest/selector.php
new file mode 100644
index 0000000..de044b8
--- /dev/null
+++ b/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/simpletest/shell_tester.php b/simpletest/shell_tester.php
new file mode 100644
index 0000000..7b98869
--- /dev/null
+++ b/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/simpletest/simpletest.php b/simpletest/simpletest.php
new file mode 100644
index 0000000..bab2c1a
--- /dev/null
+++ b/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/simpletest/socket.php b/simpletest/socket.php
new file mode 100644
index 0000000..3ad5a9f
--- /dev/null
+++ b/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/simpletest/tag.php b/simpletest/tag.php
new file mode 100644
index 0000000..7bccae2
--- /dev/null
+++ b/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/simpletest/test/acceptance_test.php b/simpletest/test/acceptance_test.php
new file mode 100644
index 0000000..9dbb5a3
--- /dev/null
+++ b/simpletest/test/acceptance_test.php
@@ -0,0 +1,1633 @@
+addHeader('User-Agent: SimpleTest ' . SimpleTest::getVersion());
+ $this->assertTrue($browser->get($this->samples() . 'network_confirm.php'));
+ $this->assertPattern('/target for the SimpleTest/', $browser->getContent());
+ $this->assertPattern('/Request method.*?