DocTests.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import re
  5. import sys
  6. import shlex
  7. import hashlib
  8. import argparse
  9. import subprocess
  10. from difflib import unified_diff
  11. class DocTests:
  12. def __init__(self, args):
  13. scriptpath = os.path.dirname(os.path.realpath(__file__))
  14. self.ledger = os.path.abspath(args.ledger)
  15. self.sourcepath = os.path.abspath(args.file)
  16. self.verbose = args.verbose
  17. self.tests = args.examples
  18. self.examples = dict()
  19. self.test_files = list()
  20. self.testin_token = 'command'
  21. self.testout_token = 'output'
  22. self.testdat_token = 'input'
  23. self.testfile_token = 'file'
  24. self.validate_token = 'validate'
  25. self.validate_cmd_token = 'validate-command'
  26. self.validate_dat_token = 'validate-data'
  27. self.testwithdat_token = 'with_input'
  28. self.testwithfile_token = 'with_file'
  29. def read_example(self):
  30. endexample = re.compile(r'^@end\s+smallexample\s*$')
  31. example = str()
  32. while True:
  33. line = self.file.readline()
  34. self.current_line += 1
  35. if len(line) <= 0 or endexample.match(line): break
  36. # Replace special texinfo character sequences with their ASCII counterpart
  37. example += re.sub(r'@([@{}])', r'\1', line)
  38. return example
  39. def test_id(self, example):
  40. return hashlib.sha1(example.rstrip()).hexdigest()[0:7].upper()
  41. def find_examples(self):
  42. startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s|%s|%s)(?::([\dA-Fa-f]+|validate))?(?:,(.*))?'
  43. % (self.testin_token, self.testout_token, self.testdat_token, self.testfile_token))
  44. while True:
  45. line = self.file.readline()
  46. self.current_line += 1
  47. if len(line) <= 0: break
  48. startmatch = startexample.match(line)
  49. if (startmatch):
  50. test_begin_pos = self.file.tell()
  51. test_begin_line = self.current_line
  52. test_kind = startmatch.group(1)
  53. test_id = startmatch.group(2)
  54. test_options = dict()
  55. for pair in re.split(r',\s*', str(startmatch.group(3))):
  56. kv = re.split(r':\s*', pair, 2)
  57. try:
  58. test_options[kv[0]] = kv[1]
  59. except IndexError:
  60. pass
  61. example = self.read_example()
  62. test_end_pos = self.file.tell()
  63. test_end_line = self.current_line
  64. if not test_id:
  65. print >> sys.stderr, 'Example', test_kind, 'in line', test_begin_line, 'is missing id.'
  66. test_id = self.test_id(example)
  67. if test_kind == self.testin_token:
  68. print >> sys.stderr, 'Use', self.test_id(example)
  69. elif test_kind == self.testin_token and test_id != self.validate_token and test_id != self.test_id(example):
  70. print >> sys.stderr, 'Expected test id', test_id, 'for example' \
  71. , test_kind, 'on line', test_begin_line, 'to be', self.test_id(example)
  72. if test_id == self.validate_token:
  73. test_id = "Val-" + str(test_begin_line)
  74. if test_kind == self.testin_token:
  75. test_kind = self.validate_cmd_token
  76. elif test_kind == self.testdat_token:
  77. test_kind = self.validate_dat_token
  78. try:
  79. self.examples[test_id]
  80. except KeyError:
  81. self.examples[test_id] = dict()
  82. try:
  83. example = self.examples[test_id][test_kind][test_kind] + example
  84. except KeyError:
  85. pass
  86. self.examples[test_id][test_kind] = {
  87. 'bpos': test_begin_pos,
  88. 'epos': test_end_pos,
  89. 'blin': test_begin_line,
  90. 'elin': test_end_line,
  91. 'opts': test_options,
  92. test_kind: example,
  93. }
  94. def parse_command(self, test_id, example):
  95. validate_command = False
  96. try:
  97. command = example[self.testin_token][self.testin_token]
  98. command = re.sub(r'\\\n', '', command)
  99. except KeyError:
  100. if self.validate_dat_token in example:
  101. command = '$ ledger bal'
  102. elif self.validate_cmd_token in example:
  103. validate_command = True
  104. command = example[self.validate_cmd_token][self.validate_cmd_token]
  105. else:
  106. return None
  107. command = filter(lambda x: x != '\n', shlex.split(command))
  108. if command[0] == '$': command.remove('$')
  109. index = command.index('ledger')
  110. command[index] = self.ledger
  111. for i,argument in enumerate(shlex.split('--args-only --columns 80')):
  112. command.insert(index+i+1, argument)
  113. try:
  114. findex = command.index('-f')
  115. except ValueError:
  116. try:
  117. findex = command.index('--file')
  118. except ValueError:
  119. findex = index+1
  120. command.insert(findex, '--file')
  121. if validate_command:
  122. command.insert(findex+1, 'sample.dat')
  123. else:
  124. command.insert(findex+1, test_id + '.dat')
  125. return (command, findex+1)
  126. def test_examples(self):
  127. failed = set()
  128. tests = self.examples.keys()
  129. if self.tests:
  130. tests = list(set(self.tests).intersection(tests))
  131. temp = list(set(self.tests).difference(tests))
  132. if len(temp) > 0:
  133. print >> sys.stderr, 'Skipping non-existent examples: %s' % ', '.join(temp)
  134. for test_id in tests:
  135. validation = False
  136. if self.validate_dat_token in self.examples[test_id] or self.validate_cmd_token in self.examples[test_id]:
  137. validation = True
  138. example = self.examples[test_id]
  139. try:
  140. (command, findex) = self.parse_command(test_id, example)
  141. except TypeError:
  142. failed.add(test_id)
  143. continue
  144. output = example.get(self.testout_token, {}).get(self.testout_token)
  145. input = example.get(self.testdat_token, {}).get(self.testdat_token)
  146. if not input:
  147. with_input = example.get(self.testin_token, {}).get('opts', {}).get(self.testwithdat_token)
  148. input = self.examples.get(with_input, {}).get(self.testdat_token, {}).get(self.testdat_token)
  149. if not input:
  150. input = example.get(self.validate_dat_token, {}).get(self.validate_dat_token)
  151. if command and (output != None or validation):
  152. test_file_created = False
  153. if findex:
  154. scriptpath = os.path.dirname(os.path.realpath(__file__))
  155. test_input_dir = os.path.join(scriptpath, '..', 'test', 'input')
  156. test_file = command[findex]
  157. if not os.path.exists(test_file):
  158. if input:
  159. test_file_created = True
  160. with open(test_file, 'w') as f:
  161. f.write(input)
  162. elif os.path.exists(os.path.join(test_input_dir, test_file)):
  163. command[findex] = os.path.join(test_input_dir, test_file)
  164. try:
  165. convert_idx = command.index('convert')
  166. convert_file = command[convert_idx+1]
  167. convert_data = example[self.testfile_token][self.testfile_token]
  168. if not os.path.exists(convert_file):
  169. with open(convert_file, 'w') as f:
  170. f.write(convert_data)
  171. except ValueError:
  172. pass
  173. error = None
  174. try:
  175. verify = subprocess.check_output(command, stderr=subprocess.STDOUT)
  176. valid = (output == verify) or (not error and validation)
  177. except subprocess.CalledProcessError, e:
  178. error = e.output
  179. valid = False
  180. failed.add(test_id)
  181. if valid and test_file_created:
  182. os.remove(test_file)
  183. if self.verbose > 0:
  184. print test_id, ':', 'Passed' if valid else 'FAILED: {}'.format(error) if error else 'FAILED'
  185. else:
  186. sys.stdout.write('.' if valid else 'E')
  187. if not (valid or error):
  188. failed.add(test_id)
  189. if self.verbose > 1:
  190. print ' '.join(command)
  191. if not validation:
  192. for line in unified_diff(output.split('\n'), verify.split('\n'), fromfile='generated', tofile='expected'):
  193. print(line)
  194. print
  195. else:
  196. if self.verbose > 0:
  197. print test_id, ':', 'Skipped'
  198. else:
  199. sys.stdout.write('X')
  200. if not self.verbose:
  201. print
  202. if len(failed) > 0:
  203. print "\nThe following examples failed:"
  204. print " ", "\n ".join(failed)
  205. return len(failed)
  206. def main(self):
  207. self.file = open(self.sourcepath)
  208. self.current_line = 0
  209. self.find_examples()
  210. failed_examples = self.test_examples()
  211. self.file.close()
  212. return failed_examples
  213. if __name__ == "__main__":
  214. def getargs():
  215. parser = argparse.ArgumentParser(prog='DocTests',
  216. description='Test and validate ledger examples from the texinfo manual')
  217. parser.add_argument('-v', '--verbose',
  218. dest='verbose',
  219. action='count',
  220. help='be verbose. Add -vv for more verbosity')
  221. parser.add_argument('-l', '--ledger',
  222. dest='ledger',
  223. type=str,
  224. action='store',
  225. required=True,
  226. help='the path to the ledger executable to test with')
  227. parser.add_argument('-f', '--file',
  228. dest='file',
  229. type=str,
  230. action='store',
  231. required=True,
  232. help='the texinfo documentation file to run the examples from')
  233. parser.add_argument('examples',
  234. metavar='EXAMPLE',
  235. type=str,
  236. nargs='*',
  237. help='the examples to test')
  238. return parser.parse_args()
  239. args = getargs()
  240. script = DocTests(args)
  241. status = script.main()
  242. sys.exit(status)