blob: 46a9e8367c3cf4fa6a04c908143175b414f132dd [file] [log] [blame] [view]
# Writing Complex Tests #
For many tests, writing one or more static HTML files is
sufficient. However there are a large class of tests for which this
approach is insufficient, including:
* Tests that require cross-domain access
* Tests that depend on setting specific headers or status codes
* Tests that need to inspect the browser sent request
* Tests that require state to be stored on the server
* Tests that require precise timing of the response.
To make writing such tests possible, we are using a number of
server-side components designed to make it easy to manipulate the
precise details of the response:
* *wptserve*, a custom python HTTP server.
* *pywebsocket*, an existing websockets server
This document will concentrate on the features of wptserve available
to test authors.
## Introduction to wptserve ##
wptserve is a python-based web server. By default it serves static
files in the testsuite. For more sophisticated requirements, several
mechanisms are available to take control of the response. These are
outlined below.
## Pipes ##
Suitable for:
* Cross domain requests
* Adding headers or status codes to static files
* Controlling the sending of static file bodies
Pipes are designed to allow simple manipulation of the way that
static files are sent without requiring any custom code. They are also
useful for cross-origin tests because they can be used to activate a
substitution mechanism which can fill in details of ports and server
names in the setup on which the tests are being run.
Pipes are indicated by adding a query string to a request for a static
resource, with the parameter name `pipe`. The value of the query
should be a `|` serperated list of pipe functions. For example to
return a `.html` file with the status code 410 and a Content-Type of
text/plain, one might use:
/resources/example.html?pipe=status(410)|header(Content-Type,text/plain)
There are a selection of pipe functions provided with wptserve and
more may be added if there are good use cases.
### sub ###
Used to subsitute variables from the server environment, or from the
request into the response. A typical use case is for testing
cross-domain since the exact domain name and ports of the servers are
generally unknown.
Substitutions are marked in a file using a block delimited by `{{`
and `}}`. Inside the block the following variables are avalible:
* `{{host}}` - the host name of the server exclusing any subdomain part.
* `{{domains[]}}` - the domain name of a particular subdomain
e.g. `{{domains[www]}}` for the `www` subdomain.
* `{{ports[][]}}` - The port number of servers, by protocol
e.g. `{{ports[http][1]}}` for the second (i.e. non-default) http
server.
* `{{headers[]}}` - The HTTP headers in the request
e.g. `{{headers[X-Test]}}` for a hypothetical `X-Test` header.
* `{{GET[]}}` - The query parameters for the request
e.g. `{{GET[id]}}` for an id parameter sent with the request.
So, for example, to write a javascript file called `xhr.js` that does a
cross domain XHR test to a different subdomain and port, one would
write in the file:
var server_url = "http://{{domains[www]}}:{{ports[http][1]}}/path/to/resource";
//Create the actual XHR and so on
The file would then be included as:
<script src="xhr.js?pipe=sub"></script>
### status ###
Used to set the HTTP status of the response, for example:
example.js?pipe=status(410)
### headers ###
Used to add or replace http headers in the response. Takes two or
three arguments; the header name, the header value and whether to
append the header rather than replace an existing header (default:
False). So, for example, a request for:
example.html?pipe=header(Content-Type,text/plain)
causes example.html to be returned with a text/plain content type
whereas:
example.html?pipe=header(Content-Type,text/plain,True)
Will cause example.html to be returned with both text/html and
text/plain content-type headers.
### slice ###
Used to send only part of a response body. Takes the start and,
optionally, end bytes as arguments, although either can be null to
indicate the start or end of the file, respectively. So for example:
example.txt?pipe=slice(10,20)
Would result in a response with a body containing 10 bytes of
example.txt including byte 10 but excluding byte 20.
example.txt?pipe=slice(10)
Would cause all bytes from byte 10 of example.txt to be sent, but:
example.txt?pipe=slice(null,20)
Would send the first 20 bytes of example.txt.
### trickle ###
Used to send the body of a response in chunks with delays. Takes a
single argument that is a microsyntax consisting of colon-separated
commands. There are three types of commands:
* Bare numbers represent a number of bytes to send
* Numbers prefixed `d` indicate a delay in seconds
* Numbers prefixed `r` must only appear at the end of the command, and
indicate that the preceding N items must be repeated until there is
no more content to send.
In the absence of a repetition command, the entire remainder of the content is
sent at once when the command list is exhausted. So for example:
example.txt?pipe=trickle(d1)
causes a 1s delay before sending the entirety of example.txt.
example.txt?pipe=trickle(100:d1)
causes 100 bytes of example.txt to be sent, followed by a 1s delay,
and then the remainder of the file to be sent. On the other hand:
example.txt?pipe=trickle(100:d1:r2)
Will cause the file to be sent in 100 byte chunks separated by a 1s
delay until the whole content has been sent.
## asis files ##
Suitable for:
* Static, HTTP-non-compliant responses
asis files are simply files with the extension `.asis`. They are sent
byte for byte to the server without adding a HTTP status line,
headers, or anything else. This makes them suitable for testing
situations where the precise bytes on the wire are static, and control
over the timing is unnecessary, but the response does not conform to
HTTP requirements.
## py files ##
Suitable for:
* All tests requiring dynamic responses
* Tests that need to store server side state.
The most flexible mechanism for writing tests is to use `.py`
files. These are interpreted as code and are suitable for the same
kinds of tasks that one might achieve using cgi, PHP or a similar
technology. Unlike cgi or PHP, the file is not executed directly and
does not produce output by writing to `stdout`. Instead files must
contain (at least) a function named `main`, with the signature:
def main(request, response):
pass
Here `request` is a `Request` object that contains details of the
request, and `response` is a `Response` object that can be used to set
properties of the response. Full details of these objects is
provided in the [wptserve documentation](http://wptserve.readthedocs.org/en/latest/).
In many cases tests will not need to work with the `response` object
directly. Instead they can set the status, headers and body simply by
returning values from the `main` function. If any value is returned,
it is interpreted as the response body. If two values are returned
they are interpreted as headers and body, and three values are
interpreted as status, headers, body. So, for example:
def main(request, response):
return "TEST"
creates a response with no non-default headers and the body
`TEST`. Headers can be added as follows:
def main(request, response):
return ([("Content-Type", "text/plain"), ("X-Test", "test")],
"TEST")
And a status code as:
def main(request, response):
return (410,
[("Content-Type", "text/plain"), ("X-Test", "test")],
"TEST")
A custom status string may be returned by using a tuple `code, string`
in place of the code alone.
At the other end of the scale, some tests require precision over the
exact bytes sent over the wire and their timing. This can be achieved
using the `writer` property of the response, which exposes a
`ResponseWriter` object that allows wither writing specific parts of
the request or direct access to the underlying socket.
For full documentation on the facilities available in `.py` files, see
the [wptserve documentation](http://wptserve.readthedocs.org/en/latest/).