Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 """
      4 Routines for testing WSGI applications.
      5 
      6 Most interesting is the `TestApp <class-paste.fixture.TestApp.html>`_
      7 for testing WSGI applications, and the `TestFileEnvironment
      8 <class-paste.fixture.TestFileEnvironment.html>`_ class for testing the
      9 effects of command-line scripts.
     10 """
     11 
     12 from __future__ import print_function
     13 
     14 import sys
     15 import random
     16 import mimetypes
     17 import time
     18 import os
     19 import shutil
     20 import smtplib
     21 import shlex
     22 import re
     23 import six
     24 import subprocess
     25 from six.moves import cStringIO as StringIO
     26 from six.moves.urllib.parse import urlencode
     27 from six.moves.urllib import parse as urlparse
     28 try:
     29     # Python 3
     30     from http.cookies import BaseCookie
     31     from urllib.parse import splittype, splithost
     32 except ImportError:
     33     # Python 2
     34     from Cookie import BaseCookie
     35     from urllib import splittype, splithost
     36 
     37 from paste import wsgilib
     38 from paste import lint
     39 from paste.response import HeaderDict
     40 
     41 def tempnam_no_warning(*args):
     42     """
     43     An os.tempnam with the warning turned off, because sometimes
     44     you just need to use this and don't care about the stupid
     45     security warning.
     46     """
     47     return os.tempnam(*args)
     48 
     49 class NoDefault(object):
     50     pass
     51 
     52 def sorted(l):
     53     l = list(l)
     54     l.sort()
     55     return l
     56 
     57 class Dummy_smtplib(object):
     58 
     59     existing = None
     60 
     61     def __init__(self, server):
     62         import warnings
     63         warnings.warn(
     64             'Dummy_smtplib is not maintained and is deprecated',
     65             DeprecationWarning, 2)
     66         assert not self.existing, (
     67             "smtplib.SMTP() called again before Dummy_smtplib.existing.reset() "
     68             "called.")
     69         self.server = server
     70         self.open = True
     71         self.__class__.existing = self
     72 
     73     def quit(self):
     74         assert self.open, (
     75             "Called %s.quit() twice" % self)
     76         self.open = False
     77 
     78     def sendmail(self, from_address, to_addresses, msg):
     79         self.from_address = from_address
     80         self.to_addresses = to_addresses
     81         self.message = msg
     82 
     83     def install(cls):
     84         smtplib.SMTP = cls
     85 
     86     install = classmethod(install)
     87 
     88     def reset(self):
     89         assert not self.open, (
     90             "SMTP connection not quit")
     91         self.__class__.existing = None
     92 
     93 class AppError(Exception):
     94     pass
     95 
     96 class TestApp(object):
     97 
     98     # for py.test
     99     disabled = True
    100 
    101     def __init__(self, app, namespace=None, relative_to=None,
    102                  extra_environ=None, pre_request_hook=None,
    103                  post_request_hook=None):
    104         """
    105         Wraps a WSGI application in a more convenient interface for
    106         testing.
    107 
    108         ``app`` may be an application, or a Paste Deploy app
    109         URI, like ``'config:filename.ini#test'``.
    110 
    111         ``namespace`` is a dictionary that will be written to (if
    112         provided).  This can be used with doctest or some other
    113         system, and the variable ``res`` will be assigned everytime
    114         you make a request (instead of returning the request).
    115 
    116         ``relative_to`` is a directory, and filenames used for file
    117         uploads are calculated relative to this.  Also ``config:``
    118         URIs that aren't absolute.
    119 
    120         ``extra_environ`` is a dictionary of values that should go
    121         into the environment for each request.  These can provide a
    122         communication channel with the application.
    123 
    124         ``pre_request_hook`` is a function to be called prior to
    125         making requests (such as ``post`` or ``get``). This function
    126         must take one argument (the instance of the TestApp).
    127 
    128         ``post_request_hook`` is a function, similar to
    129         ``pre_request_hook``, to be called after requests are made.
    130         """
    131         if isinstance(app, (six.binary_type, six.text_type)):
    132             from paste.deploy import loadapp
    133             # @@: Should pick up relative_to from calling module's
    134             # __file__
    135             app = loadapp(app, relative_to=relative_to)
    136         self.app = app
    137         self.namespace = namespace
    138         self.relative_to = relative_to
    139         if extra_environ is None:
    140             extra_environ = {}
    141         self.extra_environ = extra_environ
    142         self.pre_request_hook = pre_request_hook
    143         self.post_request_hook = post_request_hook
    144         self.reset()
    145 
    146     def reset(self):
    147         """
    148         Resets the state of the application; currently just clears
    149         saved cookies.
    150         """
    151         self.cookies = {}
    152 
    153     def _make_environ(self):
    154         environ = self.extra_environ.copy()
    155         environ['paste.throw_errors'] = True
    156         return environ
    157 
    158     def get(self, url, params=None, headers=None, extra_environ=None,
    159             status=None, expect_errors=False):
    160         """
    161         Get the given url (well, actually a path like
    162         ``'/page.html'``).
    163 
    164         ``params``:
    165             A query string, or a dictionary that will be encoded
    166             into a query string.  You may also include a query
    167             string on the ``url``.
    168 
    169         ``headers``:
    170             A dictionary of extra headers to send.
    171 
    172         ``extra_environ``:
    173             A dictionary of environmental variables that should
    174             be added to the request.
    175 
    176         ``status``:
    177             The integer status code you expect (if not 200 or 3xx).
    178             If you expect a 404 response, for instance, you must give
    179             ``status=404`` or it will be an error.  You can also give
    180             a wildcard, like ``'3*'`` or ``'*'``.
    181 
    182         ``expect_errors``:
    183             If this is not true, then if anything is written to
    184             ``wsgi.errors`` it will be an error.  If it is true, then
    185             non-200/3xx responses are also okay.
    186 
    187         Returns a `response object
    188         <class-paste.fixture.TestResponse.html>`_
    189         """
    190         if extra_environ is None:
    191             extra_environ = {}
    192         # Hide from py.test:
    193         __tracebackhide__ = True
    194         if params:
    195             if not isinstance(params, (six.binary_type, six.text_type)):
    196                 params = urlencode(params, doseq=True)
    197             if '?' in url:
    198                 url += '&'
    199             else:
    200                 url += '?'
    201             url += params
    202         environ = self._make_environ()
    203         url = str(url)
    204         if '?' in url:
    205             url, environ['QUERY_STRING'] = url.split('?', 1)
    206         else:
    207             environ['QUERY_STRING'] = ''
    208         self._set_headers(headers, environ)
    209         environ.update(extra_environ)
    210         req = TestRequest(url, environ, expect_errors)
    211         return self.do_request(req, status=status)
    212 
    213     def _gen_request(self, method, url, params=b'', headers=None, extra_environ=None,
    214              status=None, upload_files=None, expect_errors=False):
    215         """
    216         Do a generic request.
    217         """
    218         if headers is None:
    219             headers = {}
    220         if extra_environ is None:
    221             extra_environ = {}
    222         environ = self._make_environ()
    223         # @@: Should this be all non-strings?
    224         if isinstance(params, (list, tuple, dict)):
    225             params = urlencode(params)
    226         if hasattr(params, 'items'):
    227             # Some other multi-dict like format
    228             params = urlencode(params.items())
    229             if six.PY3:
    230                 params = params.encode('utf8')
    231         if upload_files:
    232             params = urlparse.parse_qsl(params, keep_blank_values=True)
    233             content_type, params = self.encode_multipart(
    234                 params, upload_files)
    235             environ['CONTENT_TYPE'] = content_type
    236         elif params:
    237             environ.setdefault('CONTENT_TYPE', 'application/x-www-form-urlencoded')
    238         if '?' in url:
    239             url, environ['QUERY_STRING'] = url.split('?', 1)
    240         else:
    241             environ['QUERY_STRING'] = ''
    242         environ['CONTENT_LENGTH'] = str(len(params))
    243         environ['REQUEST_METHOD'] = method
    244         environ['wsgi.input'] = six.BytesIO(params)
    245         self._set_headers(headers, environ)
    246         environ.update(extra_environ)
    247         req = TestRequest(url, environ, expect_errors)
    248         return self.do_request(req, status=status)
    249 
    250     def post(self, url, params=b'', headers=None, extra_environ=None,
    251              status=None, upload_files=None, expect_errors=False):
    252         """
    253         Do a POST request.  Very like the ``.get()`` method.
    254         ``params`` are put in the body of the request.
    255 
    256         ``upload_files`` is for file uploads.  It should be a list of
    257         ``[(fieldname, filename, file_content)]``.  You can also use
    258         just ``[(fieldname, filename)]`` and the file content will be
    259         read from disk.
    260 
    261         Returns a `response object
    262         <class-paste.fixture.TestResponse.html>`_
    263         """
    264         return self._gen_request('POST', url, params=params, headers=headers,
    265                                  extra_environ=extra_environ,status=status,
    266                                  upload_files=upload_files,
    267                                  expect_errors=expect_errors)
    268 
    269     def put(self, url, params=b'', headers=None, extra_environ=None,
    270              status=None, upload_files=None, expect_errors=False):
    271         """
    272         Do a PUT request.  Very like the ``.get()`` method.
    273         ``params`` are put in the body of the request.
    274 
    275         ``upload_files`` is for file uploads.  It should be a list of
    276         ``[(fieldname, filename, file_content)]``.  You can also use
    277         just ``[(fieldname, filename)]`` and the file content will be
    278         read from disk.
    279 
    280         Returns a `response object
    281         <class-paste.fixture.TestResponse.html>`_
    282         """
    283         return self._gen_request('PUT', url, params=params, headers=headers,
    284                                  extra_environ=extra_environ,status=status,
    285                                  upload_files=upload_files,
    286                                  expect_errors=expect_errors)
    287 
    288     def delete(self, url, params=b'', headers=None, extra_environ=None,
    289                status=None, expect_errors=False):
    290         """
    291         Do a DELETE request.  Very like the ``.get()`` method.
    292         ``params`` are put in the body of the request.
    293 
    294         Returns a `response object
    295         <class-paste.fixture.TestResponse.html>`_
    296         """
    297         return self._gen_request('DELETE', url, params=params, headers=headers,
    298                                  extra_environ=extra_environ,status=status,
    299                                  upload_files=None, expect_errors=expect_errors)
    300 
    301 
    302 
    303 
    304     def _set_headers(self, headers, environ):
    305         """
    306         Turn any headers into environ variables
    307         """
    308         if not headers:
    309             return
    310         for header, value in headers.items():
    311             if header.lower() == 'content-type':
    312                 var = 'CONTENT_TYPE'
    313             elif header.lower() == 'content-length':
    314                 var = 'CONTENT_LENGTH'
    315             else:
    316                 var = 'HTTP_%s' % header.replace('-', '_').upper()
    317             environ[var] = value
    318 
    319     def encode_multipart(self, params, files):
    320         """
    321         Encodes a set of parameters (typically a name/value list) and
    322         a set of files (a list of (name, filename, file_body)) into a
    323         typical POST body, returning the (content_type, body).
    324         """
    325         boundary = '----------a_BoUnDaRy%s$' % random.random()
    326         content_type = 'multipart/form-data; boundary=%s' % boundary
    327         if six.PY3:
    328             boundary = boundary.encode('ascii')
    329 
    330         lines = []
    331         for key, value in params:
    332             lines.append(b'--'+boundary)
    333             line = 'Content-Disposition: form-data; name="%s"' % key
    334             if six.PY3:
    335                 line = line.encode('utf8')
    336             lines.append(line)
    337             lines.append(b'')
    338             line = value
    339             if six.PY3 and isinstance(line, six.text_type):
    340                 line = line.encode('utf8')
    341             lines.append(line)
    342         for file_info in files:
    343             key, filename, value = self._get_file_info(file_info)
    344             lines.append(b'--'+boundary)
    345             line = ('Content-Disposition: form-data; name="%s"; filename="%s"'
    346                          % (key, filename))
    347             if six.PY3:
    348                 line = line.encode('utf8')
    349             lines.append(line)
    350             fcontent = mimetypes.guess_type(filename)[0]
    351             line = ('Content-Type: %s'
    352                     % (fcontent or 'application/octet-stream'))
    353             if six.PY3:
    354                 line = line.encode('utf8')
    355             lines.append(line)
    356             lines.append(b'')
    357             lines.append(value)
    358         lines.append(b'--' + boundary + b'--')
    359         lines.append(b'')
    360         body = b'\r\n'.join(lines)
    361         return content_type, body
    362 
    363     def _get_file_info(self, file_info):
    364         if len(file_info) == 2:
    365             # It only has a filename
    366             filename = file_info[1]
    367             if self.relative_to:
    368                 filename = os.path.join(self.relative_to, filename)
    369             f = open(filename, 'rb')
    370             content = f.read()
    371             f.close()
    372             return (file_info[0], filename, content)
    373         elif len(file_info) == 3:
    374             return file_info
    375         else:
    376             raise ValueError(
    377                 "upload_files need to be a list of tuples of (fieldname, "
    378                 "filename, filecontent) or (fieldname, filename); "
    379                 "you gave: %r"
    380                 % repr(file_info)[:100])
    381 
    382     def do_request(self, req, status):
    383         """
    384         Executes the given request (``req``), with the expected
    385         ``status``.  Generally ``.get()`` and ``.post()`` are used
    386         instead.
    387         """
    388         if self.pre_request_hook:
    389             self.pre_request_hook(self)
    390         __tracebackhide__ = True
    391         if self.cookies:
    392             c = BaseCookie()
    393             for name, value in self.cookies.items():
    394                 c[name] = value
    395             hc = '; '.join(['='.join([m.key, m.value]) for m in c.values()])
    396             req.environ['HTTP_COOKIE'] = hc
    397         req.environ['paste.testing'] = True
    398         req.environ['paste.testing_variables'] = {}
    399         app = lint.middleware(self.app)
    400         old_stdout = sys.stdout
    401         out = CaptureStdout(old_stdout)
    402         try:
    403             sys.stdout = out
    404             start_time = time.time()
    405             raise_on_wsgi_error = not req.expect_errors
    406             raw_res = wsgilib.raw_interactive(
    407                 app, req.url,
    408                 raise_on_wsgi_error=raise_on_wsgi_error,
    409                 **req.environ)
    410             end_time = time.time()
    411         finally:
    412             sys.stdout = old_stdout
    413             sys.stderr.write(out.getvalue())
    414         res = self._make_response(raw_res, end_time - start_time)
    415         res.request = req
    416         for name, value in req.environ['paste.testing_variables'].items():
    417             if hasattr(res, name):
    418                 raise ValueError(
    419                     "paste.testing_variables contains the variable %r, but "
    420                     "the response object already has an attribute by that "
    421                     "name" % name)
    422             setattr(res, name, value)
    423         if self.namespace is not None:
    424             self.namespace['res'] = res
    425         if not req.expect_errors:
    426             self._check_status(status, res)
    427             self._check_errors(res)
    428         res.cookies_set = {}
    429         for header in res.all_headers('set-cookie'):
    430             c = BaseCookie(header)
    431             for key, morsel in c.items():
    432                 self.cookies[key] = morsel.value
    433                 res.cookies_set[key] = morsel.value
    434         if self.post_request_hook:
    435             self.post_request_hook(self)
    436         if self.namespace is None:
    437             # It's annoying to return the response in doctests, as it'll
    438             # be printed, so we only return it is we couldn't assign
    439             # it anywhere
    440             return res
    441 
    442     def _check_status(self, status, res):
    443         __tracebackhide__ = True
    444         if status == '*':
    445             return
    446         if isinstance(status, (list, tuple)):
    447             if res.status not in status:
    448                 raise AppError(
    449                     "Bad response: %s (not one of %s for %s)\n%s"
    450                     % (res.full_status, ', '.join(map(str, status)),
    451                        res.request.url, res.body))
    452             return
    453         if status is None:
    454             if res.status >= 200 and res.status < 400:
    455                 return
    456             body = res.body
    457             if six.PY3:
    458                 body = body.decode('utf8', 'xmlcharrefreplace')
    459             raise AppError(
    460                 "Bad response: %s (not 200 OK or 3xx redirect for %s)\n%s"
    461                 % (res.full_status, res.request.url,
    462                    body))
    463         if status != res.status:
    464             raise AppError(
    465                 "Bad response: %s (not %s)" % (res.full_status, status))
    466 
    467     def _check_errors(self, res):
    468         if res.errors:
    469             raise AppError(
    470                 "Application had errors logged:\n%s" % res.errors)
    471 
    472     def _make_response(self, resp, total_time):
    473         status, headers, body, errors = resp
    474         return TestResponse(self, status, headers, body, errors,
    475                             total_time)
    476 
    477 class CaptureStdout(object):
    478 
    479     def __init__(self, actual):
    480         self.captured = StringIO()
    481         self.actual = actual
    482 
    483     def write(self, s):
    484         self.captured.write(s)
    485         self.actual.write(s)
    486 
    487     def flush(self):
    488         self.actual.flush()
    489 
    490     def writelines(self, lines):
    491         for item in lines:
    492             self.write(item)
    493 
    494     def getvalue(self):
    495         return self.captured.getvalue()
    496 
    497 class TestResponse(object):
    498 
    499     # for py.test
    500     disabled = True
    501 
    502     """
    503     Instances of this class are return by `TestApp
    504     <class-paste.fixture.TestApp.html>`_
    505     """
    506 
    507     def __init__(self, test_app, status, headers, body, errors,
    508                  total_time):
    509         self.test_app = test_app
    510         self.status = int(status.split()[0])
    511         self.full_status = status
    512         self.headers = headers
    513         self.header_dict = HeaderDict.fromlist(self.headers)
    514         self.body = body
    515         self.errors = errors
    516         self._normal_body = None
    517         self.time = total_time
    518         self._forms_indexed = None
    519 
    520     def forms__get(self):
    521         """
    522         Returns a dictionary of ``Form`` objects.  Indexes are both in
    523         order (from zero) and by form id (if the form is given an id).
    524         """
    525         if self._forms_indexed is None:
    526             self._parse_forms()
    527         return self._forms_indexed
    528 
    529     forms = property(forms__get,
    530                      doc="""
    531                      A list of <form>s found on the page (instances of
    532                      `Form <class-paste.fixture.Form.html>`_)
    533                      """)
    534 
    535     def form__get(self):
    536         forms = self.forms
    537         if not forms:
    538             raise TypeError(
    539                 "You used response.form, but no forms exist")
    540         if 1 in forms:
    541             # There is more than one form
    542             raise TypeError(
    543                 "You used response.form, but more than one form exists")
    544         return forms[0]
    545 
    546     form = property(form__get,
    547                     doc="""
    548                     Returns a single `Form
    549                     <class-paste.fixture.Form.html>`_ instance; it
    550                     is an error if there are multiple forms on the
    551                     page.
    552                     """)
    553 
    554     _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)(.*?)>', re.S|re.I)
    555 
    556     def _parse_forms(self):
    557         forms = self._forms_indexed = {}
    558         form_texts = []
    559         started = None
    560         for match in self._tag_re.finditer(self.body):
    561             end = match.group(1) == '/'
    562             tag = match.group(2).lower()
    563             if tag != 'form':
    564                 continue
    565             if end:
    566                 assert started, (
    567                     "</form> unexpected at %s" % match.start())
    568                 form_texts.append(self.body[started:match.end()])
    569                 started = None
    570             else:
    571                 assert not started, (
    572                     "Nested form tags at %s" % match.start())
    573                 started = match.start()
    574         assert not started, (
    575             "Danging form: %r" % self.body[started:])
    576         for i, text in enumerate(form_texts):
    577             form = Form(self, text)
    578             forms[i] = form
    579             if form.id:
    580                 forms[form.id] = form
    581 
    582     def header(self, name, default=NoDefault):
    583         """
    584         Returns the named header; an error if there is not exactly one
    585         matching header (unless you give a default -- always an error
    586         if there is more than one header)
    587         """
    588         found = None
    589         for cur_name, value in self.headers:
    590             if cur_name.lower() == name.lower():
    591                 assert not found, (
    592                     "Ambiguous header: %s matches %r and %r"
    593                     % (name, found, value))
    594                 found = value
    595         if found is None:
    596             if default is NoDefault:
    597                 raise KeyError(
    598                     "No header found: %r (from %s)"
    599                     % (name, ', '.join([n for n, v in self.headers])))
    600             else:
    601                 return default
    602         return found
    603 
    604     def all_headers(self, name):
    605         """
    606         Gets all headers by the ``name``, returns as a list
    607         """
    608         found = []
    609         for cur_name, value in self.headers:
    610             if cur_name.lower() == name.lower():
    611                 found.append(value)
    612         return found
    613 
    614     def follow(self, **kw):
    615         """
    616         If this request is a redirect, follow that redirect.  It
    617         is an error if this is not a redirect response.  Returns
    618         another response object.
    619         """
    620         assert self.status >= 300 and self.status < 400, (
    621             "You can only follow redirect responses (not %s)"
    622             % self.full_status)
    623         location = self.header('location')
    624         type, rest = splittype(location)
    625         host, path = splithost(rest)
    626         # @@: We should test that it's not a remote redirect
    627         return self.test_app.get(location, **kw)
    628 
    629     def click(self, description=None, linkid=None, href=None,
    630               anchor=None, index=None, verbose=False):
    631         """
    632         Click the link as described.  Each of ``description``,
    633         ``linkid``, and ``url`` are *patterns*, meaning that they are
    634         either strings (regular expressions), compiled regular
    635         expressions (objects with a ``search`` method), or callables
    636         returning true or false.
    637 
    638         All the given patterns are ANDed together:
    639 
    640         * ``description`` is a pattern that matches the contents of the
    641           anchor (HTML and all -- everything between ``<a...>`` and
    642           ``</a>``)
    643 
    644         * ``linkid`` is a pattern that matches the ``id`` attribute of
    645           the anchor.  It will receive the empty string if no id is
    646           given.
    647 
    648         * ``href`` is a pattern that matches the ``href`` of the anchor;
    649           the literal content of that attribute, not the fully qualified
    650           attribute.
    651 
    652         * ``anchor`` is a pattern that matches the entire anchor, with
    653           its contents.
    654 
    655         If more than one link matches, then the ``index`` link is
    656         followed.  If ``index`` is not given and more than one link
    657         matches, or if no link matches, then ``IndexError`` will be
    658         raised.
    659 
    660         If you give ``verbose`` then messages will be printed about
    661         each link, and why it does or doesn't match.  If you use
    662         ``app.click(verbose=True)`` you'll see a list of all the
    663         links.
    664 
    665         You can use multiple criteria to essentially assert multiple
    666         aspects about the link, e.g., where the link's destination is.
    667         """
    668         __tracebackhide__ = True
    669         found_html, found_desc, found_attrs = self._find_element(
    670             tag='a', href_attr='href',
    671             href_extract=None,
    672             content=description,
    673             id=linkid,
    674             href_pattern=href,
    675             html_pattern=anchor,
    676             index=index, verbose=verbose)
    677         return self.goto(found_attrs['uri'])
    678 
    679     def clickbutton(self, description=None, buttonid=None, href=None,
    680                     button=None, index=None, verbose=False):
    681         """
    682         Like ``.click()``, except looks for link-like buttons.
    683         This kind of button should look like
    684         ``<button onclick="...location.href='url'...">``.
    685         """
    686         __tracebackhide__ = True
    687         found_html, found_desc, found_attrs = self._find_element(
    688             tag='button', href_attr='onclick',
    689             href_extract=re.compile(r"location\.href='(.*?)'"),
    690             content=description,
    691             id=buttonid,
    692             href_pattern=href,
    693             html_pattern=button,
    694             index=index, verbose=verbose)
    695         return self.goto(found_attrs['uri'])
    696 
    697     def _find_element(self, tag, href_attr, href_extract,
    698                       content, id,
    699                       href_pattern,
    700                       html_pattern,
    701                       index, verbose):
    702         content_pat = _make_pattern(content)
    703         id_pat = _make_pattern(id)
    704         href_pat = _make_pattern(href_pattern)
    705         html_pat = _make_pattern(html_pattern)
    706 
    707         _tag_re = re.compile(r'<%s\s+(.*?)>(.*?)</%s>' % (tag, tag),
    708                              re.I+re.S)
    709 
    710         def printlog(s):
    711             if verbose:
    712                 print(s)
    713 
    714         found_links = []
    715         total_links = 0
    716         for match in _tag_re.finditer(self.body):
    717             el_html = match.group(0)
    718             el_attr = match.group(1)
    719             el_content = match.group(2)
    720             attrs = _parse_attrs(el_attr)
    721             if verbose:
    722                 printlog('Element: %r' % el_html)
    723             if not attrs.get(href_attr):
    724                 printlog('  Skipped: no %s attribute' % href_attr)
    725                 continue
    726             el_href = attrs[href_attr]
    727             if href_extract:
    728                 m = href_extract.search(el_href)
    729                 if not m:
    730                     printlog("  Skipped: doesn't match extract pattern")
    731                     continue
    732                 el_href = m.group(1)
    733             attrs['uri'] = el_href
    734             if el_href.startswith('#'):
    735                 printlog('  Skipped: only internal fragment href')
    736                 continue
    737             if el_href.startswith('javascript:'):
    738                 printlog('  Skipped: cannot follow javascript:')
    739                 continue
    740             total_links += 1
    741             if content_pat and not content_pat(el_content):
    742                 printlog("  Skipped: doesn't match description")
    743                 continue
    744             if id_pat and not id_pat(attrs.get('id', '')):
    745                 printlog("  Skipped: doesn't match id")
    746                 continue
    747             if href_pat and not href_pat(el_href):
    748                 printlog("  Skipped: doesn't match href")
    749                 continue
    750             if html_pat and not html_pat(el_html):
    751                 printlog("  Skipped: doesn't match html")
    752                 continue
    753             printlog("  Accepted")
    754             found_links.append((el_html, el_content, attrs))
    755         if not found_links:
    756             raise IndexError(
    757                 "No matching elements found (from %s possible)"
    758                 % total_links)
    759         if index is None:
    760             if len(found_links) > 1:
    761                 raise IndexError(
    762                     "Multiple links match: %s"
    763                     % ', '.join([repr(anc) for anc, d, attr in found_links]))
    764             found_link = found_links[0]
    765         else:
    766             try:
    767                 found_link = found_links[index]
    768             except IndexError:
    769                 raise IndexError(
    770                     "Only %s (out of %s) links match; index %s out of range"
    771                     % (len(found_links), total_links, index))
    772         return found_link
    773 
    774     def goto(self, href, method='get', **args):
    775         """
    776         Go to the (potentially relative) link ``href``, using the
    777         given method (``'get'`` or ``'post'``) and any extra arguments
    778         you want to pass to the ``app.get()`` or ``app.post()``
    779         methods.
    780 
    781         All hostnames and schemes will be ignored.
    782         """
    783         scheme, host, path, query, fragment = urlparse.urlsplit(href)
    784         # We
    785         scheme = host = fragment = ''
    786         href = urlparse.urlunsplit((scheme, host, path, query, fragment))
    787         href = urlparse.urljoin(self.request.full_url, href)
    788         method = method.lower()
    789         assert method in ('get', 'post'), (
    790             'Only "get" or "post" are allowed for method (you gave %r)'
    791             % method)
    792         if method == 'get':
    793             method = self.test_app.get
    794         else:
    795             method = self.test_app.post
    796         return method(href, **args)
    797 
    798     _normal_body_regex = re.compile(br'[ \n\r\t]+')
    799 
    800     def normal_body__get(self):
    801         if self._normal_body is None:
    802             self._normal_body = self._normal_body_regex.sub(
    803                 b' ', self.body)
    804         return self._normal_body
    805 
    806     normal_body = property(normal_body__get,
    807                            doc="""
    808                            Return the whitespace-normalized body
    809                            """)
    810 
    811     def __contains__(self, s):
    812         """
    813         A response 'contains' a string if it is present in the body
    814         of the response.  Whitespace is normalized when searching
    815         for a string.
    816         """
    817         if not isinstance(s, (six.binary_type, six.text_type)):
    818             s = str(s)
    819         if isinstance(s, six.text_type):
    820             ## FIXME: we don't know that this response uses utf8:
    821             s = s.encode('utf8')
    822         return (self.body.find(s) != -1
    823                 or self.normal_body.find(s) != -1)
    824 
    825     def mustcontain(self, *strings, **kw):
    826         """
    827         Assert that the response contains all of the strings passed
    828         in as arguments.
    829 
    830         Equivalent to::
    831 
    832             assert string in res
    833         """
    834         if 'no' in kw:
    835             no = kw['no']
    836             del kw['no']
    837             if isinstance(no, (six.binary_type, six.text_type)):
    838                 no = [no]
    839         else:
    840             no = []
    841         if kw:
    842             raise TypeError(
    843                 "The only keyword argument allowed is 'no'")
    844         for s in strings:
    845             if not s in self:
    846                 print("Actual response (no %r):" % s, file=sys.stderr)
    847                 print(self, file=sys.stderr)
    848                 raise IndexError(
    849                     "Body does not contain string %r" % s)
    850         for no_s in no:
    851             if no_s in self:
    852                 print("Actual response (has %r)" % no_s, file=sys.stderr)
    853                 print(self, file=sys.stderr)
    854                 raise IndexError(
    855                     "Body contains string %r" % s)
    856 
    857     def __repr__(self):
    858         body = self.body
    859         if six.PY3:
    860             body = body.decode('utf8', 'xmlcharrefreplace')
    861         body = body[:20]
    862         return '<Response %s %r>' % (self.full_status, body)
    863 
    864     def __str__(self):
    865         simple_body = b'\n'.join([l for l in self.body.splitlines()
    866                                  if l.strip()])
    867         if six.PY3:
    868             simple_body = simple_body.decode('utf8', 'xmlcharrefreplace')
    869         return 'Response: %s\n%s\n%s' % (
    870             self.status,
    871             '\n'.join(['%s: %s' % (n, v) for n, v in self.headers]),
    872             simple_body)
    873 
    874     def showbrowser(self):
    875         """
    876         Show this response in a browser window (for debugging purposes,
    877         when it's hard to read the HTML).
    878         """
    879         import webbrowser
    880         fn = tempnam_no_warning(None, 'paste-fixture') + '.html'
    881         f = open(fn, 'wb')
    882         f.write(self.body)
    883         f.close()
    884         url = 'file:' + fn.replace(os.sep, '/')
    885         webbrowser.open_new(url)
    886 
    887 class TestRequest(object):
    888 
    889     # for py.test
    890     disabled = True
    891 
    892     """
    893     Instances of this class are created by `TestApp
    894     <class-paste.fixture.TestApp.html>`_ with the ``.get()`` and
    895     ``.post()`` methods, and are consumed there by ``.do_request()``.
    896 
    897     Instances are also available as a ``.req`` attribute on
    898     `TestResponse <class-paste.fixture.TestResponse.html>`_ instances.
    899 
    900     Useful attributes:
    901 
    902     ``url``:
    903         The url (actually usually the path) of the request, without
    904         query string.
    905 
    906     ``environ``:
    907         The environment dictionary used for the request.
    908 
    909     ``full_url``:
    910         The url/path, with query string.
    911     """
    912 
    913     def __init__(self, url, environ, expect_errors=False):
    914         if url.startswith('http://localhost'):
    915             url = url[len('http://localhost'):]
    916         self.url = url
    917         self.environ = environ
    918         if environ.get('QUERY_STRING'):
    919             self.full_url = url + '?' + environ['QUERY_STRING']
    920         else:
    921             self.full_url = url
    922         self.expect_errors = expect_errors
    923 
    924 
    925 class Form(object):
    926 
    927     """
    928     This object represents a form that has been found in a page.
    929     This has a couple useful attributes:
    930 
    931     ``text``:
    932         the full HTML of the form.
    933 
    934     ``action``:
    935         the relative URI of the action.
    936 
    937     ``method``:
    938         the method (e.g., ``'GET'``).
    939 
    940     ``id``:
    941         the id, or None if not given.
    942 
    943     ``fields``:
    944         a dictionary of fields, each value is a list of fields by
    945         that name.  ``<input type=\"radio\">`` and ``<select>`` are
    946         both represented as single fields with multiple options.
    947     """
    948 
    949     # @@: This really should be using Mechanize/ClientForm or
    950     # something...
    951 
    952     _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)([^>]*?)>', re.I)
    953 
    954     def __init__(self, response, text):
    955         self.response = response
    956         self.text = text
    957         self._parse_fields()
    958         self._parse_action()
    959 
    960     def _parse_fields(self):
    961         in_select = None
    962         in_textarea = None
    963         fields = {}
    964         for match in self._tag_re.finditer(self.text):
    965             end = match.group(1) == '/'
    966             tag = match.group(2).lower()
    967             if tag not in ('input', 'select', 'option', 'textarea',
    968                            'button'):
    969                 continue
    970             if tag == 'select' and end:
    971                 assert in_select, (
    972                     '%r without starting select' % match.group(0))
    973                 in_select = None
    974                 continue
    975             if tag == 'textarea' and end:
    976                 assert in_textarea, (
    977                     "</textarea> with no <textarea> at %s" % match.start())
    978                 in_textarea[0].value = html_unquote(self.text[in_textarea[1]:match.start()])
    979                 in_textarea = None
    980                 continue
    981             if end:
    982                 continue
    983             attrs = _parse_attrs(match.group(3))
    984             if 'name' in attrs:
    985                 name = attrs.pop('name')
    986             else:
    987                 name = None
    988             if tag == 'option':
    989                 in_select.options.append((attrs.get('value'),
    990                                           'selected' in attrs))
    991                 continue
    992             if tag == 'input' and attrs.get('type') == 'radio':
    993                 field = fields.get(name)
    994                 if not field:
    995                     field = Radio(self, tag, name, match.start(), **attrs)
    996                     fields.setdefault(name, []).append(field)
    997                 else:
    998                     field = field[0]
    999                     assert isinstance(field, Radio)
   1000                 field.options.append((attrs.get('value'),
   1001                                       'checked' in attrs))
   1002                 continue
   1003             tag_type = tag
   1004             if tag == 'input':
   1005                 tag_type = attrs.get('type', 'text').lower()
   1006             FieldClass = Field.classes.get(tag_type, Field)
   1007             field = FieldClass(self, tag, name, match.start(), **attrs)
   1008             if tag == 'textarea':
   1009                 assert not in_textarea, (
   1010                     "Nested textareas: %r and %r"
   1011                     % (in_textarea, match.group(0)))
   1012                 in_textarea = field, match.end()
   1013             elif tag == 'select':
   1014                 assert not in_select, (
   1015                     "Nested selects: %r and %r"
   1016                     % (in_select, match.group(0)))
   1017                 in_select = field
   1018             fields.setdefault(name, []).append(field)
   1019         self.fields = fields
   1020 
   1021     def _parse_action(self):
   1022         self.action = None
   1023         for match in self._tag_re.finditer(self.text):
   1024             end = match.group(1) == '/'
   1025             tag = match.group(2).lower()
   1026             if tag != 'form':
   1027                 continue
   1028             if end:
   1029                 break
   1030             attrs = _parse_attrs(match.group(3))
   1031             self.action = attrs.get('action', '')
   1032             self.method = attrs.get('method', 'GET')
   1033             self.id = attrs.get('id')
   1034             # @@: enctype?
   1035         else:
   1036             assert 0, "No </form> tag found"
   1037         assert self.action is not None, (
   1038             "No <form> tag found")
   1039 
   1040     def __setitem__(self, name, value):
   1041         """
   1042         Set the value of the named field.  If there is 0 or multiple
   1043         fields by that name, it is an error.
   1044 
   1045         Setting the value of a ``<select>`` selects the given option
   1046         (and confirms it is an option).  Setting radio fields does the
   1047         same.  Checkboxes get boolean values.  You cannot set hidden
   1048         fields or buttons.
   1049 
   1050         Use ``.set()`` if there is any ambiguity and you must provide
   1051         an index.
   1052         """
   1053         fields = self.fields.get(name)
   1054         assert fields is not None, (
   1055             "No field by the name %r found (fields: %s)"
   1056             % (name, ', '.join(map(repr, self.fields.keys()))))
   1057         assert len(fields) == 1, (
   1058             "Multiple fields match %r: %s"
   1059             % (name, ', '.join(map(repr, fields))))
   1060         fields[0].value = value
   1061 
   1062     def __getitem__(self, name):
   1063         """
   1064         Get the named field object (ambiguity is an error).
   1065         """
   1066         fields = self.fields.get(name)
   1067         assert fields is not None, (
   1068             "No field by the name %r found" % name)
   1069         assert len(fields) == 1, (
   1070             "Multiple fields match %r: %s"
   1071             % (name, ', '.join(map(repr, fields))))
   1072         return fields[0]
   1073 
   1074     def set(self, name, value, index=None):
   1075         """
   1076         Set the given name, using ``index`` to disambiguate.
   1077         """
   1078         if index is None:
   1079             self[name] = value
   1080         else:
   1081             fields = self.fields.get(name)
   1082             assert fields is not None, (
   1083                 "No fields found matching %r" % name)
   1084             field = fields[index]
   1085             field.value = value
   1086 
   1087     def get(self, name, index=None, default=NoDefault):
   1088         """
   1089         Get the named/indexed field object, or ``default`` if no field
   1090         is found.
   1091         """
   1092         fields = self.fields.get(name)
   1093         if fields is None and default is not NoDefault:
   1094             return default
   1095         if index is None:
   1096             return self[name]
   1097         else:
   1098             fields = self.fields.get(name)
   1099             assert fields is not None, (
   1100                 "No fields found matching %r" % name)
   1101             field = fields[index]
   1102             return field
   1103 
   1104     def select(self, name, value, index=None):
   1105         """
   1106         Like ``.set()``, except also confirms the target is a
   1107         ``<select>``.
   1108         """
   1109         field = self.get(name, index=index)
   1110         assert isinstance(field, Select)
   1111         field.value = value
   1112 
   1113     def submit(self, name=None, index=None, **args):
   1114         """
   1115         Submits the form.  If ``name`` is given, then also select that
   1116         button (using ``index`` to disambiguate)``.
   1117 
   1118         Any extra keyword arguments are passed to the ``.get()`` or
   1119         ``.post()`` method.
   1120 
   1121         Returns a response object.
   1122         """
   1123         fields = self.submit_fields(name, index=index)
   1124         return self.response.goto(self.action, method=self.method,
   1125                                   params=fields, **args)
   1126 
   1127     def submit_fields(self, name=None, index=None):
   1128         """
   1129         Return a list of ``[(name, value), ...]`` for the current
   1130         state of the form.
   1131         """
   1132         submit = []
   1133         if name is not None:
   1134             field = self.get(name, index=index)
   1135             submit.append((field.name, field.value_if_submitted()))
   1136         for name, fields in self.fields.items():
   1137             if name is None:
   1138                 continue
   1139             for field in fields:
   1140                 value = field.value
   1141                 if value is None:
   1142                     continue
   1143                 submit.append((name, value))
   1144         return submit
   1145 
   1146 
   1147 _attr_re = re.compile(r'([^= \n\r\t]+)[ \n\r\t]*(?:=[ \n\r\t]*(?:"([^"]*)"|([^"][^ \n\r\t>]*)))?', re.S)
   1148 
   1149 def _parse_attrs(text):
   1150     attrs = {}
   1151     for match in _attr_re.finditer(text):
   1152         attr_name = match.group(1).lower()
   1153         attr_body = match.group(2) or match.group(3)
   1154         attr_body = html_unquote(attr_body or '')
   1155         attrs[attr_name] = attr_body
   1156     return attrs
   1157 
   1158 class Field(object):
   1159 
   1160     """
   1161     Field object.
   1162     """
   1163 
   1164     # Dictionary of field types (select, radio, etc) to classes
   1165     classes = {}
   1166 
   1167     settable = True
   1168 
   1169     def __init__(self, form, tag, name, pos,
   1170                  value=None, id=None, **attrs):
   1171         self.form = form
   1172         self.tag = tag
   1173         self.name = name
   1174         self.pos = pos
   1175         self._value = value
   1176         self.id = id
   1177         self.attrs = attrs
   1178 
   1179     def value__set(self, value):
   1180         if not self.settable:
   1181             raise AttributeError(
   1182                 "You cannot set the value of the <%s> field %r"
   1183                 % (self.tag, self.name))
   1184         self._value = value
   1185 
   1186     def force_value(self, value):
   1187         """
   1188         Like setting a value, except forces it even for, say, hidden
   1189         fields.
   1190         """
   1191         self._value = value
   1192 
   1193     def value__get(self):
   1194         return self._value
   1195 
   1196     value = property(value__get, value__set)
   1197 
   1198 class Select(Field):
   1199 
   1200     """
   1201     Field representing ``<select>``
   1202     """
   1203 
   1204     def __init__(self, *args, **attrs):
   1205         super(Select, self).__init__(*args, **attrs)
   1206         self.options = []
   1207         self.multiple = attrs.get('multiple')
   1208         assert not self.multiple, (
   1209             "<select multiple> not yet supported")
   1210         # Undetermined yet:
   1211         self.selectedIndex = None
   1212 
   1213     def value__set(self, value):
   1214         for i, (option, checked) in enumerate(self.options):
   1215             if option == str(value):
   1216                 self.selectedIndex = i
   1217                 break
   1218         else:
   1219             raise ValueError(
   1220                 "Option %r not found (from %s)"
   1221                 % (value, ', '.join(
   1222                 [repr(o) for o, c in self.options])))
   1223 
   1224     def value__get(self):
   1225         if self.selectedIndex is not None:
   1226             return self.options[self.selectedIndex][0]
   1227         else:
   1228             for option, checked in self.options:
   1229                 if checked:
   1230                     return option
   1231             else:
   1232                 if self.options:
   1233                     return self.options[0][0]
   1234                 else:
   1235                     return None
   1236 
   1237     value = property(value__get, value__set)
   1238 
   1239 Field.classes['select'] = Select
   1240 
   1241 class Radio(Select):
   1242 
   1243     """
   1244     Field representing ``<input type="radio">``
   1245     """
   1246 
   1247 Field.classes['radio'] = Radio
   1248 
   1249 class Checkbox(Field):
   1250 
   1251     """
   1252     Field representing ``<input type="checkbox">``
   1253     """
   1254 
   1255     def __init__(self, *args, **attrs):
   1256         super(Checkbox, self).__init__(*args, **attrs)
   1257         self.checked = 'checked' in attrs
   1258 
   1259     def value__set(self, value):
   1260         self.checked = not not value
   1261 
   1262     def value__get(self):
   1263         if self.checked:
   1264             if self._value is None:
   1265                 return 'on'
   1266             else:
   1267                 return self._value
   1268         else:
   1269             return None
   1270 
   1271     value = property(value__get, value__set)
   1272 
   1273 Field.classes['checkbox'] = Checkbox
   1274 
   1275 class Text(Field):
   1276     """
   1277     Field representing ``<input type="text">``
   1278     """
   1279     def __init__(self, form, tag, name, pos,
   1280                  value='', id=None, **attrs):
   1281         #text fields default to empty string
   1282         Field.__init__(self, form, tag, name, pos,
   1283                        value=value, id=id, **attrs)
   1284 
   1285 Field.classes['text'] = Text
   1286 
   1287 class Textarea(Text):
   1288     """
   1289     Field representing ``<textarea>``
   1290     """
   1291 
   1292 Field.classes['textarea'] = Textarea
   1293 
   1294 class Hidden(Text):
   1295     """
   1296     Field representing ``<input type="hidden">``
   1297     """
   1298 
   1299 Field.classes['hidden'] = Hidden
   1300 
   1301 class Submit(Field):
   1302     """
   1303     Field representing ``<input type="submit">`` and ``<button>``
   1304     """
   1305 
   1306     settable = False
   1307 
   1308     def value__get(self):
   1309         return None
   1310 
   1311     value = property(value__get)
   1312 
   1313     def value_if_submitted(self):
   1314         return self._value
   1315 
   1316 Field.classes['submit'] = Submit
   1317 
   1318 Field.classes['button'] = Submit
   1319 
   1320 Field.classes['image'] = Submit
   1321 
   1322 ############################################################
   1323 ## Command-line testing
   1324 ############################################################
   1325 
   1326 
   1327 class TestFileEnvironment(object):
   1328 
   1329     """
   1330     This represents an environment in which files will be written, and
   1331     scripts will be run.
   1332     """
   1333 
   1334     # for py.test
   1335     disabled = True
   1336 
   1337     def __init__(self, base_path, template_path=None,
   1338                  script_path=None,
   1339                  environ=None, cwd=None, start_clear=True,
   1340                  ignore_paths=None, ignore_hidden=True):
   1341         """
   1342         Creates an environment.  ``base_path`` is used as the current
   1343         working directory, and generally where changes are looked for.
   1344 
   1345         ``template_path`` is the directory to look for *template*
   1346         files, which are files you'll explicitly add to the
   1347         environment.  This is done with ``.writefile()``.
   1348 
   1349         ``script_path`` is the PATH for finding executables.  Usually
   1350         grabbed from ``$PATH``.
   1351 
   1352         ``environ`` is the operating system environment,
   1353         ``os.environ`` if not given.
   1354 
   1355         ``cwd`` is the working directory, ``base_path`` by default.
   1356 
   1357         If ``start_clear`` is true (default) then the ``base_path``
   1358         will be cleared (all files deleted) when an instance is
   1359         created.  You can also use ``.clear()`` to clear the files.
   1360 
   1361         ``ignore_paths`` is a set of specific filenames that should be
   1362         ignored when created in the environment.  ``ignore_hidden``
   1363         means, if true (default) that filenames and directories
   1364         starting with ``'.'`` will be ignored.
   1365         """
   1366         self.base_path = base_path
   1367         self.template_path = template_path
   1368         if environ is None:
   1369             environ = os.environ.copy()
   1370         self.environ = environ
   1371         if script_path is None:
   1372             if sys.platform == 'win32':
   1373                 script_path = environ.get('PATH', '').split(';')
   1374             else:
   1375                 script_path = environ.get('PATH', '').split(':')
   1376         self.script_path = script_path
   1377         if cwd is None:
   1378             cwd = base_path
   1379         self.cwd = cwd
   1380         if start_clear:
   1381             self.clear()
   1382         elif not os.path.exists(base_path):
   1383             os.makedirs(base_path)
   1384         self.ignore_paths = ignore_paths or []
   1385         self.ignore_hidden = ignore_hidden
   1386 
   1387     def run(self, script, *args, **kw):
   1388         """
   1389         Run the command, with the given arguments.  The ``script``
   1390         argument can have space-separated arguments, or you can use
   1391         the positional arguments.
   1392 
   1393         Keywords allowed are:
   1394 
   1395         ``expect_error``: (default False)
   1396             Don't raise an exception in case of errors
   1397         ``expect_stderr``: (default ``expect_error``)
   1398             Don't raise an exception if anything is printed to stderr
   1399         ``stdin``: (default ``""``)
   1400             Input to the script
   1401         ``printresult``: (default True)
   1402             Print the result after running
   1403         ``cwd``: (default ``self.cwd``)
   1404             The working directory to run in
   1405 
   1406         Returns a `ProcResponse
   1407         <class-paste.fixture.ProcResponse.html>`_ object.
   1408         """
   1409         __tracebackhide__ = True
   1410         expect_error = _popget(kw, 'expect_error', False)
   1411         expect_stderr = _popget(kw, 'expect_stderr', expect_error)
   1412         cwd = _popget(kw, 'cwd', self.cwd)
   1413         stdin = _popget(kw, 'stdin', None)
   1414         printresult = _popget(kw, 'printresult', True)
   1415         args = list(map(str, args))
   1416         assert not kw, (
   1417             "Arguments not expected: %s" % ', '.join(kw.keys()))
   1418         if ' ' in script:
   1419             assert not args, (
   1420                 "You cannot give a multi-argument script (%r) "
   1421                 "and arguments (%s)" % (script, args))
   1422             script, args = script.split(None, 1)
   1423             args = shlex.split(args)
   1424         script = self._find_exe(script)
   1425         all = [script] + args
   1426         files_before = self._find_files()
   1427         proc = subprocess.Popen(all, stdin=subprocess.PIPE,
   1428                                 stderr=subprocess.PIPE,
   1429                                 stdout=subprocess.PIPE,
   1430                                 cwd=cwd,
   1431                                 env=self.environ)
   1432         stdout, stderr = proc.communicate(stdin)
   1433         files_after = self._find_files()
   1434         result = ProcResult(
   1435             self, all, stdin, stdout, stderr,
   1436             returncode=proc.returncode,
   1437             files_before=files_before,
   1438             files_after=files_after)
   1439         if printresult:
   1440             print(result)
   1441             print('-'*40)
   1442         if not expect_error:
   1443             result.assert_no_error()
   1444         if not expect_stderr:
   1445             result.assert_no_stderr()
   1446         return result
   1447 
   1448     def _find_exe(self, script_name):
   1449         if self.script_path is None:
   1450             script_name = os.path.join(self.cwd, script_name)
   1451             if not os.path.exists(script_name):
   1452                 raise OSError(
   1453                     "Script %s does not exist" % script_name)
   1454             return script_name
   1455         for path in self.script_path:
   1456             fn = os.path.join(path, script_name)
   1457             if os.path.exists(fn):
   1458                 return fn
   1459         raise OSError(
   1460             "Script %s could not be found in %s"
   1461             % (script_name, ':'.join(self.script_path)))
   1462 
   1463     def _find_files(self):
   1464         result = {}
   1465         for fn in os.listdir(self.base_path):
   1466             if self._ignore_file(fn):
   1467                 continue
   1468             self._find_traverse(fn, result)
   1469         return result
   1470 
   1471     def _ignore_file(self, fn):
   1472         if fn in self.ignore_paths:
   1473             return True
   1474         if self.ignore_hidden and os.path.basename(fn).startswith('.'):
   1475             return True
   1476         return False
   1477 
   1478     def _find_traverse(self, path, result):
   1479         full = os.path.join(self.base_path, path)
   1480         if os.path.isdir(full):
   1481             result[path] = FoundDir(self.base_path, path)
   1482             for fn in os.listdir(full):
   1483                 fn = os.path.join(path, fn)
   1484                 if self._ignore_file(fn):
   1485                     continue
   1486                 self._find_traverse(fn, result)
   1487         else:
   1488             result[path] = FoundFile(self.base_path, path)
   1489 
   1490     def clear(self):
   1491         """
   1492         Delete all the files in the base directory.
   1493         """
   1494         if os.path.exists(self.base_path):
   1495             shutil.rmtree(self.base_path)
   1496         os.mkdir(self.base_path)
   1497 
   1498     def writefile(self, path, content=None,
   1499                   frompath=None):
   1500         """
   1501         Write a file to the given path.  If ``content`` is given then
   1502         that text is written, otherwise the file in ``frompath`` is
   1503         used.  ``frompath`` is relative to ``self.template_path``
   1504         """
   1505         full = os.path.join(self.base_path, path)
   1506         if not os.path.exists(os.path.dirname(full)):
   1507             os.makedirs(os.path.dirname(full))
   1508         f = open(full, 'wb')
   1509         if content is not None:
   1510             f.write(content)
   1511         if frompath is not None:
   1512             if self.template_path:
   1513                 frompath = os.path.join(self.template_path, frompath)
   1514             f2 = open(frompath, 'rb')
   1515             f.write(f2.read())
   1516             f2.close()
   1517         f.close()
   1518         return FoundFile(self.base_path, path)
   1519 
   1520 class ProcResult(object):
   1521 
   1522     """
   1523     Represents the results of running a command in
   1524     `TestFileEnvironment
   1525     <class-paste.fixture.TestFileEnvironment.html>`_.
   1526 
   1527     Attributes to pay particular attention to:
   1528 
   1529     ``stdout``, ``stderr``:
   1530         What is produced
   1531 
   1532     ``files_created``, ``files_deleted``, ``files_updated``:
   1533         Dictionaries mapping filenames (relative to the ``base_dir``)
   1534         to `FoundFile <class-paste.fixture.FoundFile.html>`_ or
   1535         `FoundDir <class-paste.fixture.FoundDir.html>`_ objects.
   1536     """
   1537 
   1538     def __init__(self, test_env, args, stdin, stdout, stderr,
   1539                  returncode, files_before, files_after):
   1540         self.test_env = test_env
   1541         self.args = args
   1542         self.stdin = stdin
   1543         self.stdout = stdout
   1544         self.stderr = stderr
   1545         self.returncode = returncode
   1546         self.files_before = files_before
   1547         self.files_after = files_after
   1548         self.files_deleted = {}
   1549         self.files_updated = {}
   1550         self.files_created = files_after.copy()
   1551         for path, f in files_before.items():
   1552             if path not in files_after:
   1553                 self.files_deleted[path] = f
   1554                 continue
   1555             del self.files_created[path]
   1556             if f.mtime < files_after[path].mtime:
   1557                 self.files_updated[path] = files_after[path]
   1558 
   1559     def assert_no_error(self):
   1560         __tracebackhide__ = True
   1561         assert self.returncode == 0, (
   1562             "Script returned code: %s" % self.returncode)
   1563 
   1564     def assert_no_stderr(self):
   1565         __tracebackhide__ = True
   1566         if self.stderr:
   1567             print('Error output:')
   1568             print(self.stderr)
   1569             raise AssertionError("stderr output not expected")
   1570 
   1571     def __str__(self):
   1572         s = ['Script result: %s' % ' '.join(self.args)]
   1573         if self.returncode:
   1574             s.append('  return code: %s' % self.returncode)
   1575         if self.stderr:
   1576             s.append('-- stderr: --------------------')
   1577             s.append(self.stderr)
   1578         if self.stdout:
   1579             s.append('-- stdout: --------------------')
   1580             s.append(self.stdout)
   1581         for name, files, show_size in [
   1582             ('created', self.files_created, True),
   1583             ('deleted', self.files_deleted, True),
   1584             ('updated', self.files_updated, True)]:
   1585             if files:
   1586                 s.append('-- %s: -------------------' % name)
   1587                 files = files.items()
   1588                 files.sort()
   1589                 last = ''
   1590                 for path, f in files:
   1591                     t = '  %s' % _space_prefix(last, path, indent=4,
   1592                                                include_sep=False)
   1593                     last = path
   1594                     if show_size and f.size != 'N/A':
   1595                         t += '  (%s bytes)' % f.size
   1596                     s.append(t)
   1597         return '\n'.join(s)
   1598 
   1599 class FoundFile(object):
   1600 
   1601     """
   1602     Represents a single file found as the result of a command.
   1603 
   1604     Has attributes:
   1605 
   1606     ``path``:
   1607         The path of the file, relative to the ``base_path``
   1608 
   1609     ``full``:
   1610         The full path
   1611 
   1612     ``stat``:
   1613         The results of ``os.stat``.  Also ``mtime`` and ``size``
   1614         contain the ``.st_mtime`` and ``st_size`` of the stat.
   1615 
   1616     ``bytes``:
   1617         The contents of the file.
   1618 
   1619     You may use the ``in`` operator with these objects (tested against
   1620     the contents of the file), and the ``.mustcontain()`` method.
   1621     """
   1622 
   1623     file = True
   1624     dir = False
   1625 
   1626     def __init__(self, base_path, path):
   1627         self.base_path = base_path
   1628         self.path = path
   1629         self.full = os.path.join(base_path, path)
   1630         self.stat = os.stat(self.full)
   1631         self.mtime = self.stat.st_mtime
   1632         self.size = self.stat.st_size
   1633         self._bytes = None
   1634 
   1635     def bytes__get(self):
   1636         if self._bytes is None:
   1637             f = open(self.full, 'rb')
   1638             self._bytes = f.read()
   1639             f.close()
   1640         return self._bytes
   1641     bytes = property(bytes__get)
   1642 
   1643     def __contains__(self, s):
   1644         return s in self.bytes
   1645 
   1646     def mustcontain(self, s):
   1647         __tracebackhide__ = True
   1648         bytes_ = self.bytes
   1649         if s not in bytes_:
   1650             print('Could not find %r in:' % s)
   1651             print(bytes_)
   1652             assert s in bytes_
   1653 
   1654     def __repr__(self):
   1655         return '<%s %s:%s>' % (
   1656             self.__class__.__name__,
   1657             self.base_path, self.path)
   1658 
   1659 class FoundDir(object):
   1660 
   1661     """
   1662     Represents a directory created by a command.
   1663     """
   1664 
   1665     file = False
   1666     dir = True
   1667 
   1668     def __init__(self, base_path, path):
   1669         self.base_path = base_path
   1670         self.path = path
   1671         self.full = os.path.join(base_path, path)
   1672         self.size = 'N/A'
   1673         self.mtime = 'N/A'
   1674 
   1675     def __repr__(self):
   1676         return '<%s %s:%s>' % (
   1677             self.__class__.__name__,
   1678             self.base_path, self.path)
   1679 
   1680 def _popget(d, key, default=None):
   1681     """
   1682     Pop the key if found (else return default)
   1683     """
   1684     if key in d:
   1685         return d.pop(key)
   1686     return default
   1687 
   1688 def _space_prefix(pref, full, sep=None, indent=None, include_sep=True):
   1689     """
   1690     Anything shared by pref and full will be replaced with spaces
   1691     in full, and full returned.
   1692     """
   1693     if sep is None:
   1694         sep = os.path.sep
   1695     pref = pref.split(sep)
   1696     full = full.split(sep)
   1697     padding = []
   1698     while pref and full and pref[0] == full[0]:
   1699         if indent is None:
   1700             padding.append(' ' * (len(full[0]) + len(sep)))
   1701         else:
   1702             padding.append(' ' * indent)
   1703         full.pop(0)
   1704         pref.pop(0)
   1705     if padding:
   1706         if include_sep:
   1707             return ''.join(padding) + sep + sep.join(full)
   1708         else:
   1709             return ''.join(padding) + sep.join(full)
   1710     else:
   1711         return sep.join(full)
   1712 
   1713 def _make_pattern(pat):
   1714     if pat is None:
   1715         return None
   1716     if isinstance(pat, (six.binary_type, six.text_type)):
   1717         pat = re.compile(pat)
   1718     if hasattr(pat, 'search'):
   1719         return pat.search
   1720     if callable(pat):
   1721         return pat
   1722     assert 0, (
   1723         "Cannot make callable pattern object out of %r" % pat)
   1724 
   1725 def setup_module(module=None):
   1726     """
   1727     This is used by py.test if it is in the module, so you can
   1728     import this directly.
   1729 
   1730     Use like::
   1731 
   1732         from paste.fixture import setup_module
   1733     """
   1734     # Deprecated June 2008
   1735     import warnings
   1736     warnings.warn(
   1737         'setup_module is deprecated',
   1738         DeprecationWarning, 2)
   1739     if module is None:
   1740         # The module we were called from must be the module...
   1741         module = sys._getframe().f_back.f_globals['__name__']
   1742     if isinstance(module, (six.binary_type, six.text_type)):
   1743         module = sys.modules[module]
   1744     if hasattr(module, 'reset_state'):
   1745         module.reset_state()
   1746 
   1747 def html_unquote(v):
   1748     """
   1749     Unquote (some) entities in HTML.  (incomplete)
   1750     """
   1751     for ent, repl in [('&nbsp;', ' '), ('&gt;', '>'),
   1752                       ('&lt;', '<'), ('&quot;', '"'),
   1753                       ('&amp;', '&')]:
   1754         v = v.replace(ent, repl)
   1755     return v
   1756