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