test_howdoi.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. #!/usr/bin/env python
  2. """Tests for Howdoi."""
  3. import gzip
  4. import json
  5. import os
  6. import re
  7. import unittest
  8. from pathlib import Path
  9. from unittest.mock import patch
  10. import requests
  11. from cachelib import NullCache
  12. from pyquery import PyQuery as pq
  13. # pylint: disable=no-name-in-module
  14. from howdoi import howdoi
  15. # pylint: disable=protected-access
  16. original_get_result = howdoi._get_result
  17. def _format_url_to_filename(url, file_ext='html'):
  18. filename = ''.join(ch for ch in url if ch.isalnum())
  19. return filename + '.' + file_ext
  20. def _get_result_mock(url):
  21. # pylint: disable=protected-access
  22. file_name = _format_url_to_filename(url, 'html.gz')
  23. # pylint: disable=no-member
  24. file_path = Path.joinpath(Path(howdoi.HTML_CACHE_PATH), Path(file_name)).resolve()
  25. try:
  26. with gzip.open(file_path, 'rb') as f:
  27. cached_page_content = str(f.read(), encoding='utf-8')
  28. return cached_page_content
  29. except FileNotFoundError:
  30. page_content = original_get_result(url)
  31. with gzip.open(file_path, 'wb') as f:
  32. f.write(bytes(page_content, encoding='utf-8'))
  33. return page_content
  34. # pylint: disable=protected-access
  35. class HowdoiTestCase(unittest.TestCase): # pylint: disable=too-many-public-methods
  36. def setUp(self):
  37. self.patcher_get_result = patch.object(howdoi, '_get_result')
  38. self.mock_get_result = self.patcher_get_result.start()
  39. self.mock_get_result.side_effect = _get_result_mock
  40. # ensure no cache is used during testing.
  41. howdoi.cache = NullCache()
  42. self.queries = ['format date bash',
  43. 'print stack trace python',
  44. 'convert mp4 to animated gif',
  45. 'create tar archive',
  46. 'cat']
  47. self.help_queries = howdoi.SUPPORTED_HELP_QUERIES
  48. self.pt_queries = ['abrir arquivo em python',
  49. 'enviar email em django',
  50. 'hello world em c']
  51. self.bad_queries = ['moe',
  52. 'mel']
  53. self.query_without_code_or_pre_block = 'Difference between element node and Text Node'
  54. def tearDown(self):
  55. self.patcher_get_result.stop()
  56. keys_to_remove = ['HOWDOI_URL', 'HOWDOI_SEARCH_ENGINE']
  57. for key in keys_to_remove:
  58. if key in os.environ:
  59. del os.environ[key]
  60. howdoi.BLOCKED_ENGINES = []
  61. def _negative_number_query(self):
  62. query = self.queries[0]
  63. howdoi.howdoi(query + ' -n -1')
  64. def _high_positive_number_query(self):
  65. query = self.queries[0]
  66. howdoi.howdoi(query + ' -n 21')
  67. def _negative_position_query(self):
  68. query = self.queries[0]
  69. howdoi.howdoi(query + ' -p -2')
  70. def _high_positive_position_query(self):
  71. query = self.queries[0]
  72. howdoi.howdoi(query + ' -p 40')
  73. def assertValidResponse(self, res): # pylint: disable=invalid-name
  74. self.assertTrue(len(res) > 0)
  75. def test_get_link_at_pos(self):
  76. self.assertEqual(howdoi.get_link_at_pos(['/questions/42/'], 1),
  77. '/questions/42/')
  78. self.assertEqual(howdoi.get_link_at_pos(['/questions/42/'], 2),
  79. '/questions/42/')
  80. self.assertEqual(howdoi.get_link_at_pos(['/howdoi', '/questions/42/'], 1),
  81. '/howdoi')
  82. self.assertEqual(howdoi.get_link_at_pos(['/howdoi', '/questions/42/'], 2),
  83. '/questions/42/')
  84. self.assertEqual(howdoi.get_link_at_pos(['/questions/42/', '/questions/142/'], 1),
  85. '/questions/42/')
  86. @patch.object(howdoi, '_get_result')
  87. def test_blockerror(self, mock_get_links):
  88. mock_get_links.side_effect = requests.HTTPError
  89. query = self.queries[0]
  90. response = howdoi.howdoi(query)
  91. self.assertEqual(response, "ERROR: \x1b[91mUnable to get a response from any search engine\n\x1b[0m")
  92. def test_answers(self):
  93. for query in self.queries:
  94. self.assertValidResponse(howdoi.howdoi(query))
  95. for query in self.bad_queries:
  96. self.assertValidResponse(howdoi.howdoi(query))
  97. os.environ['HOWDOI_URL'] = 'pt.stackoverflow.com'
  98. for query in self.pt_queries:
  99. self.assertValidResponse(howdoi.howdoi(query))
  100. def test_answers_bing(self):
  101. os.environ['HOWDOI_SEARCH_ENGINE'] = 'bing'
  102. for query in self.queries:
  103. self.assertValidResponse(howdoi.howdoi(query))
  104. for query in self.bad_queries:
  105. self.assertValidResponse(howdoi.howdoi(query))
  106. os.environ['HOWDOI_URL'] = 'pt.stackoverflow.com'
  107. for query in self.pt_queries:
  108. self.assertValidResponse(howdoi.howdoi(query))
  109. os.environ['HOWDOI_SEARCH_ENGINE'] = ''
  110. # commenting out duckduckgo test, re-enable when issue #404 (duckduckgo blocking requests) is resolved
  111. # def test_answers_duckduckgo(self):
  112. # os.environ['HOWDOI_SEARCH_ENGINE'] = 'duckduckgo'
  113. # for query in self.queries:
  114. # self.assertValidResponse(howdoi.howdoi(query))
  115. # for query in self.bad_queries:
  116. # self.assertValidResponse(howdoi.howdoi(query))
  117. # os.environ['HOWDOI_URL'] = 'pt.stackoverflow.com'
  118. # for query in self.pt_queries:
  119. # self.assertValidResponse(howdoi.howdoi(query))
  120. # os.environ['HOWDOI_SEARCH_ENGINE'] = ''
  121. def test_answer_links_using_l_option(self):
  122. for query in self.queries:
  123. response = howdoi.howdoi(query + ' -l')
  124. self.assertNotEqual(re.match(r'http.?://.*questions/\d.*', response, re.DOTALL), None)
  125. def test_answer_links_using_all_option(self):
  126. for query in self.queries:
  127. response = howdoi.howdoi(query + ' -a')
  128. self.assertNotEqual(re.match(r'.*http.?://.*questions/\d.*', response, re.DOTALL), None)
  129. def test_position(self):
  130. query = self.queries[0]
  131. first_answer = howdoi.howdoi(query)
  132. not_first_answer = howdoi.howdoi(query + ' -p5')
  133. self.assertNotEqual(first_answer, not_first_answer)
  134. def test_all_text(self):
  135. query = self.queries[0]
  136. first_answer = howdoi.howdoi(query)
  137. second_answer = howdoi.howdoi(query + ' -a')
  138. self.assertNotEqual(first_answer, second_answer)
  139. self.assertNotEqual(re.match('.*Answer from http.?://.*', second_answer, re.DOTALL), None)
  140. def test_json_output(self):
  141. query = self.queries[0]
  142. txt_answer = howdoi.howdoi(query)
  143. json_answer = howdoi.howdoi(query + ' -j')
  144. link_answer = howdoi.howdoi(query + ' -l')
  145. json_answer = json.loads(json_answer)[0]
  146. self.assertEqual(json_answer["answer"], txt_answer)
  147. self.assertEqual(json_answer["link"], link_answer)
  148. self.assertEqual(json_answer["position"], 1)
  149. def test_multiple_answers(self):
  150. query = self.queries[0]
  151. first_answer = howdoi.howdoi(query)
  152. second_answer = howdoi.howdoi(query + ' -n3')
  153. self.assertNotEqual(first_answer, second_answer)
  154. def test_unicode_answer(self):
  155. assert howdoi.howdoi('make a log scale d3')
  156. assert howdoi.howdoi('python unittest -n3')
  157. assert howdoi.howdoi('parse html regex -a')
  158. assert howdoi.howdoi('delete remote git branch -a')
  159. def test_colorize(self):
  160. query = self.queries[0]
  161. normal = howdoi.howdoi(query)
  162. colorized = howdoi.howdoi('-c ' + query)
  163. # There is currently an issue with Github actions and colorization
  164. # so do not run checks if we are running in Github
  165. if "GITHUB_ACTION" not in os.environ:
  166. self.assertTrue(normal.find('[38;') == -1)
  167. self.assertTrue(colorized.find('[38;') != -1)
  168. # pylint: disable=line-too-long
  169. def test_get_text_without_links(self):
  170. html = '''\n <p>The halting problem is basically a\n formal way of asking if you can tell\n whether or not an arbitrary program\n will eventually halt.</p>\n \n <p>In other words, can you write a\n program called a halting oracle,\n HaltingOracle(program, input), which\n returns true if program(input) would\n eventually halt, and which returns\n false if it wouldn't?</p>\n \n <p>The answer is: no, you can't.</p>\n''' # noqa: E501
  171. paragraph = pq(html)
  172. expected_output = '''The halting problem is basically a\n formal way of asking if you can tell\n whether or not an arbitrary program\n will eventually halt.\n\n \n \nIn other words, can you write a\n program called a halting oracle,\n HaltingOracle(program, input), which\n returns true if program(input) would\n eventually halt, and which returns\n false if it wouldn't?\n\n \n \nThe answer is: no, you can't.\n\n''' # noqa: E501
  173. actual_output = howdoi.get_text(paragraph)
  174. self.assertEqual(actual_output, expected_output)
  175. def test_get_text_with_one_link(self):
  176. html = '<p>It\'s a <a href="http://paulirish.com/2010/the-protocol-relative-url/">protocol-relative URL</a> (typically HTTP or HTTPS). So if I\'m on <code>http://example.org</code> and I link (or include an image, script, etc.) to <code>//example.com/1.png</code>, it goes to <code>http://example.com/1.png</code>. If I\'m on <code>https://example.org</code>, it goes to <code>https://example.com/1.png</code>.</p>' # noqa: E501
  177. paragraph = pq(html)
  178. expected_output = "It's a [protocol-relative URL](http://paulirish.com/2010/the-protocol-relative-url/) (typically HTTP or HTTPS). So if I'm on http://example.org and I link (or include an image, script, etc.) to //example.com/1.png, it goes to http://example.com/1.png. If I'm on https://example.org, it goes to https://example.com/1.png." # noqa: E501
  179. actual_output = howdoi.get_text(paragraph)
  180. self.assertEqual(actual_output, expected_output)
  181. def test_get_text_with_multiple_links_test_one(self):
  182. html = 'Here\'s a quote from <a href="http://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style#Links" rel="nofollow noreferrer">wikipedia\'s manual of style</a> section on links (but see also <a href="http://en.wikipedia.org/wiki/Wikipedia:External_links" rel="nofollow noreferrer">their comprehensive page on External Links</a>)' # noqa: E501
  183. paragraph = pq(html)
  184. expected_output = "Here's a quote from [wikipedia's manual of style](http://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style#Links) section on links (but see also [their comprehensive page on External Links](http://en.wikipedia.org/wiki/Wikipedia:External_links))" # noqa: E501
  185. actual_output = howdoi.get_text(paragraph)
  186. self.assertEqual(actual_output, expected_output)
  187. def test_get_text_with_multiple_links_test_two(self):
  188. html = 'For example, if I were to reference <a href="http://www.apple.com/" rel="nofollow noreferrer">apple.com</a> as the subject of a sentence - or to talk about <a href="http://www.apple.com/" rel="nofollow noreferrer">Apple\'s website</a> as the topic of conversation. This being different to perhaps recommendations for reading <a href="https://ux.stackexchange.com/q/14872/6046">our article about Apple\'s website</a>.' # noqa: E501
  189. paragraph = pq(html)
  190. expected_output = "For example, if I were to reference [apple.com](http://www.apple.com/) as the subject of a sentence - or to talk about [Apple's website](http://www.apple.com/) as the topic of conversation. This being different to perhaps recommendations for reading [our article about Apple's website](https://ux.stackexchange.com/q/14872/6046)." # noqa: E501
  191. actual_output = howdoi.get_text(paragraph)
  192. self.assertEqual(actual_output, expected_output)
  193. def test_get_text_with_link_but_with_copy_duplicating_the_href(self):
  194. html = '<a href="https://github.com/jquery/jquery/blob/56136897f241db22560b58c3518578ca1453d5c7/src/manipulation.js#L451" rel="nofollow noreferrer">https://github.com/jquery/jquery/blob/56136897f241db22560b58c3518578ca1453d5c7/src/manipulation.js#L451</a>' # noqa: E501
  195. paragraph = pq(html)
  196. expected_output = 'https://github.com/jquery/jquery/blob/56136897f241db22560b58c3518578ca1453d5c7/src/manipulation.js#L451' # noqa: E501
  197. actual_output = howdoi.get_text(paragraph)
  198. self.assertEqual(actual_output, expected_output)
  199. def test_get_text_with_a_link_but_copy_is_within_nested_div(self):
  200. html = 'If the function is from a source file available on the filesystem, then <a href="https://docs.python.org/3/library/inspect.html#inspect.getsource" rel="noreferrer"><code>inspect.getsource(foo)</code></a> might be of help:' # noqa: E501
  201. paragraph = pq(html)
  202. expected_output = 'If the function is from a source file available on the filesystem, then [inspect.getsource(foo)](https://docs.python.org/3/library/inspect.html#inspect.getsource) might be of help:' # noqa: E501
  203. actual_output = howdoi.get_text(paragraph)
  204. self.assertEqual(actual_output, expected_output)
  205. # pylint: enable=line-too-long
  206. def test_get_questions(self):
  207. links = ['https://stackoverflow.com/questions/tagged/cat',
  208. 'http://rads.stackoverflow.com/amzn/click/B007KAZ166',
  209. 'https://stackoverflow.com/questions/40108569/how-to-get-the-last-line-of-a-file-using-cat-command']
  210. expected_output = [
  211. 'https://stackoverflow.com/questions/40108569/how-to-get-the-last-line-of-a-file-using-cat-command']
  212. actual_output = howdoi._get_questions(links)
  213. self.assertSequenceEqual(actual_output, expected_output)
  214. def test_help_queries(self):
  215. help_queries = self.help_queries
  216. for query in help_queries:
  217. output = howdoi.howdoi(query)
  218. self.assertTrue(output)
  219. self.assertIn('few popular howdoi commands', output)
  220. self.assertIn('retrieve n number of answers', output)
  221. self.assertIn(
  222. 'Specify the search engine you want to use e.g google,bing',
  223. output
  224. )
  225. def test_missing_pre_or_code_query(self):
  226. output = howdoi.howdoi(self.query_without_code_or_pre_block)
  227. self.assertTrue(output)
  228. def test_format_url_to_filename(self):
  229. url = 'https://stackoverflow.com/questions/tagged/cat'
  230. invalid_filename_characters = ['/', '\\', '%']
  231. filename = _format_url_to_filename(url, 'html')
  232. self.assertTrue(filename)
  233. self.assertTrue(filename.endswith('html'))
  234. for invalid_character in invalid_filename_characters:
  235. self.assertNotIn(invalid_character, filename)
  236. def test_help_queries_are_properly_validated(self):
  237. help_queries = self.help_queries
  238. for query in help_queries:
  239. is_valid_help_query = howdoi._is_help_query(query)
  240. self.assertTrue(is_valid_help_query)
  241. bad_help_queries = [self.queries[0],
  242. self.bad_queries[0], 'use how do i']
  243. for query in bad_help_queries:
  244. self.assertFalse(howdoi._is_help_query(query))
  245. def test_negative_and_high_positive_int_values_rejected(self):
  246. with self.assertRaises(SystemExit):
  247. self._negative_number_query()
  248. with self.assertRaises(SystemExit):
  249. self._negative_position_query()
  250. with self.assertRaises(SystemExit):
  251. self._high_positive_position_query()
  252. with self.assertRaises(SystemExit):
  253. self._high_positive_number_query()
  254. class HowdoiTestCaseEnvProxies(unittest.TestCase):
  255. def setUp(self):
  256. self.temp_get_proxies = howdoi.getproxies
  257. def tearDown(self):
  258. howdoi.getproxies = self.temp_get_proxies
  259. def test_get_proxies1(self):
  260. def getproxies1():
  261. proxies = {'http': 'wwwproxy.company.com',
  262. 'https': 'wwwproxy.company.com',
  263. 'ftp': 'ftpproxy.company.com'}
  264. return proxies
  265. howdoi.getproxies = getproxies1
  266. filtered_proxies = howdoi.get_proxies()
  267. self.assertTrue('http://' in filtered_proxies['http'])
  268. self.assertTrue('http://' in filtered_proxies['https'])
  269. self.assertTrue('ftp' not in filtered_proxies.keys()) # pylint: disable=consider-iterating-dictionary
  270. if __name__ == '__main__':
  271. unittest.main()