diff options
| author | ianb <devnull@localhost> | 2006-01-04 02:21:25 +0000 |
|---|---|---|
| committer | ianb <devnull@localhost> | 2006-01-04 02:21:25 +0000 |
| commit | 70ad238eca82e78e87ea4ee6948d946a94d0355d (patch) | |
| tree | db9a2a21c5136b98d657b2266b3b0bb1166b805b /paste/fixture.py | |
| parent | 436cf9d49f2bd4e304bc820fe4aeaf17bba1f25e (diff) | |
| download | paste-70ad238eca82e78e87ea4ee6948d946a94d0355d.tar.gz | |
Remove a bunch of cruft that was never fully developed for testing
Diffstat (limited to 'paste/fixture.py')
| -rw-r--r-- | paste/fixture.py | 381 |
1 files changed, 233 insertions, 148 deletions
diff --git a/paste/fixture.py b/paste/fixture.py index bf1ef18..16b7f6d 100644 --- a/paste/fixture.py +++ b/paste/fixture.py @@ -2,6 +2,11 @@ # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ Routines for testing WSGI applications. + +Most interesting is the `TestApp <class-paste.fixture.TestApp.html>`_ +for testing WSGI applications, and the `TestFileEnvironment +<class-paste.fixture.TestFileEnvironment.html>`_ class for testing the +effects of command-line scripts. """ import sys @@ -41,59 +46,6 @@ def tempnam_no_warning(*args): class NoDefault: pass -class Dummy(object): - - def __init__(self, **kw): - for name, value in kw.items(): - if name.startswith('method_'): - name = name[len('method_'):] - value = DummyMethod(value) - setattr(self, name, value) - -class DummyMethod(object): - - def __init__(self, return_value): - self.return_value = return_value - - def __call__(self, *args, **kw): - return self.return_value - -def capture_stdout(func, *args, **kw): - newstdout = StringIO() - oldstdout = sys.stdout - sys.stdout = newstdout - try: - result = func(*args, **kw) - finally: - sys.stdout = oldstdout - return result, newstdout.getvalue() - -def assert_error(func, *args, **kw): - kw.setdefault('error', Exception) - kw.setdefault('text_re', None) - error = kw.pop('error') - text_re = kw.pop('text_re') - if text_re and isinstance(text_re, str): - real_text_re = re.compile(text_re, re.S) - else: - real_text_re = text_re - try: - value = func(*args, **kw) - except error, e: - if real_text_re and not real_text_re.search(str(e)): - assert False, ( - "Exception did not match pattern; exception:\n %r;\n" - "pattern:\n %r" - % (str(e), text_re)) - except Exception, e: - assert False, ( - "Exception type %s should have been raised; got %s instead (%s)" - % (error, e.__class__, e)) - else: - assert False, ( - "Exception was expected, instead successfully returned %r" - % (value)) - def sorted(l): l = list(l) l.sort() @@ -131,66 +83,6 @@ class Dummy_smtplib(object): "SMTP connection not quit") self.__class__.existing = None -class FakeFilesystem(object): - - def __init__(self): - self.files = {} - - def make_file(self, filename, content): - self.files[filename] = content - - def open(self, filename, mode='r'): - if not self.files.has_key(filename): - raise IOError("[FakeFS] No such file or directory: %r" % filename) - - -class FakeFile(object): - - def __init__(self, filename, content=None): - self.filename = filename - self.content = content - - def open(self, mode): - if mode == 'r' or mode == 'rb': - if self.content is None: - raise IOError("[FakeFS] No such file or directory: %r" - % self.filename) - return ReaderFile(self) - elif mode == 'w' or mode == 'wb': - return WriterFile(self) - else: - assert 0, "Mode %r not yet implemented" % mode - -class ReaderFile(object): - - def __init__(self, fp): - self.file = fp - self.stream = StringIO(self.file.content) - self.open = True - - def read(self, *args): - return self.stream.read(*args) - - def close(self): - assert self.open, ( - "Closing open file") - self.open = False - -class WriterFile(object): - - def __init__(self, fp): - self.file = fp - self.stream = StringIO() - self.open = True - - def write(self, arg): - self.stream.write(arg) - - def close(self): - assert self.open, ( - "Closing an open file") - self.open = False - class AppError(Exception): pass @@ -201,6 +93,26 @@ class TestApp(object): def __init__(self, app, namespace=None, relative_to=None, extra_environ=None): + """ + Wraps a WSGI application in a more convenient interface for + testing. + + ``app`` may be an application, or a Paste Deploy app + URI, like ``'config:filename.ini#test'``. + + ``namespace`` is a dictionary that will be written to (if + provided). This can be used with doctest or some other + system, and the variable ``res`` will be assigned everytime + you make a request (instead of returning the request). + + ``relative_to`` is a directory, and filenames used for file + uploads are calculated relative to this. Also ``config:`` + URIs that aren't absolute. + + ``extra_environ`` is a dictionary of values that should go + into the environment for each request. These can provide a + communication channel with the application. + """ if isinstance(app, (str, unicode)): from paste.deploy import loadapp # @@: Should pick up relative_to from calling module's @@ -215,15 +127,49 @@ class TestApp(object): self.reset() def reset(self): + """ + Resets the state of the application; currently just clears + saved cookies. + """ self.cookies = {} - def make_environ(self): + def _make_environ(self): environ = self.extra_environ.copy() environ['paste.throw_errors'] = True return environ def get(self, url, params=None, headers={}, extra_environ={}, status=None, expect_errors=False): + """ + Get the given url (well, actually a path like + ``'/page.html'``). + + ``params``: + A query string, or a dictionary that will be encoded + into a query string. You may also include a query + string on the ``url``. + + ``headers``: + A dictionary of extra headers to send. + + ``extra_environ``: + A dictionary of environmental variables that should + be added to the request. + + ``status``: + The integer status code you expect (if not 200 or 3xx). + If you expect a 404 response, for instance, you must give + ``status=404`` or it will be an error. You can also give + a wildcard, like ``'3*'`` or ``'*'``. + + ``expect_errors``: + If this is not true, then if anything is written to + ``wsgi.errors`` it will be an error. If it is true, then + non-200/3xx responses are also okay. + + Returns a `response object + <class-paste.fixture.TestResponse.html>`_ + """ # Hide from py.test: __tracebackhide__ = True if params: @@ -234,7 +180,7 @@ class TestApp(object): else: url += '?' url += params - environ = self.make_environ() + environ = self._make_environ() for header, value in headers.items(): environ['HTTP_%s' % header.replace('-', '_').upper()] = value if '?' in url: @@ -245,7 +191,19 @@ class TestApp(object): def post(self, url, params=None, headers={}, extra_environ={}, status=None, upload_files=None, expect_errors=False): - environ = self.make_environ() + """ + Do a POST request. Very like the ``.get()`` method. + ``params`` are put in the body of the request. + + ``upload_files`` is for file uploads. It should be a list of + ``[(fieldname, filename, file_content)]``. You can also use + just ``[(fieldname, filename)]`` and the file content will be + read from disk. + + Returns a `response object + <class-paste.fixture.TestResponse.html>`_ + """ + environ = self._make_environ() # @@: Should this be all non-strings? if params and isinstance(params, (list, tuple, dict)): params = urllib.urlencode(params) @@ -277,7 +235,7 @@ class TestApp(object): lines.append('') lines.append(value) for file_info in files: - key, filename, value = self.get_file_info(file_info) + key, filename, value = self._get_file_info(file_info) lines.append('--'+boundary) lines.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) @@ -292,7 +250,7 @@ class TestApp(object): content_type = 'multipart/form-data; boundary=%s' % boundary return content_type, body - def get_file_info(self, file_info): + def _get_file_info(self, file_info): if len(file_info) == 2: # It only has a filename filename = file_info[2] @@ -312,6 +270,11 @@ class TestApp(object): % repr(file_info)[:100]) def do_request(self, req, status): + """ + Executes the given request (``req``), with the expected + ``status``. Generally ``.get()`` and ``.post()`` are used + instead. + """ __tracebackhide__ = True if self.cookies: c = SimpleCookie() @@ -331,15 +294,15 @@ class TestApp(object): finally: sys.stdout = old_stdout sys.stderr.write(out.getvalue()) - res = self.make_response(raw_res, end_time - start_time) + res = self._make_response(raw_res, end_time - start_time) res.request = req for name, value in req.environ['paste.testing_variables'].items(): setattr(res, name, value) if self.namespace is not None: self.namespace['res'] = res if not req.expect_errors: - self.check_status(status, res) - self.check_errors(res) + self._check_status(status, res) + self._check_errors(res) for header in res.all_headers('set-cookie'): c = SimpleCookie(header) for key, morsel in c.items(): @@ -350,7 +313,7 @@ class TestApp(object): # it anywhere return res - def check_status(self, status, res): + def _check_status(self, status, res): __tracebackhide__ = True if status == '*': return @@ -365,12 +328,12 @@ class TestApp(object): raise AppError( "Bad response: %s (not %s)" % (res.full_status, status)) - def check_errors(self, res): + def _check_errors(self, res): if res.errors: raise AppError( "Application had errors logged:\n%s" % res.errors) - def make_response(self, (status, headers, body, errors), total_time): + def _make_response(self, (status, headers, body, errors), total_time): return TestResponse(self, status, headers, body, errors, total_time) @@ -379,6 +342,11 @@ class TestResponse(object): # for py.test disabled = True + """ + Instances of this class are return by `TestApp + <class-paste.fixture.TestApp.html>`_ + """ + def __init__(self, test_app, status, headers, body, errors, total_time): self.test_app = test_app @@ -400,12 +368,25 @@ class TestResponse(object): self._parse_forms() return self._forms_indexed - forms = property(forms__get) + forms = property(forms__get, + doc=""" + A list of <form>s found on the page (instances of + `Form <class-paste.fixture.Form.html>`_) + """) def form__get(self): - return self.forms[0] - - form = property(form__get) + forms = self.forms + assert len(forms) == 1, ( + "You used response.form, but more than one form exists") + return forms[0] + + form = property(form__get, + doc=""" + Returns a single `Form + <class-paste.fixture.Form.html>`_ instance; it + is an error if there are multiple forms on the + page. + """) _tag_re = re.compile(r'<(/?)([a-z0-9_\-]*)(.*?)>', re.S|re.I) @@ -459,7 +440,7 @@ class TestResponse(object): def all_headers(self, name): """ - Gets all headers, returns as a list + Gets all headers by the ``name``, returns as a list """ found = [] for cur_name, value in self.headers: @@ -469,7 +450,9 @@ class TestResponse(object): def follow(self, **kw): """ - If this request is a redirect, follow that redirect. + If this request is a redirect, follow that redirect. It + is an error if this is not a redirect response. Returns + another response object. """ assert self.status >= 300 and self.status < 400, ( "You can only follow redirect responses (not %s)" @@ -553,10 +536,10 @@ class TestResponse(object): href_pattern, html_pattern, index, verbose): - content_pat = make_pattern(content) - id_pat = make_pattern(id) - href_pat = make_pattern(href_pattern) - html_pat = make_pattern(html_pattern) + content_pat = _make_pattern(content) + id_pat = _make_pattern(id) + href_pat = _make_pattern(href_pattern) + html_pat = _make_pattern(html_pattern) _tag_re = re.compile(r'<%s\s+(.*?)>(.*?)</%s>' % (tag, tag), re.I+re.S) @@ -657,7 +640,10 @@ class TestResponse(object): ' ', self.body) return self._normal_body - normal_body = property(normal_body__get) + normal_body = property(normal_body__get, + doc=""" + Return the whitespace-normalized body + """) def __contains__(self, s): """ @@ -714,6 +700,27 @@ class TestRequest(object): # for py.test disabled = True + """ + Instances of this class are created by `TestApp + <class-paste.fixture.TestApp.html>`_ with the ``.get()`` and + ``.post()`` methods, and are consumed there by ``.do_request()``. + + Instances are also available as a ``.req`` attribute on + `TestResponse <class-paste.fixture.TestResponse.html>`_ instances. + + Useful attributes: + + ``url``: + The url (actually usually the path) of the request, without + query string. + + ``environ``: + The environment dictionary used for the request. + + ``full_url``: + The url/path, with query string. + """ + def __init__(self, url, environ, expect_errors=False): if url.startswith('http://localhost'): url = url[len('http://localhost'):] @@ -997,6 +1004,10 @@ class Field(object): class Select(Field): + """ + Field representing ``<select>`` + """ + def __init__(self, *args, **attrs): super(Select, self).__init__(*args, **attrs) self.options = [] @@ -1035,12 +1046,19 @@ class Select(Field): Field.classes['select'] = Select class Radio(Select): - pass + + """ + Field representing ``<input type="radio">`` + """ Field.classes['radio'] = Radio class Checkbox(Field): + """ + Field representing ``<input type="checkbox">`` + """ + def __init__(self, *args, **attrs): super(Checkbox, self).__init__(*args, **attrs) self.checked = 'checked' in attrs @@ -1063,21 +1081,31 @@ class Checkbox(Field): Field.classes['checkbox'] = Checkbox class Text(Field): - pass + """ + Field representing ``<input type="text">`` + """ Field.classes['text'] = Text class Textarea(Text): - pass + """ + Field representing ``<textarea>`` + """ Field.classes['textarea'] = Textarea class Hidden(Text): - settable = False + """ + Field representing ``<input type="hidden">`` + """ Field.classes['hidden'] = Hidden class Submit(Field): + """ + Field representing ``<input type="submit">`` and ``<button>`` + """ + settable = False def value__get(self): @@ -1111,6 +1139,31 @@ class TestFileEnvironment(object): script_path=None, environ=None, cwd=None, start_clear=True, ignore_paths=None, ignore_hidden=True): + """ + Creates an environment. ``base_path`` is used as the current + working directory, and generally where changes are looked for. + + ``template_path`` is the directory to look for *template* + files, which are files you'll explicitly add to the + environment. This is done with ``.writefile()``. + + ``script_path`` is the PATH for finding executables. Usually + grabbed from ``$PATH``. + + ``environ`` is the operating system environment, + ``os.environ`` if not given. + + ``cwd`` is the working directory, ``base_path`` by default. + + If ``start_clear`` is true (default) then the ``base_path`` + will be cleared (all files deleted) when an instance is + created. You can also use ``.clear()`` to clear the files. + + ``ignore_paths`` is a set of specific filenames that should be + ignored when created in the environment. ``ignore_hidden`` + means, if true (default) that filenames and directories + starting with ``'.'`` will be ignored. + """ self.base_path = base_path self.template_path = template_path if environ is None: @@ -1148,7 +1201,8 @@ class TestFileEnvironment(object): ``cwd``: (default ``self.cwd``) The working directory to run in - Returns a ``ProcResponse`` object. + Returns a `ProcResponse + <class-paste.fixture.ProcResponse.html>`_ object. """ __tracebackhide__ = True expect_error = _popget(kw, 'expect_error', False) @@ -1165,16 +1219,16 @@ class TestFileEnvironment(object): "and arguments (%s)" % (script, args)) script, args = script.split(None, 1) args = shlex.split(args) - script = self.find_exe(script) + script = self._find_exe(script) all = [script] + args - files_before = self.find_files() + files_before = self._find_files() proc = subprocess.Popen(all, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd, env=self.environ) stdout, stderr = proc.communicate(stdin) - files_after = self.find_files() + files_after = self._find_files() result = ProcResult( self, all, stdin, stdout, stderr, returncode=proc.returncode, @@ -1189,7 +1243,7 @@ class TestFileEnvironment(object): result.assert_no_stderr() return result - def find_exe(self, script_name): + def _find_exe(self, script_name): if self.script_path is None: script_name = os.path.join(self.cwd, script_name) if not os.path.exists(script_name): @@ -1204,7 +1258,7 @@ class TestFileEnvironment(object): "Script %s could not be found in %s" % (script_name, ':'.join(self.script_path))) - def find_files(self): + def _find_files(self): result = {} for fn in os.listdir(self.base_path): if self._ignore_file(fn): @@ -1265,7 +1319,8 @@ class ProcResult(object): """ Represents the results of running a command in - ``TestFileEnvironment``. + `TestFileEnvironment + <class-paste.fixture.TestFileEnvironment.html>`_. Attributes to pay particular attention to: @@ -1274,7 +1329,8 @@ class ProcResult(object): ``files_created``, ``files_deleted``, ``files_updated``: Dictionaries mapping filenames (relative to the ``base_dir``) - to ``FoundFile`` or ``FoundDir`` objects. + to `FoundFile <class-paste.fixture.FoundFile.html>`_ or + `FoundDir <class-paste.fixture.FoundDir.html>`_ objects. """ def __init__(self, test_env, args, stdin, stdout, stderr, @@ -1340,6 +1396,28 @@ class ProcResult(object): class FoundFile(object): + """ + Represents a single file found as the result of a command. + + Has attributes: + + ``path``: + The path of the file, relative to the ``base_path`` + + ``full``: + The full path + + ``stat``: + The results of ``os.stat``. Also ``mtime`` and ``size`` + contain the ``.st_mtime`` and ``st_size`` of the stat. + + ``bytes``: + The contents of the file. + + You may use the ``in`` operator with these objects (tested against + the contents of the file), and the ``.mustcontain()`` method. + """ + file = True dir = False @@ -1378,6 +1456,10 @@ class FoundFile(object): class FoundDir(object): + """ + Represents a directory created by a command. + """ + file = False dir = True @@ -1426,7 +1508,7 @@ def _space_prefix(pref, full, sep=None, indent=None, include_sep=True): else: return sep.join(full) -def make_pattern(pat): +def _make_pattern(pat): if pat is None: return None if isinstance(pat, (str, unicode)): @@ -1456,6 +1538,9 @@ def setup_module(module=None): module.reset_state() def html_unquote(v): + """ + Unquote (some) entities in HTML. (incomplete) + """ for ent, repl in [(' ', ' '), ('>', '>'), ('<', '<'), ('"', '"'), ('&', '&')]: |
