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