feedparsertest.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. #!/usr/bin/env python
  2. __author__ = "Mark Pilgrim <http://diveintomark.org/>"
  3. __license__ = """
  4. Copyright 2010-2015 Kurt McKee <contactme@kurtmckee.org>
  5. Copyright 2004-2008 Mark Pilgrim
  6. All rights reserved.
  7. Redistribution and use in source and binary forms, with or without modification,
  8. are permitted provided that the following conditions are met:
  9. * Redistributions of source code must retain the above copyright notice,
  10. this list of conditions and the following disclaimer.
  11. * Redistributions in binary form must reproduce the above copyright notice,
  12. this list of conditions and the following disclaimer in the documentation
  13. and/or other materials provided with the distribution.
  14. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
  15. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  16. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  17. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  18. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  19. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  20. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  21. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  22. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  23. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  24. POSSIBILITY OF SUCH DAMAGE."""
  25. import codecs
  26. import datetime
  27. import glob
  28. import operator
  29. import os
  30. import posixpath
  31. import pprint
  32. import re
  33. import struct
  34. import sys
  35. import threading
  36. import time
  37. import unittest
  38. import urllib
  39. import warnings
  40. import zlib
  41. import BaseHTTPServer
  42. import SimpleHTTPServer
  43. import feedparser
  44. if not feedparser._XML_AVAILABLE:
  45. sys.stderr.write('No XML parsers available, unit testing can not proceed\n')
  46. sys.exit(1)
  47. try:
  48. # the utf_32 codec was introduced in Python 2.6; it's necessary to
  49. # check this as long as feedparser supports Python 2.4 and 2.5
  50. codecs.lookup('utf_32')
  51. except LookupError:
  52. _UTF32_AVAILABLE = False
  53. else:
  54. _UTF32_AVAILABLE = True
  55. _s2bytes = feedparser._s2bytes
  56. _l2bytes = feedparser._l2bytes
  57. #---------- custom HTTP server (used to serve test feeds) ----------
  58. _PORT = 8097 # not really configurable, must match hardcoded port in tests
  59. _HOST = '127.0.0.1' # also not really configurable
  60. class FeedParserTestRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  61. headers_re = re.compile(_s2bytes(r"^Header:\s+([^:]+):(.+)$"), re.MULTILINE)
  62. def send_head(self):
  63. """Send custom headers defined in test case
  64. Example:
  65. <!--
  66. Header: Content-type: application/atom+xml
  67. Header: X-Foo: bar
  68. -->
  69. """
  70. # Short-circuit the HTTP status test `test_redirect_to_304()`
  71. if self.path == '/-/return-304.xml':
  72. self.send_response(304)
  73. self.send_header('Content-type', 'text/xml')
  74. self.end_headers()
  75. return feedparser._StringIO(u''.encode('utf-8'))
  76. path = self.translate_path(self.path)
  77. # the compression tests' filenames determine the header sent
  78. if self.path.startswith('/tests/compression'):
  79. if self.path.endswith('gz'):
  80. headers = {'Content-Encoding': 'gzip'}
  81. else:
  82. headers = {'Content-Encoding': 'deflate'}
  83. headers['Content-type'] = 'application/xml'
  84. else:
  85. headers = dict([(k.decode('utf-8'), v.decode('utf-8').strip()) for k, v in self.headers_re.findall(open(path, 'rb').read())])
  86. f = open(path, 'rb')
  87. if (self.headers.get('if-modified-since') == headers.get('Last-Modified', 'nom')) \
  88. or (self.headers.get('if-none-match') == headers.get('ETag', 'nomatch')):
  89. status = 304
  90. else:
  91. status = 200
  92. headers.setdefault('Status', status)
  93. self.send_response(int(headers['Status']))
  94. headers.setdefault('Content-type', self.guess_type(path))
  95. self.send_header("Content-type", headers['Content-type'])
  96. self.send_header("Content-Length", str(os.stat(f.name)[6]))
  97. for k, v in headers.items():
  98. if k not in ('Status', 'Content-type'):
  99. self.send_header(k, v)
  100. self.end_headers()
  101. return f
  102. def log_request(self, *args):
  103. pass
  104. class FeedParserTestServer(threading.Thread):
  105. """HTTP Server that runs in a thread and handles a predetermined number of requests"""
  106. def __init__(self, requests):
  107. threading.Thread.__init__(self)
  108. self.requests = requests
  109. self.ready = threading.Event()
  110. def run(self):
  111. self.httpd = BaseHTTPServer.HTTPServer((_HOST, _PORT), FeedParserTestRequestHandler)
  112. self.ready.set()
  113. while self.requests:
  114. self.httpd.handle_request()
  115. self.requests -= 1
  116. self.ready.clear()
  117. #---------- dummy test case class (test methods are added dynamically) ----------
  118. unicode1_re = re.compile(_s2bytes(" u'"))
  119. unicode2_re = re.compile(_s2bytes(' u"'))
  120. # _bytes is only used in everythingIsUnicode().
  121. # In Python 2 it's str, and in Python 3 it's bytes.
  122. _bytes = type(_s2bytes(''))
  123. def everythingIsUnicode(d):
  124. """Takes a dictionary, recursively verifies that every value is unicode"""
  125. for k, v in d.iteritems():
  126. if isinstance(v, dict) and k != 'headers':
  127. if not everythingIsUnicode(v):
  128. return False
  129. elif isinstance(v, list):
  130. for i in v:
  131. if isinstance(i, dict) and not everythingIsUnicode(i):
  132. return False
  133. elif isinstance(i, _bytes):
  134. return False
  135. elif isinstance(v, _bytes):
  136. return False
  137. return True
  138. def failUnlessEval(self, xmlfile, evalString, msg=None):
  139. """Fail unless eval(evalString, env)"""
  140. env = feedparser.parse(xmlfile)
  141. try:
  142. if not eval(evalString, globals(), env):
  143. failure=(msg or 'not eval(%s) \nWITH env(%s)' % (evalString, pprint.pformat(env)))
  144. raise self.failureException, failure
  145. if not everythingIsUnicode(env):
  146. raise self.failureException, "not everything is unicode \nWITH env(%s)" % (pprint.pformat(env), )
  147. except SyntaxError:
  148. # Python 3 doesn't have the `u""` syntax, so evalString needs to be modified,
  149. # which will require the failure message to be updated
  150. evalString = re.sub(unicode1_re, _s2bytes(" '"), evalString)
  151. evalString = re.sub(unicode2_re, _s2bytes(' "'), evalString)
  152. if not eval(evalString, globals(), env):
  153. failure=(msg or 'not eval(%s) \nWITH env(%s)' % (evalString, pprint.pformat(env)))
  154. raise self.failureException, failure
  155. class BaseTestCase(unittest.TestCase):
  156. failUnlessEval = failUnlessEval
  157. class TestCase(BaseTestCase):
  158. pass
  159. class TestTemporaryFallbackBehavior(unittest.TestCase):
  160. "These tests are temporarily here because of issues 310 and 328"
  161. def test_issue_328_fallback_behavior(self):
  162. warnings.filterwarnings('error')
  163. d = feedparser.FeedParserDict()
  164. d['published'] = u'pub string'
  165. d['published_parsed'] = u'pub tuple'
  166. d['updated'] = u'upd string'
  167. d['updated_parsed'] = u'upd tuple'
  168. # Ensure that `updated` doesn't map to `published` when it exists
  169. self.assertTrue('published' in d)
  170. self.assertTrue('published_parsed' in d)
  171. self.assertTrue('updated' in d)
  172. self.assertTrue('updated_parsed' in d)
  173. self.assertEqual(d['published'], 'pub string')
  174. self.assertEqual(d['published_parsed'], 'pub tuple')
  175. self.assertEqual(d['updated'], 'upd string')
  176. self.assertEqual(d['updated_parsed'], 'upd tuple')
  177. d = feedparser.FeedParserDict()
  178. d['published'] = u'pub string'
  179. d['published_parsed'] = u'pub tuple'
  180. # Ensure that `updated` doesn't actually exist
  181. self.assertTrue('updated' not in d)
  182. self.assertTrue('updated_parsed' not in d)
  183. # Ensure that accessing `updated` throws a DeprecationWarning
  184. try:
  185. d['updated']
  186. except DeprecationWarning:
  187. # Expected behavior
  188. pass
  189. else:
  190. # Wrong behavior
  191. self.assertEqual(True, False)
  192. try:
  193. d['updated_parsed']
  194. except DeprecationWarning:
  195. # Expected behavior
  196. pass
  197. else:
  198. # Wrong behavior
  199. self.assertEqual(True, False)
  200. # Ensure that `updated` maps to `published`
  201. warnings.filterwarnings('ignore')
  202. self.assertEqual(d['updated'], u'pub string')
  203. self.assertEqual(d['updated_parsed'], u'pub tuple')
  204. warnings.resetwarnings()
  205. class TestEverythingIsUnicode(unittest.TestCase):
  206. "Ensure that `everythingIsUnicode()` is working appropriately"
  207. def test_everything_is_unicode(self):
  208. self.assertTrue(everythingIsUnicode(
  209. {'a': u'a', 'b': [u'b', {'c': u'c'}], 'd': {'e': u'e'}}
  210. ))
  211. def test_not_everything_is_unicode(self):
  212. self.assertFalse(everythingIsUnicode({'a': _s2bytes('a')}))
  213. self.assertFalse(everythingIsUnicode({'a': [_s2bytes('a')]}))
  214. self.assertFalse(everythingIsUnicode({'a': {'b': _s2bytes('b')}}))
  215. self.assertFalse(everythingIsUnicode({'a': [{'b': _s2bytes('b')}]}))
  216. class TestLooseParser(BaseTestCase):
  217. "Test the sgmllib-based parser by manipulating feedparser " \
  218. "into believing no XML parsers are installed"
  219. def __init__(self, arg):
  220. unittest.TestCase.__init__(self, arg)
  221. self._xml_available = feedparser._XML_AVAILABLE
  222. def setUp(self):
  223. feedparser._XML_AVAILABLE = 0
  224. def tearDown(self):
  225. feedparser._XML_AVAILABLE = self._xml_available
  226. class TestStrictParser(BaseTestCase):
  227. pass
  228. class TestMicroformats(BaseTestCase):
  229. pass
  230. class TestEncodings(BaseTestCase):
  231. def test_doctype_replacement(self):
  232. "Ensure that non-ASCII-compatible encodings don't hide " \
  233. "disallowed ENTITY declarations"
  234. doc = """<?xml version="1.0" encoding="utf-16be"?>
  235. <!DOCTYPE feed [
  236. <!ENTITY exponential1 "bogus ">
  237. <!ENTITY exponential2 "&exponential1;&exponential1;">
  238. <!ENTITY exponential3 "&exponential2;&exponential2;">
  239. ]>
  240. <feed><title type="html">&exponential3;</title></feed>"""
  241. doc = codecs.BOM_UTF16_BE + doc.encode('utf-16be')
  242. result = feedparser.parse(doc)
  243. self.assertEqual(result['feed']['title'], u'&amp;exponential3')
  244. def test_gb2312_converted_to_gb18030_in_xml_encoding(self):
  245. # \u55de was chosen because it exists in gb18030 but not gb2312
  246. feed = u'''<?xml version="1.0" encoding="gb2312"?>
  247. <feed><title>\u55de</title></feed>'''
  248. result = feedparser.parse(feed.encode('gb18030'), response_headers={
  249. 'Content-Type': 'text/xml'
  250. })
  251. self.assertEqual(result.encoding, 'gb18030')
  252. class TestFeedParserDict(unittest.TestCase):
  253. "Ensure that FeedParserDict returns values as expected and won't crash"
  254. def setUp(self):
  255. self.d = feedparser.FeedParserDict()
  256. def _check_key(self, k):
  257. self.assertTrue(k in self.d)
  258. self.assertTrue(hasattr(self.d, k))
  259. self.assertEqual(self.d[k], 1)
  260. self.assertEqual(getattr(self.d, k), 1)
  261. def _check_no_key(self, k):
  262. self.assertTrue(k not in self.d)
  263. self.assertTrue(not hasattr(self.d, k))
  264. def test_empty(self):
  265. keys = (
  266. 'a','entries', 'id', 'guid', 'summary', 'subtitle', 'description',
  267. 'category', 'enclosures', 'license', 'categories',
  268. )
  269. for k in keys:
  270. self._check_no_key(k)
  271. self.assertTrue('items' not in self.d)
  272. self.assertTrue(hasattr(self.d, 'items')) # dict.items() exists
  273. def test_neutral(self):
  274. self.d['a'] = 1
  275. self._check_key('a')
  276. def test_single_mapping_target_1(self):
  277. self.d['id'] = 1
  278. self._check_key('id')
  279. self._check_key('guid')
  280. def test_single_mapping_target_2(self):
  281. self.d['guid'] = 1
  282. self._check_key('id')
  283. self._check_key('guid')
  284. def test_multiple_mapping_target_1(self):
  285. self.d['summary'] = 1
  286. self._check_key('summary')
  287. self._check_key('description')
  288. def test_multiple_mapping_target_2(self):
  289. self.d['subtitle'] = 1
  290. self._check_key('subtitle')
  291. self._check_key('description')
  292. def test_multiple_mapping_mapped_key(self):
  293. self.d['description'] = 1
  294. self._check_key('summary')
  295. self._check_key('description')
  296. def test_license(self):
  297. self.d['links'] = []
  298. try:
  299. self.d['license']
  300. self.assertTrue(False)
  301. except KeyError:
  302. pass
  303. self.d['links'].append({'rel': 'license'})
  304. try:
  305. self.d['license']
  306. self.assertTrue(False)
  307. except KeyError:
  308. pass
  309. self.d['links'].append({'rel': 'license', 'href': 'http://dom.test/'})
  310. self.assertEqual(self.d['license'], 'http://dom.test/')
  311. def test_category(self):
  312. self.d['tags'] = []
  313. try:
  314. self.d['category']
  315. self.assertTrue(False)
  316. except KeyError:
  317. pass
  318. self.d['tags'] = [{}]
  319. try:
  320. self.d['category']
  321. self.assertTrue(False)
  322. except KeyError:
  323. pass
  324. self.d['tags'] = [{'term': 'cat'}]
  325. self.assertEqual(self.d['category'], 'cat')
  326. self.d['tags'].append({'term': 'dog'})
  327. self.assertEqual(self.d['category'], 'cat')
  328. class TestOpenResource(unittest.TestCase):
  329. "Ensure that `_open_resource()` interprets its arguments as URIs, " \
  330. "file-like objects, or in-memory feeds as expected"
  331. def test_fileobj(self):
  332. r = feedparser._open_resource(sys.stdin, '', '', '', '', [], {})
  333. self.assertTrue(r is sys.stdin)
  334. def test_feed(self):
  335. f = feedparser.parse(u'feed://localhost:8097/tests/http/target.xml')
  336. self.assertEqual(f.href, u'http://localhost:8097/tests/http/target.xml')
  337. def test_feed_http(self):
  338. f = feedparser.parse(u'feed:http://localhost:8097/tests/http/target.xml')
  339. self.assertEqual(f.href, u'http://localhost:8097/tests/http/target.xml')
  340. def test_bytes(self):
  341. s = '<feed><item><title>text</title></item></feed>'.encode('utf-8')
  342. r = feedparser._open_resource(s, '', '', '', '', [], {})
  343. self.assertEqual(s, r.read())
  344. def test_string(self):
  345. s = '<feed><item><title>text</title></item></feed>'
  346. r = feedparser._open_resource(s, '', '', '', '', [], {})
  347. self.assertEqual(s.encode('utf-8'), r.read())
  348. def test_unicode_1(self):
  349. s = u'<feed><item><title>text</title></item></feed>'
  350. r = feedparser._open_resource(s, '', '', '', '', [], {})
  351. self.assertEqual(s.encode('utf-8'), r.read())
  352. def test_unicode_2(self):
  353. s = u'<feed><item><title>t\u00e9xt</title></item></feed>'
  354. r = feedparser._open_resource(s, '', '', '', '', [], {})
  355. self.assertEqual(s.encode('utf-8'), r.read())
  356. class TestMakeSafeAbsoluteURI(unittest.TestCase):
  357. "Exercise the URI joining and sanitization code"
  358. base = u'http://d.test/d/f.ext'
  359. def _mktest(rel, expect, doc):
  360. def fn(self):
  361. value = feedparser._makeSafeAbsoluteURI(self.base, rel)
  362. self.assertEqual(value, expect)
  363. fn.__doc__ = doc
  364. return fn
  365. # make the test cases; the call signature is:
  366. # (relative_url, expected_return_value, test_doc_string)
  367. test_abs = _mktest(u'https://s.test/', u'https://s.test/', 'absolute uri')
  368. test_rel = _mktest(u'/new', u'http://d.test/new', 'relative uri')
  369. test_bad = _mktest(u'x://bad.test/', u'', 'unacceptable uri protocol')
  370. test_mag = _mktest(u'magnet:?xt=a', u'magnet:?xt=a', 'magnet uri')
  371. def test_catch_ValueError(self):
  372. 'catch ValueError in Python 2.7 and up'
  373. uri = u'http://bad]test/'
  374. value1 = feedparser._makeSafeAbsoluteURI(uri)
  375. value2 = feedparser._makeSafeAbsoluteURI(self.base, uri)
  376. swap = feedparser.ACCEPTABLE_URI_SCHEMES
  377. feedparser.ACCEPTABLE_URI_SCHEMES = ()
  378. value3 = feedparser._makeSafeAbsoluteURI(self.base, uri)
  379. feedparser.ACCEPTABLE_URI_SCHEMES = swap
  380. # Only Python 2.7 and up throw a ValueError, otherwise uri is returned
  381. self.assertTrue(value1 in (uri, u''))
  382. self.assertTrue(value2 in (uri, u''))
  383. self.assertTrue(value3 in (uri, u''))
  384. class TestConvertToIdn(unittest.TestCase):
  385. "Test IDN support (unavailable in Jython as of Jython 2.5.2)"
  386. # this is the greek test domain
  387. hostname = u'\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1'
  388. hostname += u'.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae'
  389. def test_control(self):
  390. r = feedparser._convert_to_idn(u'http://example.test/')
  391. self.assertEqual(r, u'http://example.test/')
  392. def test_idn(self):
  393. r = feedparser._convert_to_idn(u'http://%s/' % (self.hostname,))
  394. self.assertEqual(r, u'http://xn--hxajbheg2az3al.xn--jxalpdlp/')
  395. def test_port(self):
  396. r = feedparser._convert_to_idn(u'http://%s:8080/' % (self.hostname,))
  397. self.assertEqual(r, u'http://xn--hxajbheg2az3al.xn--jxalpdlp:8080/')
  398. class TestCompression(unittest.TestCase):
  399. "Test the gzip and deflate support in the HTTP code"
  400. def test_gzip_good(self):
  401. f = feedparser.parse('http://localhost:8097/tests/compression/gzip.gz')
  402. self.assertEqual(f.version, 'atom10')
  403. def test_gzip_not_compressed(self):
  404. f = feedparser.parse('http://localhost:8097/tests/compression/gzip-not-compressed.gz')
  405. self.assertEqual(f.bozo, 1)
  406. self.assertTrue(isinstance(f.bozo_exception, IOError))
  407. self.assertEqual(f['feed']['title'], 'gzip')
  408. def test_gzip_struct_error(self):
  409. f = feedparser.parse('http://localhost:8097/tests/compression/gzip-struct-error.gz')
  410. self.assertEqual(f.bozo, 1)
  411. self.assertTrue(isinstance(f.bozo_exception, struct.error))
  412. def test_zlib_good(self):
  413. f = feedparser.parse('http://localhost:8097/tests/compression/deflate.z')
  414. self.assertEqual(f.version, 'atom10')
  415. def test_zlib_no_headers(self):
  416. f = feedparser.parse('http://localhost:8097/tests/compression/deflate-no-headers.z')
  417. self.assertEqual(f.version, 'atom10')
  418. def test_zlib_not_compressed(self):
  419. f = feedparser.parse('http://localhost:8097/tests/compression/deflate-not-compressed.z')
  420. self.assertEqual(f.bozo, 1)
  421. self.assertTrue(isinstance(f.bozo_exception, zlib.error))
  422. self.assertEqual(f['feed']['title'], 'deflate')
  423. class TestHTTPStatus(unittest.TestCase):
  424. "Test HTTP redirection and other status codes"
  425. def test_301(self):
  426. f = feedparser.parse('http://localhost:8097/tests/http/http_status_301.xml')
  427. self.assertEqual(f.status, 301)
  428. self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
  429. self.assertEqual(f.entries[0].title, 'target')
  430. def test_302(self):
  431. f = feedparser.parse('http://localhost:8097/tests/http/http_status_302.xml')
  432. self.assertEqual(f.status, 302)
  433. self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
  434. self.assertEqual(f.entries[0].title, 'target')
  435. def test_303(self):
  436. f = feedparser.parse('http://localhost:8097/tests/http/http_status_303.xml')
  437. self.assertEqual(f.status, 303)
  438. self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
  439. self.assertEqual(f.entries[0].title, 'target')
  440. def test_307(self):
  441. f = feedparser.parse('http://localhost:8097/tests/http/http_status_307.xml')
  442. self.assertEqual(f.status, 307)
  443. self.assertEqual(f.href, 'http://localhost:8097/tests/http/target.xml')
  444. self.assertEqual(f.entries[0].title, 'target')
  445. def test_304(self):
  446. # first retrieve the url
  447. u = 'http://localhost:8097/tests/http/http_status_304.xml'
  448. f = feedparser.parse(u)
  449. self.assertEqual(f.status, 200)
  450. self.assertEqual(f.entries[0].title, 'title 304')
  451. # extract the etag and last-modified headers
  452. e = [v for k, v in f.headers.items() if k.lower() == 'etag'][0]
  453. mh = [v for k, v in f.headers.items() if k.lower() == 'last-modified'][0]
  454. ms = f.updated
  455. mt = f.updated_parsed
  456. md = datetime.datetime(*mt[0:7])
  457. self.assertTrue(isinstance(mh, basestring))
  458. self.assertTrue(isinstance(ms, basestring))
  459. self.assertTrue(isinstance(mt, time.struct_time))
  460. self.assertTrue(isinstance(md, datetime.datetime))
  461. # test that sending back the etag results in a 304
  462. f = feedparser.parse(u, etag=e)
  463. self.assertEqual(f.status, 304)
  464. # test that sending back last-modified (string) results in a 304
  465. f = feedparser.parse(u, modified=ms)
  466. self.assertEqual(f.status, 304)
  467. # test that sending back last-modified (9-tuple) results in a 304
  468. f = feedparser.parse(u, modified=mt)
  469. self.assertEqual(f.status, 304)
  470. # test that sending back last-modified (datetime) results in a 304
  471. f = feedparser.parse(u, modified=md)
  472. self.assertEqual(f.status, 304)
  473. def test_404(self):
  474. f = feedparser.parse('http://localhost:8097/tests/http/http_status_404.xml')
  475. self.assertEqual(f.status, 404)
  476. def test_redirect_to_304(self):
  477. # ensure that an http redirect to an http 304 doesn't
  478. # trigger a bozo_exception
  479. u = 'http://localhost:8097/tests/http/http_redirect_to_304.xml'
  480. f = feedparser.parse(u)
  481. self.assertTrue(f.bozo == 0)
  482. self.assertTrue(f.status == 302)
  483. class TestDateParsers(unittest.TestCase):
  484. "Test the various date parsers; most of the test cases are constructed " \
  485. "dynamically based on the contents of the `date_tests` dict, below"
  486. def test_None(self):
  487. self.assertTrue(feedparser._parse_date(None) is None)
  488. def _check_date(self, func, dtstring, expected_value):
  489. try:
  490. parsed_value = func(dtstring)
  491. except (OverflowError, ValueError):
  492. parsed_value = None
  493. self.assertEqual(parsed_value, expected_value)
  494. # self.assertEqual(parsed_value, feedparser._parse_date(dtstring))
  495. def test_year_10000_date(self):
  496. # On some systems this date string will trigger an OverflowError.
  497. # On Jython and x64 systems, however, it's interpreted just fine.
  498. try:
  499. date = feedparser._parse_date_rfc822(u'Sun, 31 Dec 9999 23:59:59 -9999')
  500. except OverflowError:
  501. date = None
  502. self.assertTrue(date in (None, (10000, 1, 5, 4, 38, 59, 2, 5, 0)))
  503. date_tests = {
  504. feedparser._parse_date_greek: (
  505. (u'', None), # empty string
  506. (u'\u039a\u03c5\u03c1, 11 \u0399\u03bf\u03cd\u03bb 2004 12:00:00 EST', (2004, 7, 11, 17, 0, 0, 6, 193, 0)),
  507. ),
  508. feedparser._parse_date_hungarian: (
  509. (u'', None), # empty string
  510. (u'2004-j\u00falius-13T9:15-05:00', (2004, 7, 13, 14, 15, 0, 1, 195, 0)),
  511. ),
  512. feedparser._parse_date_iso8601: (
  513. (u'', None), # empty string
  514. (u'-0312', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # 2-digit year/month only variant
  515. (u'031231', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # 2-digit year/month/day only, no hyphens
  516. (u'03-12-31', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # 2-digit year/month/day only
  517. (u'-03-12', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # 2-digit year/month only
  518. (u'03335', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # 2-digit year/ordinal, no hyphens
  519. (u'2003-12-31T10:14:55.1234Z', (2003, 12, 31, 10, 14, 55, 2, 365, 0)), # fractional seconds
  520. # Special case for Google's extra zero in the month
  521. (u'2003-012-31T10:14:55+00:00', (2003, 12, 31, 10, 14, 55, 2, 365, 0)),
  522. ),
  523. feedparser._parse_date_nate: (
  524. (u'', None), # empty string
  525. (u'2004-05-25 \uc624\ud6c4 11:23:17', (2004, 5, 25, 14, 23, 17, 1, 146, 0)),
  526. ),
  527. feedparser._parse_date_onblog: (
  528. (u'', None), # empty string
  529. (u'2004\ub144 05\uc6d4 28\uc77c 01:31:15', (2004, 5, 27, 16, 31, 15, 3, 148, 0)),
  530. ),
  531. feedparser._parse_date_perforce: (
  532. (u'', None), # empty string
  533. (u'Fri, 2006/09/15 08:19:53 EDT', (2006, 9, 15, 12, 19, 53, 4, 258, 0)),
  534. ),
  535. feedparser._parse_date_rfc822: (
  536. (u'', None), # empty string
  537. (u'Thu, 01 Jan 0100 00:00:01 +0100', (99, 12, 31, 23, 0, 1, 3, 365, 0)), # ancient date
  538. (u'Thu, 01 Jan 04 19:48:21 GMT', (2004, 1, 1, 19, 48, 21, 3, 1, 0)), # 2-digit year
  539. (u'Thu, 01 Jan 2004 19:48:21 GMT', (2004, 1, 1, 19, 48, 21, 3, 1, 0)), # 4-digit year
  540. (u'Thu, 5 Apr 2012 10:00:00 GMT', (2012, 4, 5, 10, 0, 0, 3, 96, 0)), # 1-digit day
  541. (u'Wed, 19 Aug 2009 18:28:00 Etc/GMT', (2009, 8, 19, 18, 28, 0, 2, 231, 0)), # etc/gmt timezone
  542. (u'Wed, 19 Feb 2012 22:40:00 GMT-01:01', (2012, 2, 19, 23, 41, 0, 6, 50, 0)), # gmt+hh:mm timezone
  543. (u'Mon, 13 Feb, 2012 06:28:00 UTC', (2012, 2, 13, 6, 28, 0, 0, 44, 0)), # extraneous comma
  544. (u'Thu, 01 Jan 2004 00:00 GMT', (2004, 1, 1, 0, 0, 0, 3, 1, 0)), # no seconds
  545. (u'Thu, 01 Jan 2004', (2004, 1, 1, 0, 0, 0, 3, 1, 0)), # no time
  546. # Additional tests to handle Disney's long month names and invalid timezones
  547. (u'Mon, 26 January 2004 16:31:00 AT', (2004, 1, 26, 20, 31, 0, 0, 26, 0)),
  548. (u'Mon, 26 January 2004 16:31:00 ET', (2004, 1, 26, 21, 31, 0, 0, 26, 0)),
  549. (u'Mon, 26 January 2004 16:31:00 CT', (2004, 1, 26, 22, 31, 0, 0, 26, 0)),
  550. (u'Mon, 26 January 2004 16:31:00 MT', (2004, 1, 26, 23, 31, 0, 0, 26, 0)),
  551. (u'Mon, 26 January 2004 16:31:00 PT', (2004, 1, 27, 0, 31, 0, 1, 27, 0)),
  552. # Swapped month and day
  553. (u'Thu Aug 30 2012 17:26:16 +0200', (2012, 8, 30, 15, 26, 16, 3, 243, 0)),
  554. (u'Sun, 16 Dec 2012 1:2:3:4 GMT', None), # invalid time
  555. (u'Sun, 16 zzz 2012 11:47:32 GMT', None), # invalid month
  556. (u'Sun, Dec x 2012 11:47:32 GMT', None), # invalid day (swapped day/month)
  557. ('Sun, 16 Dec zz 11:47:32 GMT', None), # invalid year
  558. ('Sun, 16 Dec 2012 11:47:32 +zz:00', None), # invalid timezone hour
  559. ('Sun, 16 Dec 2012 11:47:32 +00:zz', None), # invalid timezone minute
  560. ('Sun, 99 Jun 2009 12:00:00 GMT', None), # out-of-range day
  561. ),
  562. feedparser._parse_date_asctime: (
  563. (u'Sun Jan 4 16:29:06 2004', (2004, 1, 4, 16, 29, 6, 6, 4, 0)),
  564. (u'Sun Jul 15 01:16:00 +0000 2012', (2012, 7, 15, 1, 16, 0, 6, 197, 0)),
  565. ),
  566. feedparser._parse_date_w3dtf: (
  567. (u'', None), # empty string
  568. (u'2003-12-31T10:14:55Z', (2003, 12, 31, 10, 14, 55, 2, 365, 0)), # UTC
  569. (u'2003-12-31T10:14:55-08:00', (2003, 12, 31, 18, 14, 55, 2, 365, 0)), # San Francisco timezone
  570. (u'2003-12-31T18:14:55+08:00', (2003, 12, 31, 10, 14, 55, 2, 365, 0)), # Tokyo timezone
  571. (u'2007-04-23T23:25:47.538+10:00', (2007, 4, 23, 13, 25, 47, 0, 113, 0)), # fractional seconds
  572. (u'2003-12-31', (2003, 12, 31, 0, 0, 0, 2, 365, 0)), # year/month/day only
  573. (u'2003-12', (2003, 12, 1, 0, 0, 0, 0, 335, 0)), # year/month only
  574. (u'2003', (2003, 1, 1, 0, 0, 0, 2, 1, 0)), # year only
  575. # Special cases for rollovers in leap years
  576. (u'2004-02-28T18:14:55-08:00', (2004, 2, 29, 2, 14, 55, 6, 60, 0)), # feb 28 in leap year
  577. (u'2003-02-28T18:14:55-08:00', (2003, 3, 1, 2, 14, 55, 5, 60, 0)), # feb 28 in non-leap year
  578. (u'2000-02-28T18:14:55-08:00', (2000, 2, 29, 2, 14, 55, 1, 60, 0)), # feb 28 in leap year on century divisible by 400
  579. # Out-of-range times
  580. (u'9999-12-31T23:59:59-99:99', None), # Date is out-of-range
  581. (u'2003-12-31T25:14:55Z', None), # invalid (25 hours)
  582. (u'2003-12-31T10:61:55Z', None), # invalid (61 minutes)
  583. (u'2003-12-31T10:14:61Z', None), # invalid (61 seconds)
  584. # Invalid formats
  585. (u'22013', None), # Year is too long
  586. (u'013', None), # Year is too short
  587. (u'2013-01-27-01', None), # Date has to many parts
  588. (u'2013-01-28T11:30:00-06:00Textra', None), # Too many 't's
  589. # Non-integer values
  590. (u'2013-xx-27', None), # Date
  591. (u'2013-01-28T09:xx:00Z', None), # Time
  592. (u'2013-01-28T09:00:00+00:xx', None), # Timezone
  593. # MSSQL-style dates
  594. (u'2004-07-08 23:56:58 -00:20', (2004, 7, 9, 0, 16, 58, 4, 191, 0)), # with timezone
  595. (u'2004-07-08 23:56:58', (2004, 7, 8, 23, 56, 58, 3, 190, 0)), # without timezone
  596. (u'2004-07-08 23:56:58.0', (2004, 7, 8, 23, 56, 58, 3, 190, 0)), # with fractional second
  597. )
  598. }
  599. def make_date_test(f, s, t):
  600. return lambda self: self._check_date(f, s, t)
  601. for func, items in date_tests.iteritems():
  602. for i, (dtstring, dttuple) in enumerate(items):
  603. uniqfunc = make_date_test(func, dtstring, dttuple)
  604. setattr(TestDateParsers, 'test_%s_%02i' % (func.__name__, i), uniqfunc)
  605. class TestHTMLGuessing(unittest.TestCase):
  606. "Exercise the HTML sniffing code"
  607. def _mktest(text, expect, doc):
  608. def fn(self):
  609. value = bool(feedparser._FeedParserMixin.lookslikehtml(text))
  610. self.assertEqual(value, expect)
  611. fn.__doc__ = doc
  612. return fn
  613. test_text_1 = _mktest(u'plain text', False, u'plain text')
  614. test_text_2 = _mktest(u'2 < 3', False, u'plain text with angle bracket')
  615. test_html_1 = _mktest(u'<a href="">a</a>', True, u'anchor tag')
  616. test_html_2 = _mktest(u'<i>i</i>', True, u'italics tag')
  617. test_html_3 = _mktest(u'<b>b</b>', True, u'bold tag')
  618. test_html_4 = _mktest(u'<code>', False, u'allowed tag, no end tag')
  619. test_html_5 = _mktest(u'<rss> .. </rss>', False, u'disallowed tag')
  620. test_entity_1 = _mktest(u'AT&T', False, u'corporation name')
  621. test_entity_2 = _mktest(u'&copy;', True, u'named entity reference')
  622. test_entity_3 = _mktest(u'&#169;', True, u'numeric entity reference')
  623. test_entity_4 = _mktest(u'&#xA9;', True, u'hex numeric entity reference')
  624. #---------- additional api unit tests, not backed by files
  625. class TestBuildRequest(unittest.TestCase):
  626. "Test that HTTP request objects are created as expected"
  627. def test_extra_headers(self):
  628. """You can pass in extra headers and they go into the request object."""
  629. request = feedparser._build_urllib2_request(
  630. 'http://example.com/feed',
  631. 'agent-name',
  632. None, None, None, None,
  633. {'Cache-Control': 'max-age=0'})
  634. # nb, urllib2 folds the case of the headers
  635. self.assertEqual(
  636. request.get_header('Cache-control'), 'max-age=0')
  637. class TestLxmlBug(unittest.TestCase):
  638. def test_lxml_etree_bug(self):
  639. try:
  640. import lxml.etree
  641. except ImportError:
  642. pass
  643. else:
  644. doc = u"<feed>&illformed_charref</feed>".encode('utf8')
  645. # Importing lxml.etree currently causes libxml2 to
  646. # throw SAXException instead of SAXParseException.
  647. feedparser.parse(feedparser._StringIO(doc))
  648. self.assertTrue(True)
  649. #---------- parse test files and create test methods ----------
  650. def convert_to_utf8(data):
  651. "Identify data's encoding using its byte order mark" \
  652. "and convert it to its utf-8 equivalent"
  653. if data[:4] == _l2bytes([0x4c, 0x6f, 0xa7, 0x94]):
  654. return data.decode('cp037').encode('utf-8')
  655. elif data[:4] == _l2bytes([0x00, 0x00, 0xfe, 0xff]):
  656. if not _UTF32_AVAILABLE:
  657. return None
  658. return data.decode('utf-32be').encode('utf-8')
  659. elif data[:4] == _l2bytes([0xff, 0xfe, 0x00, 0x00]):
  660. if not _UTF32_AVAILABLE:
  661. return None
  662. return data.decode('utf-32le').encode('utf-8')
  663. elif data[:4] == _l2bytes([0x00, 0x00, 0x00, 0x3c]):
  664. if not _UTF32_AVAILABLE:
  665. return None
  666. return data.decode('utf-32be').encode('utf-8')
  667. elif data[:4] == _l2bytes([0x3c, 0x00, 0x00, 0x00]):
  668. if not _UTF32_AVAILABLE:
  669. return None
  670. return data.decode('utf-32le').encode('utf-8')
  671. elif data[:4] == _l2bytes([0x00, 0x3c, 0x00, 0x3f]):
  672. return data.decode('utf-16be').encode('utf-8')
  673. elif data[:4] == _l2bytes([0x3c, 0x00, 0x3f, 0x00]):
  674. return data.decode('utf-16le').encode('utf-8')
  675. elif (data[:2] == _l2bytes([0xfe, 0xff])) and (data[2:4] != _l2bytes([0x00, 0x00])):
  676. return data[2:].decode('utf-16be').encode('utf-8')
  677. elif (data[:2] == _l2bytes([0xff, 0xfe])) and (data[2:4] != _l2bytes([0x00, 0x00])):
  678. return data[2:].decode('utf-16le').encode('utf-8')
  679. elif data[:3] == _l2bytes([0xef, 0xbb, 0xbf]):
  680. return data[3:]
  681. # no byte order mark was found
  682. return data
  683. skip_re = re.compile(_s2bytes("SkipUnless:\s*(.*?)\n"))
  684. desc_re = re.compile(_s2bytes("Description:\s*(.*?)\s*Expect:\s*(.*)\s*-->"))
  685. def getDescription(xmlfile, data):
  686. """Extract test data
  687. Each test case is an XML file which contains not only a test feed
  688. but also the description of the test and the condition that we
  689. would expect the parser to create when it parses the feed. Example:
  690. <!--
  691. Description: feed title
  692. Expect: feed['title'] == u'Example feed'
  693. -->
  694. """
  695. skip_results = skip_re.search(data)
  696. if skip_results:
  697. skipUnless = skip_results.group(1).strip()
  698. else:
  699. skipUnless = '1'
  700. search_results = desc_re.search(data)
  701. if not search_results:
  702. raise RuntimeError, "can't parse %s" % xmlfile
  703. description, evalString = map(lambda s: s.strip(), list(search_results.groups()))
  704. description = xmlfile + ": " + unicode(description, 'utf8')
  705. return description, evalString, skipUnless
  706. def buildTestCase(xmlfile, description, evalString):
  707. func = lambda self, xmlfile=xmlfile, evalString=evalString: \
  708. self.failUnlessEval(xmlfile, evalString)
  709. func.__doc__ = description
  710. return func
  711. def runtests():
  712. "Read the files in the tests/ directory, dynamically add tests to the " \
  713. "TestCases above, spawn the HTTP server, and run the test suite"
  714. if sys.argv[1:]:
  715. allfiles = filter(lambda s: s.endswith('.xml'), reduce(operator.add, map(glob.glob, sys.argv[1:]), []))
  716. wellformedfiles = illformedfiles = encodingfiles = entitiesfiles = microformatfiles = []
  717. sys.argv = [sys.argv[0]] #+ sys.argv[2:]
  718. else:
  719. allfiles = glob.glob(os.path.join('.', 'tests', '**', '**', '*.xml'))
  720. wellformedfiles = glob.glob(os.path.join('.', 'tests', 'wellformed', '**', '*.xml'))
  721. illformedfiles = glob.glob(os.path.join('.', 'tests', 'illformed', '*.xml'))
  722. encodingfiles = glob.glob(os.path.join('.', 'tests', 'encoding', '*.xml'))
  723. entitiesfiles = glob.glob(os.path.join('.', 'tests', 'entities', '*.xml'))
  724. microformatfiles = glob.glob(os.path.join('.', 'tests', 'microformats', '**', '*.xml'))
  725. httpd = None
  726. # there are several compression test cases that must be accounted for
  727. # as well as a number of http status tests that redirect to a target
  728. # and a few `_open_resource`-related tests
  729. httpcount = 6 + 16 + 2
  730. httpcount += len([f for f in allfiles if 'http' in f])
  731. httpcount += len([f for f in wellformedfiles if 'http' in f])
  732. httpcount += len([f for f in illformedfiles if 'http' in f])
  733. httpcount += len([f for f in encodingfiles if 'http' in f])
  734. try:
  735. for c, xmlfile in enumerate(allfiles + encodingfiles + illformedfiles + entitiesfiles):
  736. addTo = TestCase
  737. if xmlfile in encodingfiles:
  738. addTo = TestEncodings
  739. elif xmlfile in entitiesfiles:
  740. addTo = (TestStrictParser, TestLooseParser)
  741. elif xmlfile in microformatfiles:
  742. addTo = TestMicroformats
  743. elif xmlfile in wellformedfiles:
  744. addTo = (TestStrictParser, TestLooseParser)
  745. f = open(xmlfile, 'rb')
  746. data = f.read()
  747. f.close()
  748. if 'encoding' in xmlfile:
  749. data = convert_to_utf8(data)
  750. if data is None:
  751. # convert_to_utf8 found a byte order mark for utf_32
  752. # but it's not supported in this installation of Python
  753. if 'http' in xmlfile:
  754. httpcount -= 1 + (xmlfile in wellformedfiles)
  755. continue
  756. description, evalString, skipUnless = getDescription(xmlfile, data)
  757. testName = 'test_%06d' % c
  758. ishttp = 'http' in xmlfile
  759. try:
  760. if not eval(skipUnless): raise NotImplementedError
  761. except (ImportError, LookupError, NotImplementedError, AttributeError):
  762. if ishttp:
  763. httpcount -= 1 + (xmlfile in wellformedfiles)
  764. continue
  765. if ishttp:
  766. xmlfile = 'http://%s:%s/%s' % (_HOST, _PORT, posixpath.normpath(xmlfile.replace('\\', '/')))
  767. testFunc = buildTestCase(xmlfile, description, evalString)
  768. if isinstance(addTo, tuple):
  769. setattr(addTo[0], testName, testFunc)
  770. setattr(addTo[1], testName, testFunc)
  771. else:
  772. setattr(addTo, testName, testFunc)
  773. if httpcount:
  774. httpd = FeedParserTestServer(httpcount)
  775. httpd.daemon = True
  776. httpd.start()
  777. httpd.ready.wait()
  778. testsuite = unittest.TestSuite()
  779. testloader = unittest.TestLoader()
  780. testsuite.addTest(testloader.loadTestsFromTestCase(TestCase))
  781. testsuite.addTest(testloader.loadTestsFromTestCase(TestStrictParser))
  782. testsuite.addTest(testloader.loadTestsFromTestCase(TestLooseParser))
  783. testsuite.addTest(testloader.loadTestsFromTestCase(TestEncodings))
  784. testsuite.addTest(testloader.loadTestsFromTestCase(TestDateParsers))
  785. testsuite.addTest(testloader.loadTestsFromTestCase(TestHTMLGuessing))
  786. testsuite.addTest(testloader.loadTestsFromTestCase(TestHTTPStatus))
  787. testsuite.addTest(testloader.loadTestsFromTestCase(TestCompression))
  788. testsuite.addTest(testloader.loadTestsFromTestCase(TestConvertToIdn))
  789. testsuite.addTest(testloader.loadTestsFromTestCase(TestMicroformats))
  790. testsuite.addTest(testloader.loadTestsFromTestCase(TestOpenResource))
  791. testsuite.addTest(testloader.loadTestsFromTestCase(TestFeedParserDict))
  792. testsuite.addTest(testloader.loadTestsFromTestCase(TestMakeSafeAbsoluteURI))
  793. testsuite.addTest(testloader.loadTestsFromTestCase(TestEverythingIsUnicode))
  794. testsuite.addTest(testloader.loadTestsFromTestCase(TestTemporaryFallbackBehavior))
  795. testsuite.addTest(testloader.loadTestsFromTestCase(TestLxmlBug))
  796. testresults = unittest.TextTestRunner(verbosity=1).run(testsuite)
  797. # Return 0 if successful, 1 if there was a failure
  798. sys.exit(not testresults.wasSuccessful())
  799. finally:
  800. if httpd:
  801. if httpd.requests:
  802. # Should never get here unless something went horribly wrong, like the
  803. # user hitting Ctrl-C. Tell our HTTP server that it's done, then do
  804. # one more request to flush it. This rarely works; the combination of
  805. # threading, self-terminating HTTP servers, and unittest is really
  806. # quite flaky. Just what you want in a testing framework, no?
  807. httpd.requests = 0
  808. if httpd.ready:
  809. urllib.urlopen('http://127.0.0.1:8097/tests/wellformed/rss/aaa_wellformed.xml').read()
  810. httpd.join(0)
  811. if __name__ == "__main__":
  812. runtests()