aboutsummaryrefslogtreecommitdiff
path: root/runtests.py
blob: 906a5732a08a4e8e16c4faa7fadbff494fa4b7a6 (plain)
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from ctypes import CDLL, c_char_p, c_long
  4. import sys
  5. import platform
  6. from difflib import unified_diff
  7. from subprocess import *
  8. import argparse
  9. from HTMLParser import HTMLParser
  10. from htmlentitydefs import name2codepoint
  11. import re
  12. if __name__ == "__main__":
  13. parser = argparse.ArgumentParser(description='Run cmark tests.')
  14. parser.add_argument('--program', dest='program', nargs='?', default=None,
  15. help='program to test')
  16. parser.add_argument('--spec', dest='spec', nargs='?', default='spec.txt',
  17. help='path to spec')
  18. parser.add_argument('--pattern', dest='pattern', nargs='?',
  19. default=None, help='limit to sections matching regex pattern')
  20. parser.add_argument('--library_dir', dest='library_dir', nargs='?',
  21. default=None, help='directory containing dynamic library')
  22. args = parser.parse_args(sys.argv[1:])
  23. if not args.program:
  24. sysname = platform.system()
  25. libname = "libcmark"
  26. if sysname == 'Darwin':
  27. libname += ".dylib"
  28. elif sysname == 'Windows':
  29. libname += ".dll"
  30. else:
  31. libname += ".so"
  32. if args and args.library_dir:
  33. libpath = args.library_dir + "/" + libname
  34. else:
  35. libpath = "build/src/" + libname
  36. cmark = CDLL(libpath)
  37. markdown = cmark.cmark_markdown_to_html
  38. markdown.restype = c_char_p
  39. markdown.argtypes = [c_char_p, c_long]
  40. def md2html(text, prog):
  41. if prog:
  42. p1 = Popen([prog], stdout=PIPE, stdin=PIPE, stderr=PIPE)
  43. [result, err] = p1.communicate(input=text)
  44. return [p1.returncode, result, err]
  45. else:
  46. return [0, markdown(text, len(text)), '']
  47. # Normalization code, adapted from
  48. # https://github.com/karlcow/markdown-testsuite/
  49. significant_attrs = ["alt", "href", "src", "title"]
  50. normalize_whitespace_re = re.compile('\s+')
  51. class MyHTMLParser(HTMLParser):
  52. def __init__(self):
  53. HTMLParser.__init__(self)
  54. self.last = "starttag"
  55. self.in_pre = False
  56. self.output = u""
  57. def handle_data(self, data):
  58. if self.in_pre:
  59. self.output += data
  60. else:
  61. data = normalize_whitespace_re.sub(' ', data)
  62. data_strip = data.strip()
  63. if (self.last == "ref") and data_strip and data[0] == " ":
  64. self.output += " "
  65. self.data_end_in_space_not_empty = (data[-1] == ' ' and data_strip)
  66. self.output += data_strip
  67. self.last = "data"
  68. def handle_endtag(self, tag):
  69. if tag == "pre":
  70. self.in_pre = False
  71. self.output += "</" + tag + ">"
  72. self.last = "endtag"
  73. def handle_starttag(self, tag, attrs):
  74. if tag == "pre":
  75. self.in_pre = True
  76. self.output += "<" + tag
  77. attrs = filter(lambda attr: attr[0] in significant_attrs, attrs)
  78. if attrs:
  79. attrs.sort()
  80. for attr in attrs:
  81. self.output += " " + attr[0] + "=" + '"' + attr[1] + '"'
  82. self.output += ">"
  83. self.last = "starttag"
  84. def handle_startendtag(self, tag, attrs):
  85. """Ignore closing tag for self-closing void elements."""
  86. self.handle_starttag(tag, attrs)
  87. def handle_entityref(self, name):
  88. self.add_space_from_last_data()
  89. try:
  90. self.output += unichr(name2codepoint[name])
  91. except KeyError:
  92. self.output += name
  93. self.last = "ref"
  94. def handle_charref(self, name):
  95. self.add_space_from_last_data()
  96. try:
  97. if name.startswith("x"):
  98. c = unichr(int(name[1:], 16))
  99. else:
  100. c = unichr(int(name))
  101. self.output += c
  102. except ValueError:
  103. self.output += name
  104. self.last = "ref"
  105. # Helpers.
  106. def add_space_from_last_data(self):
  107. """Maintain the space at: `a <span>b</span>`"""
  108. if self.last == 'data' and self.data_end_in_space_not_empty:
  109. self.output += ' '
  110. def normalize(html):
  111. r"""
  112. Return normalized form of HTML which igores insignificant output differences.
  113. Multiple inner whitespaces to a single space
  114. >>> normalize("<p>a \t\nb</p>")
  115. u'<p>a b</p>'
  116. Surrounding whitespaces are removed:
  117. >>> normalize("<p> a</p>")
  118. u'<p>a</p>'
  119. >>> normalize("<p>a </p>")
  120. u'<p>a</p>'
  121. TODO: how to deal with the following cases without a full list of the void tags?
  122. >>> normalize("<p>a <b>b</b></p>")
  123. u'<p>a<b>b</b></p>'
  124. >>> normalize("<p><b>b</b> c</p>")
  125. u'<p><b>b</b>c</p>'
  126. >>> normalize("<p>a <br></p>")
  127. u'<p>a<br></p>'
  128. `pre` elements preserve whitespace:
  129. >>> normalize("<pre>a \t\nb</pre>")
  130. u'<pre>a \t\nb</pre>'
  131. Self-closing tags:
  132. >>> normalize("<p><br /></p>")
  133. u'<p><br></p>'
  134. References are converted to Unicode:
  135. >>> normalize("<p>&lt;</p>")
  136. u'<p><</p>'
  137. >>> normalize("<p>&#60;</p>")
  138. u'<p><</p>'
  139. >>> normalize("<p>&#x3C;</p>")
  140. u'<p><</p>'
  141. >>> normalize("<p>&#x4E2D;</p>")
  142. u'<p>\u4e2d</p>'
  143. Spaces around entities are kept:
  144. >>> normalize("<p>a &lt; b</p>")
  145. u'<p>a < b</p>'
  146. >>> normalize("<p>a&lt;b</p>")
  147. u'<p>a<b</p>'
  148. Most attributes are ignored:
  149. >>> normalize('<p id="a"></p>')
  150. u'<p></p>'
  151. Critical attributes are considered and sorted alphabetically:
  152. >>> normalize('<a href="a"></a>')
  153. u'<a href="a"></a>'
  154. >>> normalize('<img src="a" alt="a">')
  155. u'<img alt="a" src="a">'
  156. """
  157. parser = MyHTMLParser()
  158. parser.feed(html.decode(encoding='UTF-8'))
  159. parser.close()
  160. return parser.output
  161. def print_test_header(headertext, example_number, start_line, end_line):
  162. print "Example %d (lines %d-%d) %s" % (example_number,start_line,end_line,headertext)
  163. def do_test(markdown_lines, expected_html_lines, headertext,
  164. example_number, start_line, end_line, prog=None):
  165. real_markdown_text = ''.join(markdown_lines).replace('→','\t')
  166. [retcode, actual_html, err] = md2html(real_markdown_text, prog)
  167. if retcode == 0:
  168. actual_html_lines = actual_html.splitlines(True)
  169. expected_html = ''.join(expected_html_lines)
  170. if normalize(actual_html) == normalize(expected_html):
  171. return 'pass'
  172. else:
  173. print_test_header(headertext, example_number,start_line,end_line)
  174. sys.stdout.write(real_markdown_text)
  175. for diffline in unified_diff(expected_html_lines, actual_html_lines,
  176. "expected HTML", "actual HTML"):
  177. sys.stdout.write(diffline)
  178. sys.stdout.write('\n')
  179. return 'fail'
  180. else:
  181. print_test_header(example_number,start_line,end_line)
  182. print "program returned error code %d" % retcode
  183. print(err)
  184. return 'error'
  185. def do_tests(specfile, prog, pattern):
  186. line_number = 0
  187. start_line = 0
  188. end_line = 0
  189. example_number = 0
  190. passed = 0
  191. failed = 0
  192. errored = 0
  193. markdown_lines = []
  194. html_lines = []
  195. active = True
  196. state = 0 # 0 regular text, 1 markdown example, 2 html output
  197. headertext = ''
  198. header_re = re.compile('#+ ')
  199. if pattern:
  200. pattern_re = re.compile(pattern)
  201. with open(specfile, 'r') as specf:
  202. for line in specf:
  203. line_number = line_number + 1
  204. if state == 0 and re.match(header_re, line):
  205. headertext = header_re.sub('', line).strip()
  206. if pattern:
  207. if re.search(pattern_re, line):
  208. active = True
  209. else:
  210. active = False
  211. if line.strip() == ".":
  212. state = (state + 1) % 3
  213. if state == 0:
  214. example_number = example_number + 1
  215. end_line = line_number
  216. if active:
  217. result = do_test(markdown_lines, html_lines,
  218. headertext, example_number,
  219. start_line, end_line, prog)
  220. if result == 'pass':
  221. passed = passed + 1
  222. elif result == 'fail':
  223. failed = failed + 1
  224. else:
  225. errored = errored + 1
  226. start_line = 0
  227. markdown_lines = []
  228. html_lines = []
  229. elif state == 1:
  230. if start_line == 0:
  231. start_line = line_number
  232. markdown_lines.append(line)
  233. elif state == 2:
  234. html_lines.append(line)
  235. print "%d passed, %d failed, %d errored" % (passed, failed, errored)
  236. return (failed == 0 and errored == 0)
  237. if __name__ == "__main__":
  238. if do_tests(args.spec, args.program, args.pattern):
  239. exit(0)
  240. else:
  241. exit(1)