1#!/usr/bin/env python3 2 3# 4# man-test.py - test exapmles in a man page 5# 6# Copyright (C) 2021 Masatake YAMATO 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program. If not, see <http://www.gnu.org/licenses/>. 20# 21 22# 23# Python 3.5 or later is required. 24# On Windows, unix-like shell (e.g. bash) and diff command are needed. 25# 26 27import sys 28import re 29import os 30import subprocess 31import copy 32 33def print_usage(n, f): 34 print ('Usage: man-test.py TMPDIR CTAGS ctags-lang-<LANG>.7.rst.in...', file=f) 35 sys.exit(n) 36 37def next_segment(line): 38 if line.endswith ('\\'): 39 return line[0:-1] 40 else: 41 return (line + '\n') 42 43def wash_cmdline(cmdline): 44 return cmdline 45 46def verify_test_case(t): 47 prefix = '%(man_file)s[%(nth)d]:%(start_linum)d: '%t 48 msg = False 49 if not 'code' in t: 50 msg = 'cannot find input lines' 51 elif not 'tags' in t: 52 msg = 'cannot find expected tags output' 53 elif not 'cmdline' in t: 54 msg = 'cannot find ctags command line' 55 56 if msg: 57 msg = prefix + msg 58 return msg 59 60def is_option(c): 61 if re.search('--[a-z_].*', c): 62 return True 63 elif re.search('^-[a-z]$', c): 64 return True 65 return False 66 67def run_test_case(tmpdir, ctags, t): 68 d = tmpdir + '/' + str(os.getpid()) 69 os.makedirs (d,exist_ok=True) 70 i = d + '/' + t['input_file_name'] 71 o0 = 'actual.tags' 72 o = d + '/' + o0 73 e0 = 'expected.tags' 74 e = d + '/' + e0 75 D = d + '/' + 'tags.diff' 76 O0 = 'args.ctags' 77 O = d + '/' + O0 78 with open(i, mode='w', encoding='utf-8') as f: 79 f.write(t['code']) 80 81 with open(e, mode='w', encoding='utf-8') as g: 82 g.write(t['tags']) 83 84 inputf=None 85 with open(O, mode='w', encoding='utf-8') as Of: 86 in_pattern = False 87 for c in t['cmdline'].split(): 88 if c == '--options=NONE': 89 continue 90 elif c.startswith('input.'): 91 inputf = c 92 continue 93 elif c.startswith('--regex-'): 94 in_pattern = c 95 elif in_pattern and not is_option(c): 96 # TODO: This doesn't work if whitespace is repeated. 97 in_pattern = in_pattern + ' ' + c 98 else: 99 if in_pattern: 100 print (in_pattern, file=Of) 101 in_pattern = False 102 print (c, file=Of) 103 if in_pattern: 104 print (in_pattern, file=Of) 105 106 with open(o, mode='w', encoding='utf-8') as h: 107 cmdline = [ctags, '--quiet', '--options=NONE', 108 '--options=' + O0, inputf] 109 subprocess.run(cmdline, cwd=d, stdout=h) 110 111 with open(D, mode='w', encoding='utf-8') as diff: 112 r = subprocess.run(['diff', '-uN', '--strip-trailing-cr', o0, e0], 113 cwd=d, stdout=diff).returncode 114 115 if r == 0: 116 t['result'] = True 117 t['result_readable'] = 'passed' 118 else: 119 with open(o, encoding='utf-8') as f: 120 t['actual_tags'] = f.read() 121 t['result'] = False 122 t['result_readable'] = 'failed' 123 with open(D, encoding='utf-8') as diff: 124 t['tags_diff'] = diff.read() 125 os.remove(O) 126 os.remove(i) 127 os.remove(e) 128 os.remove(o) 129 os.remove(D) 130 os.rmdir(d) 131 return t 132 133def report_result(r): 134 print ('%(man_file)s[%(nth)d]:%(start_linum)d...%(result_readable)s'%r) 135 136def report_failure(r): 137 print ('## %(man_file)s[%(nth)d]:%(start_linum)d'%r) 138 print ('### input') 139 print ('```') 140 print (r['code']) 141 print ('```') 142 print ('### cmdline') 143 print ('```') 144 print (r['cmdline']) 145 print ('```') 146 print ('### expected tags') 147 print ('```') 148 print (r['tags']) 149 print ('```') 150 print ('### actual tags') 151 print ('```') 152 print (r['actual_tags']) 153 print ('```') 154 print ('### diff of tag files') 155 print ('```') 156 print (r['tags_diff']) 157 print ('```') 158 159class state: 160 start = 0 161 tags = 1 162 code = 2 163 code_done = 3 164 input = 4 165 output = 5 166 output_after_options = 6 167 168def extract_test_cases(f): 169 linum=0 170 nth=0 171 s=state.start 172 test_spec = {} 173 174 for line in f.readlines(): 175 linum += 1 176 line = line.rstrip('\r\n') 177 178 if s == state.tags or s == state.code: 179 if prefix: 180 m = re.search('^' + prefix + '(.*)$', line) 181 if m: 182 sink += next_segment(m.group(1)) 183 continue 184 if line == '': 185 sink += '\n' 186 continue 187 else: 188 m = re.search('^([ \t]+)(.+)$', line) 189 if m: 190 prefix = m.group(1) 191 sink += next_segment(m.group(2)) 192 continue 193 elif re.search ('^([ \t]*)$', line): 194 continue 195 196 sink = sink.rstrip('\r\n') + '\n' 197 198 if s == state.code: 199 test_spec['code'] = sink 200 s = state.code_done 201 else: 202 test_spec['tags'] = sink 203 test_spec['nth'] = nth 204 nth += 1 205 test_spec['end_linum'] = linum 206 s = state.start 207 yield test_spec 208 209 m = s == state.start and re.search ('^"(input\.[^"]+)"$', line) 210 if m: 211 test_spec ['start_linum'] = linum 212 test_spec ['input_file_name'] = m.group(1) 213 s = state.input 214 continue 215 m = s == state.input and re.search ('^.. code-block::.*', line) 216 if m: 217 sink = '' 218 prefix = False 219 s = state.code 220 continue 221 m = s == state.code_done and re.search ('^"output.tags"$', line) 222 if m: 223 s = state.output 224 continue 225 m = s == state.output and re.search ('with[ \t]"([^"]+)"', line) 226 if m: 227 test_spec ['cmdline'] = wash_cmdline (m.group(1)) 228 s = state.output_after_options 229 continue 230 if s == state.output_after_options \ 231 and (line == "::" or re.search ('^.. code-block:: *tags$', line)): 232 sink = '' 233 prefix = False 234 s = state.tags 235 continue 236 237def man_test (tmpdir, ctags, man_file): 238 failures = [] 239 result = True 240 print ('# Run test cases in ' + man_file) 241 print ('```') 242 with open(man_file, encoding='utf-8') as f: 243 for t in extract_test_cases (f): 244 t['man_file'] = man_file 245 v = verify_test_case (t) 246 if v: 247 print ("error: " + v, file=sys.stderr) 248 result = False 249 continue 250 r = run_test_case (tmpdir, ctags, t) 251 report_result (r) 252 if not r['result']: 253 result = False 254 failures.append(copy.copy(r)) 255 print ('```') 256 if (len(failures) > 0): 257 print ('# Failed test case(s)') 258 for f in failures: 259 report_failure(f) 260 return result 261 262def man_tests (tmpdir, ctags, man_files): 263 result = 0 264 for m in man_files: 265 if not man_test(tmpdir, ctags, m): 266 result += 1 267 print ('OK' if result == 0 else 'FAILED') 268 return result == 0 269 270if (len(sys.argv) < 4) or (sys.argv[1] == '-h' or sys.argv[1] == '--help'): 271 print_usage (2, sys.stderr) 272 273tmpdir = sys.argv[1] 274ctags = os.path.abspath(sys.argv[2]) 275sys.exit(0 if man_tests (tmpdir, ctags, sys.argv[3:]) else 1) 276