#!/usr/bin/env python3
#
# man-test.py - test exapmles in a man page
#
# Copyright (C) 2021 Masatake YAMATO
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
#
# Python 3.5 or later is required.
# On Windows, unix-like shell (e.g. bash) and diff command are needed.
#
import sys
import re
import os
import subprocess
import copy
def print_usage(n, f):
print ('Usage: man-test.py TMPDIR CTAGS ctags-lang-.7.rst.in...', file=f)
sys.exit(n)
def next_segment(line):
if line.endswith ('\\'):
return line[0:-1]
else:
return (line + '\n')
def wash_cmdline(cmdline):
return cmdline
def verify_test_case(t):
prefix = '%(man_file)s[%(nth)d]:%(start_linum)d: '%t
msg = False
if not 'code' in t:
msg = 'cannot find input lines'
elif not 'tags' in t:
msg = 'cannot find expected tags output'
elif not 'cmdline' in t:
msg = 'cannot find ctags command line'
if msg:
msg = prefix + msg
return msg
def is_option(c):
if re.search('--[a-z_].*', c):
return True
elif re.search('^-[a-z]$', c):
return True
return False
def run_test_case(tmpdir, ctags, t):
d = tmpdir + '/' + str(os.getpid())
os.makedirs (d,exist_ok=True)
i = d + '/' + t['input_file_name']
o0 = 'actual.tags'
o = d + '/' + o0
e0 = 'expected.tags'
e = d + '/' + e0
D = d + '/' + 'tags.diff'
O0 = 'args.ctags'
O = d + '/' + O0
with open(i, mode='w', encoding='utf-8') as f:
f.write(t['code'])
with open(e, mode='w', encoding='utf-8') as g:
g.write(t['tags'])
inputf=None
with open(O, mode='w', encoding='utf-8') as Of:
in_pattern = False
for c in t['cmdline'].split():
if c == '--options=NONE':
continue
elif c.startswith('input.'):
inputf = c
continue
elif c.startswith('--regex-'):
in_pattern = c
elif in_pattern and not is_option(c):
# TODO: This doesn't work if whitespace is repeated.
in_pattern = in_pattern + ' ' + c
else:
if in_pattern:
print (in_pattern, file=Of)
in_pattern = False
print (c, file=Of)
if in_pattern:
print (in_pattern, file=Of)
with open(o, mode='w', encoding='utf-8') as h:
cmdline = [ctags, '--quiet', '--options=NONE',
'--options=' + O0, inputf]
subprocess.run(cmdline, cwd=d, stdout=h)
with open(D, mode='w', encoding='utf-8') as diff:
r = subprocess.run(['diff', '-uN', '--strip-trailing-cr', o0, e0],
cwd=d, stdout=diff).returncode
if r == 0:
t['result'] = True
t['result_readable'] = 'passed'
else:
with open(o, encoding='utf-8') as f:
t['actual_tags'] = f.read()
t['result'] = False
t['result_readable'] = 'failed'
with open(D, encoding='utf-8') as diff:
t['tags_diff'] = diff.read()
os.remove(O)
os.remove(i)
os.remove(e)
os.remove(o)
os.remove(D)
os.rmdir(d)
return t
def report_result(r):
print ('%(man_file)s[%(nth)d]:%(start_linum)d...%(result_readable)s'%r)
def report_failure(r):
print ('## %(man_file)s[%(nth)d]:%(start_linum)d'%r)
print ('### input')
print ('```')
print (r['code'])
print ('```')
print ('### cmdline')
print ('```')
print (r['cmdline'])
print ('```')
print ('### expected tags')
print ('```')
print (r['tags'])
print ('```')
print ('### actual tags')
print ('```')
print (r['actual_tags'])
print ('```')
print ('### diff of tag files')
print ('```')
print (r['tags_diff'])
print ('```')
class state:
start = 0
tags = 1
code = 2
code_done = 3
input = 4
output = 5
output_after_options = 6
def extract_test_cases(f):
linum=0
nth=0
s=state.start
test_spec = {}
for line in f.readlines():
linum += 1
line = line.rstrip('\r\n')
if s == state.tags or s == state.code:
if prefix:
m = re.search('^' + prefix + '(.*)$', line)
if m:
sink += next_segment(m.group(1))
continue
if line == '':
sink += '\n'
continue
else:
m = re.search('^([ \t]+)(.+)$', line)
if m:
prefix = m.group(1)
sink += next_segment(m.group(2))
continue
elif re.search ('^([ \t]*)$', line):
continue
sink = sink.rstrip('\r\n') + '\n'
if s == state.code:
test_spec['code'] = sink
s = state.code_done
else:
test_spec['tags'] = sink
test_spec['nth'] = nth
nth += 1
test_spec['end_linum'] = linum
s = state.start
yield test_spec
m = s == state.start and re.search ('^"(input\.[^"]+)"$', line)
if m:
test_spec ['start_linum'] = linum
test_spec ['input_file_name'] = m.group(1)
s = state.input
continue
m = s == state.input and re.search ('^.. code-block::.*', line)
if m:
sink = ''
prefix = False
s = state.code
continue
m = s == state.code_done and re.search ('^"output.tags"$', line)
if m:
s = state.output
continue
m = s == state.output and re.search ('with[ \t]"([^"]+)"', line)
if m:
test_spec ['cmdline'] = wash_cmdline (m.group(1))
s = state.output_after_options
continue
if s == state.output_after_options \
and (line == "::" or re.search ('^.. code-block:: *tags$', line)):
sink = ''
prefix = False
s = state.tags
continue
def man_test (tmpdir, ctags, man_file):
failures = []
result = True
print ('# Run test cases in ' + man_file)
print ('```')
with open(man_file, encoding='utf-8') as f:
for t in extract_test_cases (f):
t['man_file'] = man_file
v = verify_test_case (t)
if v:
print ("error: " + v, file=sys.stderr)
result = False
continue
r = run_test_case (tmpdir, ctags, t)
report_result (r)
if not r['result']:
result = False
failures.append(copy.copy(r))
print ('```')
if (len(failures) > 0):
print ('# Failed test case(s)')
for f in failures:
report_failure(f)
return result
def man_tests (tmpdir, ctags, man_files):
result = 0
for m in man_files:
if not man_test(tmpdir, ctags, m):
result += 1
print ('OK' if result == 0 else 'FAILED')
return result == 0
if (len(sys.argv) < 4) or (sys.argv[1] == '-h' or sys.argv[1] == '--help'):
print_usage (2, sys.stderr)
tmpdir = sys.argv[1]
ctags = os.path.abspath(sys.argv[2])
sys.exit(0 if man_tests (tmpdir, ctags, sys.argv[3:]) else 1)