fixture.py 58 KB

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