fixture.py 57 KB

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