xref: /Universal-ctags/misc/man-test.py (revision f89abf0ea10438b9114d155f52cb6a9160228eb4)
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