xref: /Universal-ctags/misc/units.py (revision a91961f00377fe8f36a7d3900034048e3250e2bd)
1d58ff7ccSK.Takata#!/usr/bin/env python3
2d58ff7ccSK.Takata
3646e1eb7SK.Takata#
4646e1eb7SK.Takata# units.py - Units test harness for ctags
5646e1eb7SK.Takata#
6646e1eb7SK.Takata# Copyright (C) 2019 Ken Takata
7646e1eb7SK.Takata# (Based on "units" written by Masatake YAMATO.)
8646e1eb7SK.Takata#
9646e1eb7SK.Takata# This program is free software; you can redistribute it and/or modify
10646e1eb7SK.Takata# it under the terms of the GNU General Public License as published by
11646e1eb7SK.Takata# the Free Software Foundation; either version 2 of the License, or
12646e1eb7SK.Takata# (at your option) any later version.
13646e1eb7SK.Takata#
14646e1eb7SK.Takata# This program is distributed in the hope that it will be useful,
15646e1eb7SK.Takata# but WITHOUT ANY WARRANTY; without even the implied warranty of
16646e1eb7SK.Takata# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17646e1eb7SK.Takata# GNU General Public License for more details.
18646e1eb7SK.Takata#
19646e1eb7SK.Takata# You should have received a copy of the GNU General Public License
20646e1eb7SK.Takata# along with this program.  If not, see <http://www.gnu.org/licenses/>.
21646e1eb7SK.Takata#
22646e1eb7SK.Takata
23646e1eb7SK.Takata#
24646e1eb7SK.Takata# Python 3.5 or later is required.
253e288efeSK.Takata# On Windows, unix-like shell (e.g. bash) and some unix tools (sed,
263e288efeSK.Takata# diff, etc.) are needed.
27646e1eb7SK.Takata#
28646e1eb7SK.Takata
29d58ff7ccSK.Takataimport time     # for debugging
30d58ff7ccSK.Takataimport argparse
31d58ff7ccSK.Takataimport filecmp
32d58ff7ccSK.Takataimport glob
33646e1eb7SK.Takataimport io
34d58ff7ccSK.Takataimport os
35d58ff7ccSK.Takataimport platform
36d58ff7ccSK.Takataimport queue
37d58ff7ccSK.Takataimport re
38d58ff7ccSK.Takataimport shutil
3928cf668aSK.Takataimport stat
40d58ff7ccSK.Takataimport subprocess
41d58ff7ccSK.Takataimport sys
42d58ff7ccSK.Takataimport threading
43d58ff7ccSK.Takata
44646e1eb7SK.Takata#
45646e1eb7SK.Takata# Global Parameters
46646e1eb7SK.Takata#
47646e1eb7SK.TakataSHELL = '/bin/sh'
48d58ff7ccSK.TakataCTAGS = './ctags'
49646e1eb7SK.TakataREADTAGS = './readtags'
50d8f6b0edSMasatake YAMATOOPTSCRIPT = './optscript'
51d58ff7ccSK.TakataWITH_TIMEOUT = 0
52d58ff7ccSK.TakataWITH_VALGRIND = False
53d58ff7ccSK.TakataCOLORIZED_OUTPUT = True
54d58ff7ccSK.TakataCATEGORIES = []
55d58ff7ccSK.TakataUNITS = []
56d58ff7ccSK.TakataLANGUAGES = []
57d58ff7ccSK.TakataPRETENSE_OPTS = ''
58d58ff7ccSK.TakataRUN_SHRINK = False
59d58ff7ccSK.TakataSHOW_DIFF_OUTPUT = False
60646e1eb7SK.TakataNUM_WORKER_THREADS = 4
61ad8e6505SMasatake YAMATODIFF_U_NUM = 0
62d58ff7ccSK.Takata
63646e1eb7SK.Takata#
64646e1eb7SK.Takata# Internal variables and constants
65646e1eb7SK.Takata#
66d58ff7ccSK.Takata_FEATURE_LIST = []
67d58ff7ccSK.Takata_PREPERE_ENV = ''
68d58ff7ccSK.Takata_DEFAULT_CATEGORY = 'ROOT'
69d58ff7ccSK.Takata_TIMEOUT_EXIT = 124
70d58ff7ccSK.Takata_VG_TIMEOUT_FACTOR = 10
71d58ff7ccSK.Takata_VALGRIND_EXIT = 58
72d58ff7ccSK.Takata_STDERR_OUTPUT_NAME = 'STDERR.tmp'
73d58ff7ccSK.Takata_DIFF_OUTPUT_NAME = 'DIFF.tmp'
74d892ff97SMasatake YAMATO_VALGRIND_OUTPUT_NAME = 'VALGRIND.tmp'
75d58ff7ccSK.Takata
76d58ff7ccSK.Takata#
77d58ff7ccSK.Takata# Results
78d58ff7ccSK.Takata#
79d58ff7ccSK.TakataL_PASSED = []
80d58ff7ccSK.TakataL_FIXED = []
81d58ff7ccSK.TakataL_FAILED_BY_STATUS = []
82d58ff7ccSK.TakataL_FAILED_BY_DIFF = []
83d58ff7ccSK.TakataL_SKIPPED_BY_FEATURES = []
84d58ff7ccSK.TakataL_SKIPPED_BY_LANGUAGES = []
85d58ff7ccSK.TakataL_SKIPPED_BY_ILOOP = []
86d58ff7ccSK.TakataL_KNOWN_BUGS = []
87d58ff7ccSK.TakataL_FAILED_BY_TIMEED_OUT = []
88d58ff7ccSK.TakataL_BROKEN_ARGS_CTAGS = []
89d58ff7ccSK.TakataL_VALGRIND = []
90646e1eb7SK.TakataTMAIN_STATUS = True
91646e1eb7SK.TakataTMAIN_FAILED = []
92d58ff7ccSK.Takata
93d58ff7ccSK.Takatadef remove_prefix(string, prefix):
94d58ff7ccSK.Takata    if string.startswith(prefix):
95d58ff7ccSK.Takata        return string[len(prefix):]
96d58ff7ccSK.Takata    else:
97d58ff7ccSK.Takata        return string
98d58ff7ccSK.Takata
99d58ff7ccSK.Takatadef is_cygwin():
100d58ff7ccSK.Takata    system = platform.system()
101d58ff7ccSK.Takata    return system.startswith('CYGWIN_NT') or system.startswith('MINGW32_NT')
102d58ff7ccSK.Takata
103d58ff7ccSK.Takatadef isabs(path):
104d58ff7ccSK.Takata    if is_cygwin():
105d58ff7ccSK.Takata        import ntpath
106d58ff7ccSK.Takata        if ntpath.isabs(path):
107d58ff7ccSK.Takata            return True
108d58ff7ccSK.Takata    return os.path.isabs(path)
109d58ff7ccSK.Takata
110099a0013SK.Takatadef action_help(parser, action, *args):
111099a0013SK.Takata    parser.print_help()
112099a0013SK.Takata    return 0
113099a0013SK.Takata
114099a0013SK.Takatadef error_exit(status, msg):
115099a0013SK.Takata    print(msg, file=sys.stderr)
116099a0013SK.Takata    sys.exit(status)
117099a0013SK.Takata
118d58ff7ccSK.Takatadef line(*args, file=sys.stdout):
119d58ff7ccSK.Takata    if len(args) > 0:
120d58ff7ccSK.Takata        ch = args[0]
121d58ff7ccSK.Takata    else:
122d58ff7ccSK.Takata        ch = '-'
123d58ff7ccSK.Takata    print(ch * 60, file=file)
124d58ff7ccSK.Takata
12528cf668aSK.Takatadef remove_readonly(func, path, _):
12628cf668aSK.Takata    # Clear the readonly bit and reattempt the removal
12728cf668aSK.Takata    os.chmod(path, stat.S_IWRITE | stat.S_IREAD)
12828cf668aSK.Takata    dname = os.path.dirname(path)
12928cf668aSK.Takata    os.chmod(dname, os.stat(dname).st_mode | stat.S_IWRITE)
13028cf668aSK.Takata    func(path)
13128cf668aSK.Takata
132099a0013SK.Takatadef clean_bundles(bundles):
133099a0013SK.Takata    if not os.path.isfile(bundles):
134099a0013SK.Takata        return
135d58ff7ccSK.Takata    with open(bundles, 'r') as f:
136099a0013SK.Takata        for fn in f.read().splitlines():
137d58ff7ccSK.Takata            if os.path.isdir(fn):
13828cf668aSK.Takata                shutil.rmtree(fn, onerror=remove_readonly)
139d58ff7ccSK.Takata            elif os.path.isfile(fn):
140d58ff7ccSK.Takata                os.remove(fn)
141d58ff7ccSK.Takata    os.remove(bundles)
142099a0013SK.Takata
143099a0013SK.Takatadef clean_tcase(d, bundles):
144099a0013SK.Takata    if os.path.isdir(d):
145099a0013SK.Takata        clean_bundles(bundles)
146099a0013SK.Takata        for fn in glob.glob(d + '/*.tmp'):
147099a0013SK.Takata            os.remove(fn)
148099a0013SK.Takata        for fn in glob.glob(d + '/*.TMP'):
149d58ff7ccSK.Takata            os.remove(fn)
150d58ff7ccSK.Takata
151d58ff7ccSK.Takatadef check_availability(cmd):
152d58ff7ccSK.Takata    if not shutil.which(cmd):
153d58ff7ccSK.Takata        error_exit(1, cmd + ' command is not available')
154d58ff7ccSK.Takata
155d58ff7ccSK.Takatadef check_units(name, category):
156d58ff7ccSK.Takata    if len(UNITS) == 0:
157d58ff7ccSK.Takata        return True
158d58ff7ccSK.Takata
159d58ff7ccSK.Takata    for u in UNITS:
160d58ff7ccSK.Takata        ret = re.match(r'(.+)/(.+)', u)
161d58ff7ccSK.Takata        if ret:
162d58ff7ccSK.Takata            if ret.group(1, 2) == (category, name):
163d58ff7ccSK.Takata                return True
164d58ff7ccSK.Takata        elif u == name:
165d58ff7ccSK.Takata            return True
166d58ff7ccSK.Takata    return False
167d58ff7ccSK.Takata
168d58ff7ccSK.Takatadef init_features():
169d58ff7ccSK.Takata    global _FEATURE_LIST
1703e288efeSK.Takata    ret = subprocess.run([CTAGS, '--quiet', '--options=NONE', '--list-features', '--with-list=no'],
1713e288efeSK.Takata            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
172d58ff7ccSK.Takata    _FEATURE_LIST = re.sub(r'(?m)^([^ ]+).*$', r'\1',
173d58ff7ccSK.Takata            ret.stdout.decode('utf-8')).splitlines()
174d58ff7ccSK.Takata
175d58ff7ccSK.Takatadef check_features(feature, ffile):
176d58ff7ccSK.Takata    features = []
177d58ff7ccSK.Takata    if feature:
178d58ff7ccSK.Takata        features = [feature]
179d58ff7ccSK.Takata    elif os.path.isfile(ffile):
180d58ff7ccSK.Takata        with open(ffile, 'r') as f:
181d58ff7ccSK.Takata            features = f.read().splitlines()
182d58ff7ccSK.Takata
183d58ff7ccSK.Takata    for expected in features:
184d58ff7ccSK.Takata        if expected == '':
185d58ff7ccSK.Takata            continue
186d58ff7ccSK.Takata        found = False
187d58ff7ccSK.Takata        found_unexpectedly = False
188d58ff7ccSK.Takata        if expected[0] == '!':
189d58ff7ccSK.Takata            if expected[1:] in _FEATURE_LIST:
190d58ff7ccSK.Takata                found_unexpectedly = True
191d58ff7ccSK.Takata        else:
192d58ff7ccSK.Takata            if expected in _FEATURE_LIST:
193d58ff7ccSK.Takata                found = True
194d58ff7ccSK.Takata        if found_unexpectedly:
195d58ff7ccSK.Takata            return (False, expected)
196d58ff7ccSK.Takata        elif not found:
197d58ff7ccSK.Takata            return (False, expected)
198d58ff7ccSK.Takata    return (True, '')
199d58ff7ccSK.Takata
200d58ff7ccSK.Takatadef check_languages(cmdline, lfile):
201d58ff7ccSK.Takata    if not os.path.isfile(lfile):
202d58ff7ccSK.Takata        return (True, '')
203d58ff7ccSK.Takata
2043e288efeSK.Takata    ret = subprocess.run(cmdline + ['--list-languages'],
2053e288efeSK.Takata            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
206d58ff7ccSK.Takata    langs = ret.stdout.decode('utf-8').splitlines()
207d58ff7ccSK.Takata
208d58ff7ccSK.Takata    with open(lfile, 'r') as f:
209d58ff7ccSK.Takata        for expected in f.read().splitlines():
210d58ff7ccSK.Takata            found = False
211d58ff7ccSK.Takata            if expected in langs:
212d58ff7ccSK.Takata                found = True
213d58ff7ccSK.Takata            if not found:
214d58ff7ccSK.Takata                return (False, expected)
215d58ff7ccSK.Takata    return (True, '')
216d58ff7ccSK.Takata
217d58ff7ccSK.Takatadef decorate(decorator, msg, colorized):
218d58ff7ccSK.Takata    if decorator == 'red':
219d58ff7ccSK.Takata        num = '31'
220d58ff7ccSK.Takata    elif decorator == 'green':
221d58ff7ccSK.Takata        num = '32'
222d58ff7ccSK.Takata    elif decorator == 'yellow':
223d58ff7ccSK.Takata        num = '33'
224d58ff7ccSK.Takata    else:
225099a0013SK.Takata        error_exit(1, 'INTERNAL ERROR: wrong run_result function')
226d58ff7ccSK.Takata
227d58ff7ccSK.Takata    if colorized:
228d58ff7ccSK.Takata        return "\x1b[" + num + 'm' + msg + "\x1b[m"
229d58ff7ccSK.Takata    else:
230d58ff7ccSK.Takata        return msg
231d58ff7ccSK.Takata
232d58ff7ccSK.Takatadef run_result(result_type, msg, output, *args, file=sys.stdout):
233d58ff7ccSK.Takata    func_dict = {
234d58ff7ccSK.Takata            'skip': run_result_skip,
235d58ff7ccSK.Takata            'error': run_result_error,
236d58ff7ccSK.Takata            'ok': run_result_ok,
237d58ff7ccSK.Takata            'known_error': run_result_known_error,
238d58ff7ccSK.Takata            }
239d58ff7ccSK.Takata
240d58ff7ccSK.Takata    func_dict[result_type](msg, file, COLORIZED_OUTPUT, *args)
241d58ff7ccSK.Takata    file.flush()
242d58ff7ccSK.Takata    if output:
243d58ff7ccSK.Takata        with open(output, 'w') as f:
244d58ff7ccSK.Takata            func_dict[result_type](msg, f, False, *args)
245d58ff7ccSK.Takata
246d58ff7ccSK.Takatadef run_result_skip(msg, f, colorized, *args):
247cf2d55c4SK.Takata    s = msg + decorate('yellow', 'skipped', colorized)
248d58ff7ccSK.Takata    if len(args) > 0:
249cf2d55c4SK.Takata        s += ' (' + args[0] + ')'
250cf2d55c4SK.Takata    print(s, file=f)
251d58ff7ccSK.Takata
252d58ff7ccSK.Takatadef run_result_error(msg, f, colorized, *args):
253cf2d55c4SK.Takata    s = msg + decorate('red', 'failed', colorized)
254d58ff7ccSK.Takata    if len(args) > 0:
255cf2d55c4SK.Takata        s += ' (' + args[0] + ')'
256cf2d55c4SK.Takata    print(s, file=f)
257d58ff7ccSK.Takata
258d58ff7ccSK.Takatadef run_result_ok(msg, f, colorized, *args):
259cf2d55c4SK.Takata    s = msg + decorate('green', 'passed', colorized)
260d58ff7ccSK.Takata    if len(args) > 0:
261cf2d55c4SK.Takata        s += ' (' + args[0] + ')'
262cf2d55c4SK.Takata    print(s, file=f)
263d58ff7ccSK.Takata
264d58ff7ccSK.Takatadef run_result_known_error(msg, f, colorized, *args):
265cf2d55c4SK.Takata    s = msg + decorate('yellow', 'failed', colorized) + ' (KNOWN bug)'
266cf2d55c4SK.Takata    print(s, file=f)
267d58ff7ccSK.Takata
268d58ff7ccSK.Takatadef run_shrink(cmdline_template, finput, foutput, lang):
269ec80aeceSK.Takata    script = sys.argv[0]
270ec80aeceSK.Takata    script = os.path.splitext(script)[0]   # remove '.py'
271ec80aeceSK.Takata
272ec80aeceSK.Takata    print('Shrinking ' + finput + ' as ' + lang)
273ec80aeceSK.Takata    # fallback to the shell script version
2746ab7d727SAlberto Fanjul    subprocess.run([SHELL, script, 'shrink',
275ec80aeceSK.Takata        '--timeout=1', '--foreground',
276ec80aeceSK.Takata        cmdline_template, finput, foutput])
277d58ff7ccSK.Takata
2783e288efeSK.Takata# return a filter for normalizing the basename
2793e288efeSK.Takata#
2803e288efeSK.Takata# If internal is True, return a pair of [pattern, replacement],
2813e288efeSK.Takata# otherwise return a list of command line arguments.
282d58ff7ccSK.Takatadef basename_filter(internal, output_type):
2833e288efeSK.Takata    filters_external = {
2843e288efeSK.Takata            'ctags': 's%\(^[^\t]\{1,\}\t\)\(/\{0,1\}\([^/\t]\{1,\}/\)*\)%\\1%',
28565349126SMasatake YAMATO            # "input" in the expresion is for finding input file names in the TAGS file.
28665349126SMasatake YAMATO            # RAWOUT.tmp:
28765349126SMasatake YAMATO            #
28865349126SMasatake YAMATO            #   ./Units/parser-ada.r/ada-etags-suffix.d/input_0.adb,238
28965349126SMasatake YAMATO            #   package body Input_0 is   ^?Input_0/b^A1,0
29065349126SMasatake YAMATO            #
29165349126SMasatake YAMATO            # With the original expression, both "./Units/parser-ada.r/ada-etags-suffix.d/"
29265349126SMasatake YAMATO            # and "package body Input_0 is   Input_0/' are deleted.
29365349126SMasatake YAMATO            # FILTERED.tmp:
29465349126SMasatake YAMATO            #
29565349126SMasatake YAMATO            # input_0.adb,238
29665349126SMasatake YAMATO            # b^A1,0
29765349126SMasatake YAMATO            #
29865349126SMasatake YAMATO            # Adding "input" ot the expression is for deleting only the former one and for
29965349126SMasatake YAMATO            # skpping the later one.
30065349126SMasatake YAMATO            #
30165349126SMasatake YAMATO            # FIXME: if "input" is included as a substring of tag entry names, filtering
30265349126SMasatake YAMATO            # with this expression makes the test fail.
30365349126SMasatake YAMATO            'etags': 's%.*\/\(input[-._][[:print:]]\{1,\}\),\([0-9]\{1,\}$\)%\\1,\\2%',
3043e288efeSK.Takata            'xref': 's%\(.*[[:digit:]]\{1,\} \)\([^ ]\{1,\}[^ ]\{1,\}\)/\([^ ].\{1,\}.\{1,\}$\)%\\1\\3%',
3053e288efeSK.Takata            'json': 's%\("path": \)"[^"]\{1,\}/\([^/"]\{1,\}\)"%\\1"\\2"%',
306d58ff7ccSK.Takata            }
307d58ff7ccSK.Takata    filters_internal = {
3083e288efeSK.Takata            'ctags': [r'(^[^\t]+\t)(/?([^/\t]+/)*)', r'\1'],
30965349126SMasatake YAMATO            # See above comments about "input".
31065349126SMasatake YAMATO            'etags': [r'.*/(input[-._]\S+),([0-9]+$)', r'\1,\2'],
3113e288efeSK.Takata            'xref': [r'(.*\d+ )([^ ]+[^ ]+)/([^ ].+.+$)', r'\1\3'],
3123e288efeSK.Takata            'json': [r'("path": )"[^"]+/([^/"]+)"', r'\1"\2"'],
313d58ff7ccSK.Takata            }
314d58ff7ccSK.Takata    if internal:
315d58ff7ccSK.Takata        return filters_internal[output_type]
316d58ff7ccSK.Takata    else:
3173e288efeSK.Takata        return ['sed', '-e', filters_external[output_type]]
3183e288efeSK.Takata
3193e288efeSK.Takata# convert a command line list to a command line string
3203e288efeSK.Takatadef join_cmdline(cmdline):
321113e31c0SK.Takata    # surround with '' if an argument includes spaces or '\'
3223e288efeSK.Takata    # TODO: use more robust way
323113e31c0SK.Takata    return ' '.join("'" + x + "'" if (' ' in x) or ('\\' in x) else x
324113e31c0SK.Takata        for x in cmdline)
325d58ff7ccSK.Takata
326d58ff7ccSK.Takatadef run_record_cmdline(cmdline, ffilter, ocmdline, output_type):
327d58ff7ccSK.Takata    with open(ocmdline, 'w') as f:
328d58ff7ccSK.Takata        print("%s\n%s \\\n| %s \\\n| %s\n" % (
3293e288efeSK.Takata            _PREPERE_ENV,
3303e288efeSK.Takata            join_cmdline(cmdline),
3313e288efeSK.Takata            join_cmdline(basename_filter(False, output_type)),
332d58ff7ccSK.Takata            ffilter), file=f)
333d58ff7ccSK.Takata
334d58ff7ccSK.Takatadef prepare_bundles(frm, to, obundles):
335d58ff7ccSK.Takata    for src in glob.glob(frm + '/*'):
336d58ff7ccSK.Takata        fn = os.path.basename(src)
337d58ff7ccSK.Takata        if fn.startswith('input.'):
338d58ff7ccSK.Takata            continue
339d58ff7ccSK.Takata        elif fn.startswith('expected.tags'):
340d58ff7ccSK.Takata            continue
341d58ff7ccSK.Takata        elif fn.startswith('README'):
342d58ff7ccSK.Takata            continue
343d58ff7ccSK.Takata        elif fn in ['features', 'languages', 'filters']:
344d58ff7ccSK.Takata            continue
345d58ff7ccSK.Takata        elif fn == 'args.ctags':
346d58ff7ccSK.Takata            continue
347d58ff7ccSK.Takata        else:
348d58ff7ccSK.Takata            dist = to + '/' + fn
349d58ff7ccSK.Takata            if os.path.isdir(src):
35028cf668aSK.Takata                shutil.copytree(src, dist, copy_function=shutil.copyfile)
351d58ff7ccSK.Takata            else:
35228cf668aSK.Takata                shutil.copyfile(src, dist)
353d58ff7ccSK.Takata            with open(obundles, 'a') as f:
354d58ff7ccSK.Takata                print(dist, file=f)
355d58ff7ccSK.Takata
356d58ff7ccSK.Takatadef anon_normalize_sub(internal, ctags, input_actual, *args):
357d58ff7ccSK.Takata    # TODO: "Units" should not be hardcoded.
358d58ff7ccSK.Takata    input_expected = './Units' + re.sub(r'^.*?/Units', r'', input_actual, 1)
359d58ff7ccSK.Takata
3603e288efeSK.Takata    ret = subprocess.run([CTAGS, '--quiet', '--options=NONE', '--_anonhash=' + input_actual],
3613e288efeSK.Takata            stdout=subprocess.PIPE)
362d58ff7ccSK.Takata    actual = ret.stdout.decode('utf-8').splitlines()[0]
3633e288efeSK.Takata    ret = subprocess.run([CTAGS, '--quiet', '--options=NONE', '--_anonhash=' + input_expected],
3643e288efeSK.Takata            stdout=subprocess.PIPE)
365d58ff7ccSK.Takata    expected = ret.stdout.decode('utf-8').splitlines()[0]
366d58ff7ccSK.Takata
367d58ff7ccSK.Takata    if internal:
3683e288efeSK.Takata        retlist = [[actual, expected]]
3693e288efeSK.Takata    else:
3703e288efeSK.Takata        retlist = ['-e', 's/' + actual + '/' + expected + '/g']
371d58ff7ccSK.Takata    if len(args) > 0:
3723e288efeSK.Takata        return retlist + anon_normalize_sub(internal, ctags, *args)
373d58ff7ccSK.Takata    else:
3743e288efeSK.Takata        return retlist
375d58ff7ccSK.Takata
376d58ff7ccSK.Takatadef is_anon_normalize_needed(rawout):
377d58ff7ccSK.Takata    with open(rawout, 'r', errors='ignore') as f:
378d58ff7ccSK.Takata        if re.search(r'[0-9a-f]{8}', f.read()):
379d58ff7ccSK.Takata            return True
380d58ff7ccSK.Takata    return False
381d58ff7ccSK.Takata
3823e288efeSK.Takata# return a list of filters for normalizing anonhash
3833e288efeSK.Takata#
3843e288efeSK.Takata# If internal is True, return a list of pairs of [pattern, replacement],
3853e288efeSK.Takata# otherwise return a list of command line arguments.
386d58ff7ccSK.Takatadef anon_normalize(internal, rawout, ctags, input_actual, *args):
387d58ff7ccSK.Takata    if is_anon_normalize_needed(rawout):
388d58ff7ccSK.Takata        return anon_normalize_sub(internal, ctags, input_actual, *args)
389d58ff7ccSK.Takata    else:
3903e288efeSK.Takata        return []
391d58ff7ccSK.Takata
392d58ff7ccSK.Takatadef run_filter(finput, foutput, base_filter, anon_filters):
393d58ff7ccSK.Takata    pat1 = [re.compile(base_filter[0]), base_filter[1]]
394d58ff7ccSK.Takata    pat2 = [(re.compile(p[0]), p[1]) for p in anon_filters]
395d58ff7ccSK.Takata    with open(finput, 'r', errors='surrogateescape') as fin, \
396d58ff7ccSK.Takata            open(foutput, 'w', errors='surrogateescape', newline='\n') as fout:
397d58ff7ccSK.Takata        for l in fin:
398d58ff7ccSK.Takata            l = pat1[0].sub(pat1[1], l, 1)
399d58ff7ccSK.Takata            for p in pat2:
400d58ff7ccSK.Takata                l = p[0].sub(p[1], l)
401d58ff7ccSK.Takata            print(l, end='', file=fout)
402d58ff7ccSK.Takata
403eafd0e70SK.Takatadef guess_lang(cmdline, finput):
404eafd0e70SK.Takata    ret = subprocess.run(cmdline + ['--print-language', finput],
405eafd0e70SK.Takata            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
406eafd0e70SK.Takata    return re.sub(r'^.*: ', r'',
407eafd0e70SK.Takata            ret.stdout.decode('utf-8').replace("\r\n", "\n").replace("\n", ''))
408eafd0e70SK.Takata
409eafd0e70SK.Takatadef guess_lang_from_log(log):
410eafd0e70SK.Takata    with open(log, 'r', encoding='utf-8', errors='ignore') as f:
411eafd0e70SK.Takata        for l in f:
412eafd0e70SK.Takata            ret = re.match('OPENING.* as (.*) language .*file ', l)
413eafd0e70SK.Takata            if ret:
414eafd0e70SK.Takata                return ret.group(1)
415eafd0e70SK.Takata    return ''
416eafd0e70SK.Takata
417d58ff7ccSK.Takatadef run_tcase(finput, t, name, tclass, category, build_t, extra_inputs):
418d58ff7ccSK.Takata    global L_PASSED
419d58ff7ccSK.Takata    global L_FIXED
420d58ff7ccSK.Takata    global L_FAILED_BY_STATUS
421d58ff7ccSK.Takata    global L_FAILED_BY_DIFF
422d58ff7ccSK.Takata    global L_SKIPPED_BY_FEATURES
423d58ff7ccSK.Takata    global L_SKIPPED_BY_LANGUAGES
424d58ff7ccSK.Takata    global L_SKIPPED_BY_ILOOP
425d58ff7ccSK.Takata    global L_KNOWN_BUGS
426d58ff7ccSK.Takata    global L_FAILED_BY_TIMEED_OUT
427d58ff7ccSK.Takata    global L_BROKEN_ARGS_CTAGS
428d58ff7ccSK.Takata    global L_VALGRIND
429d58ff7ccSK.Takata
430d58ff7ccSK.Takata    o = build_t
431d58ff7ccSK.Takata
432d58ff7ccSK.Takata    fargs = t + '/args.ctags'
433d58ff7ccSK.Takata    ffeatures = t + '/features'
434d58ff7ccSK.Takata    flanguages = t + '/languages'
435d58ff7ccSK.Takata    ffilter = t + '/filter'
436d58ff7ccSK.Takata
437d58ff7ccSK.Takata    fexpected = t + '/expected.tags'
438d58ff7ccSK.Takata    output_type = 'ctags'
439d58ff7ccSK.Takata    output_label = ''
4403e288efeSK.Takata    output_tflag = []
441d58ff7ccSK.Takata    output_feature = ''
442d58ff7ccSK.Takata    output_lang_extras = ''
443d58ff7ccSK.Takata
444d58ff7ccSK.Takata    if os.path.isfile(fexpected):
445d58ff7ccSK.Takata        pass
446d58ff7ccSK.Takata    elif os.path.isfile(t + '/expected.tags-e'):
447d58ff7ccSK.Takata        fexpected = t + '/expected.tags-e'
448d58ff7ccSK.Takata        output_type = 'etags'
449d58ff7ccSK.Takata        output_label = '/' + output_type
4503e288efeSK.Takata        output_tflag = ['-e', '--tag-relative=no']
451d58ff7ccSK.Takata    elif os.path.isfile(t + '/expected.tags-x'):
452d58ff7ccSK.Takata        fexpected = t + '/expected.tags-x'
453d58ff7ccSK.Takata        output_type = 'xref'
454d58ff7ccSK.Takata        output_label = '/' + output_type
4553e288efeSK.Takata        output_tflag = ['-x']
456d58ff7ccSK.Takata    elif os.path.isfile(t + '/expected.tags-json'):
457d58ff7ccSK.Takata        fexpected = t + '/expected.tags-json'
458d58ff7ccSK.Takata        output_type = 'json'
459d58ff7ccSK.Takata        output_label = '/' + output_type
4603e288efeSK.Takata        output_tflag = ['--output-format=json']
461d58ff7ccSK.Takata        output_feature = 'json'
462d58ff7ccSK.Takata
463d58ff7ccSK.Takata    if len(extra_inputs) > 0:
464d58ff7ccSK.Takata        output_lang_extras = ' (multi inputs)'
465d58ff7ccSK.Takata
466d58ff7ccSK.Takata    if not shutil.which(ffilter):
467d58ff7ccSK.Takata        ffilter = 'cat'
468d58ff7ccSK.Takata
469d58ff7ccSK.Takata    ostderr = o + '/' + _STDERR_OUTPUT_NAME
470d58ff7ccSK.Takata    orawout = o + '/RAWOUT.tmp'
471d58ff7ccSK.Takata    ofiltered = o + '/FILTERED.tmp'
472d58ff7ccSK.Takata    odiff = o + '/' + _DIFF_OUTPUT_NAME
473d58ff7ccSK.Takata    ocmdline = o + '/CMDLINE.tmp'
474d892ff97SMasatake YAMATO    ovalgrind = o + '/' + _VALGRIND_OUTPUT_NAME
475d58ff7ccSK.Takata    oresult = o + '/RESULT.tmp'
476d58ff7ccSK.Takata    oshrink_template = o + '/SHRINK-%s.tmp'
477d58ff7ccSK.Takata    obundles = o + '/BUNDLES'
478d58ff7ccSK.Takata
479d58ff7ccSK.Takata    broken_args_ctags = False
480d58ff7ccSK.Takata
481d58ff7ccSK.Takata    #
482d58ff7ccSK.Takata    # Filtered by UNIT
483d58ff7ccSK.Takata    #
484d58ff7ccSK.Takata    if not check_units(name, category):
485d58ff7ccSK.Takata        return False
486d58ff7ccSK.Takata
487d58ff7ccSK.Takata    #
488d58ff7ccSK.Takata    # Build cmdline
489d58ff7ccSK.Takata    #
490c314f261SMasatake YAMATO    cmdline = [CTAGS, '--verbose', '--options=NONE', '--fields=-T']
4913e288efeSK.Takata    if PRETENSE_OPTS != '':
4923e288efeSK.Takata        cmdline += [PRETENSE_OPTS]
4933e288efeSK.Takata    cmdline += ['--optlib-dir=+' + t + '/optlib', '-o', '-']
494d58ff7ccSK.Takata    if os.path.isfile(fargs):
4953e288efeSK.Takata        cmdline += ['--options=' + fargs]
4963e288efeSK.Takata        ret = subprocess.run(cmdline + ['--_force-quit=0'],
4973e288efeSK.Takata                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
498d58ff7ccSK.Takata        if ret.returncode != 0:
499d58ff7ccSK.Takata            broken_args_ctags = True
5005b838a43SMasatake YAMATO
5015b838a43SMasatake YAMATO    #
5025b838a43SMasatake YAMATO    # make a backup (basedcmdline) of cmdline.  basedcmdline is used
5035b838a43SMasatake YAMATO    # as a command line template for running shrinker.  basedcmdline
5045b838a43SMasatake YAMATO    # should not include the name of the input file name.  The
5055b838a43SMasatake YAMATO    # shrinker makes a another cmdline by applying a real input file
5065b838a43SMasatake YAMATO    # name to the template.  On the other hand, cmdline is
5075b838a43SMasatake YAMATO    # destructively updated by appending input file name in this
5085b838a43SMasatake YAMATO    # function. The file name should not be included in the cmdline
5095b838a43SMasatake YAMATO    # template.
5105b838a43SMasatake YAMATO    #
5115b838a43SMasatake YAMATO    # To avoid the updating in this function propagating to
5125b838a43SMasatake YAMATO    # basecmdline, we copy the cmdline here.
5135b838a43SMasatake YAMATO    #
5145b838a43SMasatake YAMATO    basecmdline = cmdline[:]
515d58ff7ccSK.Takata
516fccf7bfeSK.Takata    #
517fccf7bfeSK.Takata    # Filtered by LANGUAGES
518fccf7bfeSK.Takata    #
519fccf7bfeSK.Takata    guessed_lang = None
520fccf7bfeSK.Takata    if len(LANGUAGES) > 0:
521fccf7bfeSK.Takata        guessed_lang = guess_lang(basecmdline, finput)
522fccf7bfeSK.Takata        if not guessed_lang in LANGUAGES:
523fccf7bfeSK.Takata            return False
524fccf7bfeSK.Takata
525d58ff7ccSK.Takata    clean_tcase(o, obundles)
526d58ff7ccSK.Takata    os.makedirs(o, exist_ok=True)
5279d6d762aSK.Takata    if not os.path.samefile(o, t):
528d58ff7ccSK.Takata        prepare_bundles(t, o, obundles)
529d58ff7ccSK.Takata
530d58ff7ccSK.Takata
531eafd0e70SK.Takata    # helper function for building some strings based on guessed_lang
532eafd0e70SK.Takata    def build_strings(guessed_lang):
533eafd0e70SK.Takata        return ('%-59s ' % ('Testing ' + name + ' as ' + guessed_lang + output_lang_extras + output_label),
534eafd0e70SK.Takata                join_cmdline(basecmdline) + ' --language-force=' + guessed_lang + ' %s > /dev/null 2>&1',
535eafd0e70SK.Takata                oshrink_template % (guessed_lang.replace('/', '-')))
536d58ff7ccSK.Takata
537d58ff7ccSK.Takata    (tmp, feat) = check_features(output_feature, ffeatures)
538d58ff7ccSK.Takata    if not tmp:
539fccf7bfeSK.Takata        if not guessed_lang:
540eafd0e70SK.Takata            guessed_lang = guess_lang(basecmdline, finput)
541eafd0e70SK.Takata        msg = build_strings(guessed_lang)[0]
542d58ff7ccSK.Takata        L_SKIPPED_BY_FEATURES += [category + '/' + name]
543d58ff7ccSK.Takata        if feat.startswith('!'):
544d58ff7ccSK.Takata            run_result('skip', msg, oresult, 'unwanted feature "' + feat[1:] + '" is available')
545d58ff7ccSK.Takata        else:
546d58ff7ccSK.Takata            run_result('skip', msg, oresult, 'required feature "' + feat + '" is not available')
547d58ff7ccSK.Takata        return False
548eafd0e70SK.Takata    (tmp, lang) = check_languages(basecmdline, flanguages)
549d58ff7ccSK.Takata    if not tmp:
550fccf7bfeSK.Takata        if not guessed_lang:
551eafd0e70SK.Takata            guessed_lang = guess_lang(basecmdline, finput)
552eafd0e70SK.Takata        msg = build_strings(guessed_lang)[0]
553d58ff7ccSK.Takata        L_SKIPPED_BY_LANGUAGES += [category + '/' + name]
554d58ff7ccSK.Takata        run_result('skip', msg, oresult, 'required language parser "' + lang + '" is not available')
555d58ff7ccSK.Takata        return False
556d58ff7ccSK.Takata    if WITH_TIMEOUT == 0 and tclass == 'i':
557fccf7bfeSK.Takata        if not guessed_lang:
558eafd0e70SK.Takata            guessed_lang = guess_lang(basecmdline, finput)
559eafd0e70SK.Takata        msg = build_strings(guessed_lang)[0]
560d58ff7ccSK.Takata        L_SKIPPED_BY_ILOOP += [category + '/' + name]
561d58ff7ccSK.Takata        run_result('skip', msg, oresult, 'may cause an infinite loop')
562d58ff7ccSK.Takata        return False
563d58ff7ccSK.Takata    if broken_args_ctags:
564fccf7bfeSK.Takata        if not guessed_lang:
565eafd0e70SK.Takata            guessed_lang = guess_lang(basecmdline, finput)
566eafd0e70SK.Takata        msg = build_strings(guessed_lang)[0]
567d58ff7ccSK.Takata        L_BROKEN_ARGS_CTAGS += [category + '/' + name]
568d58ff7ccSK.Takata        run_result('error', msg, None, 'broken args.ctags?')
569d58ff7ccSK.Takata        return False
570d58ff7ccSK.Takata
5713e288efeSK.Takata    cmdline += output_tflag + [finput]
572d58ff7ccSK.Takata    if len(extra_inputs) > 0:
5733e288efeSK.Takata        cmdline += extra_inputs
574d58ff7ccSK.Takata
575d58ff7ccSK.Takata    timeout_value = WITH_TIMEOUT
576d58ff7ccSK.Takata    if WITH_VALGRIND:
577516f81e7SMasatake YAMATO        cmdline = ['valgrind', '--leak-check=full', '--track-origins=yes',
578516f81e7SMasatake YAMATO                   '--error-exitcode=' + str(_VALGRIND_EXIT), '--log-file=' + ovalgrind] + cmdline
579d58ff7ccSK.Takata        timeout_value *= _VG_TIMEOUT_FACTOR
580d58ff7ccSK.Takata    if timeout_value == 0:
581d58ff7ccSK.Takata        timeout_value = None
582d58ff7ccSK.Takata
583d58ff7ccSK.Takata    start = time.time()
584d58ff7ccSK.Takata    try:
5853e288efeSK.Takata        with open(orawout, 'wb') as fo, \
5863e288efeSK.Takata                open(ostderr, 'wb') as fe:
5873e288efeSK.Takata            ret = subprocess.run(cmdline, stdout=fo, stderr=fe,
5883e288efeSK.Takata                    timeout=timeout_value)
589d58ff7ccSK.Takata        run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
590d58ff7ccSK.Takata    except subprocess.TimeoutExpired:
591fccf7bfeSK.Takata        if not guessed_lang:
592eafd0e70SK.Takata            guessed_lang = guess_lang(basecmdline, finput)
593eafd0e70SK.Takata        (msg, cmdline_template, oshrink) = build_strings(guessed_lang)
594d58ff7ccSK.Takata        L_FAILED_BY_TIMEED_OUT += [category + '/' + name]
595d58ff7ccSK.Takata        run_result('error', msg, oresult, 'TIMED OUT')
596d58ff7ccSK.Takata        run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
597d58ff7ccSK.Takata        if RUN_SHRINK and len(extra_inputs) == 0:
598d58ff7ccSK.Takata            run_shrink(cmdline_template, finput, oshrink, guessed_lang)
599d58ff7ccSK.Takata        return False
600d58ff7ccSK.Takata    #print('execute time: %f' % (time.time() - start))
601d58ff7ccSK.Takata
602eafd0e70SK.Takata    guessed_lang = guess_lang_from_log(ostderr)
603eafd0e70SK.Takata    (msg, cmdline_template, oshrink) = build_strings(guessed_lang)
604eafd0e70SK.Takata
605d58ff7ccSK.Takata    if ret.returncode != 0:
606d58ff7ccSK.Takata        if WITH_VALGRIND and ret.returncode == _VALGRIND_EXIT and \
607d58ff7ccSK.Takata                tclass != 'v':
608d58ff7ccSK.Takata            L_VALGRIND += [category + '/' + name]
609d58ff7ccSK.Takata            run_result('error', msg, oresult, 'valgrind-error')
610d58ff7ccSK.Takata            run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
611d58ff7ccSK.Takata            return False
612d58ff7ccSK.Takata        elif tclass == 'b':
613d58ff7ccSK.Takata            L_KNOWN_BUGS += [category + '/' + name]
614d58ff7ccSK.Takata            run_result('known_error', msg, oresult)
615d58ff7ccSK.Takata            run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
616d58ff7ccSK.Takata            if RUN_SHRINK and len(extra_inputs) == 0:
617d58ff7ccSK.Takata                run_shrink(cmdline_template, finput, oshrink, guessed_lang)
618d58ff7ccSK.Takata            return True
619d58ff7ccSK.Takata        else:
620d58ff7ccSK.Takata            L_FAILED_BY_STATUS += [category + '/' + name]
621d58ff7ccSK.Takata            run_result('error', msg, oresult, 'unexpected exit status: ' + str(ret.returncode))
622d58ff7ccSK.Takata            run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
623d58ff7ccSK.Takata            if RUN_SHRINK and len(extra_inputs) == 0:
624d58ff7ccSK.Takata                run_shrink(cmdline_template, finput, oshrink, guessed_lang)
625d58ff7ccSK.Takata            return False
626d58ff7ccSK.Takata    elif WITH_VALGRIND and tclass == 'v':
627d58ff7ccSK.Takata        L_FIXED += [category + '/' + name]
628d58ff7ccSK.Takata
629d58ff7ccSK.Takata    if not os.path.isfile(fexpected):
630d58ff7ccSK.Takata        clean_tcase(o, obundles)
631d58ff7ccSK.Takata        if tclass == 'b':
632d58ff7ccSK.Takata            L_FIXED += [category + '/' + name]
633d58ff7ccSK.Takata        elif tclass == 'i':
634d58ff7ccSK.Takata            L_FIXED += [category + '/' + name]
635d58ff7ccSK.Takata
636d58ff7ccSK.Takata        L_PASSED += [category + '/' + name]
637d58ff7ccSK.Takata        run_result('ok', msg, None, '"expected.tags*" not found')
638d58ff7ccSK.Takata        return True
639d58ff7ccSK.Takata
640d58ff7ccSK.Takata    start = time.time()
641d58ff7ccSK.Takata    if ffilter != 'cat':
642d58ff7ccSK.Takata        # Use external filter
643d58ff7ccSK.Takata        filter_cmd = basename_filter(False, output_type) + \
644d58ff7ccSK.Takata                anon_normalize(False, orawout, CTAGS, finput, *extra_inputs) + \
6453e288efeSK.Takata                ['<', orawout]
6463e288efeSK.Takata        filter_cmd += ['|', ffilter]
6473e288efeSK.Takata        filter_cmd += ['>', ofiltered]
648d58ff7ccSK.Takata        #print(filter_cmd)
6493e288efeSK.Takata        subprocess.run([SHELL, '-c', join_cmdline(filter_cmd)])
650d58ff7ccSK.Takata    else:
651d58ff7ccSK.Takata        # Use internal filter
652d58ff7ccSK.Takata        run_filter(orawout, ofiltered, basename_filter(True, output_type),
653d58ff7ccSK.Takata                anon_normalize(True, orawout, CTAGS, finput, *extra_inputs))
654d58ff7ccSK.Takata    #print('filter time: %f' % (time.time() - start))
655d58ff7ccSK.Takata
656d58ff7ccSK.Takata    start = time.time()
657d58ff7ccSK.Takata    if filecmp.cmp(fexpected, ofiltered):
658d58ff7ccSK.Takata        ret.returncode = 0
659d58ff7ccSK.Takata    else:
6603e288efeSK.Takata        with open(odiff, 'wb') as f:
661ad8e6505SMasatake YAMATO            ret = subprocess.run(['diff', '-U', str(DIFF_U_NUM),
662ad8e6505SMasatake YAMATO                '-I', '^!_TAG', '--strip-trailing-cr', fexpected, ofiltered],
6633e288efeSK.Takata                stdout=f)
664d58ff7ccSK.Takata    #print('diff time: %f' % (time.time() - start))
665d58ff7ccSK.Takata
666d58ff7ccSK.Takata    if ret.returncode == 0:
667d58ff7ccSK.Takata        clean_tcase(o, obundles)
668d58ff7ccSK.Takata        if tclass == 'b':
669d58ff7ccSK.Takata            L_FIXED += [category + '/' + name]
670d58ff7ccSK.Takata        elif WITH_TIMEOUT != 0 and tclass == 'i':
671d58ff7ccSK.Takata            L_FIXED += [category + '/' + name]
672d58ff7ccSK.Takata
673d58ff7ccSK.Takata        L_PASSED += [category + '/' + name]
674d58ff7ccSK.Takata        run_result('ok', msg, None)
675d58ff7ccSK.Takata        return True
676d58ff7ccSK.Takata    else:
677d58ff7ccSK.Takata        if tclass == 'b':
678d58ff7ccSK.Takata            L_KNOWN_BUGS += [category + '/' + name]
679d58ff7ccSK.Takata            run_result('known_error', msg, oresult)
680d58ff7ccSK.Takata            run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
681d58ff7ccSK.Takata            return True
682d58ff7ccSK.Takata        else:
683d58ff7ccSK.Takata            L_FAILED_BY_DIFF += [category + '/' + name]
684d58ff7ccSK.Takata            run_result('error', msg, oresult, 'unexpected output')
685d58ff7ccSK.Takata            run_record_cmdline(cmdline, ffilter, ocmdline, output_type)
686d58ff7ccSK.Takata            return False
687d58ff7ccSK.Takata
688646e1eb7SK.Takatadef create_thread_queue(func):
689646e1eb7SK.Takata    q = queue.Queue()
690646e1eb7SK.Takata    threads = []
691646e1eb7SK.Takata    for i in range(NUM_WORKER_THREADS):
692646e1eb7SK.Takata        t = threading.Thread(target=worker, args=(func, q), daemon=True)
693646e1eb7SK.Takata        t.start()
694646e1eb7SK.Takata        threads.append(t)
695646e1eb7SK.Takata    return (q, threads)
696646e1eb7SK.Takata
697646e1eb7SK.Takatadef worker(func, q):
698d58ff7ccSK.Takata    while True:
699d58ff7ccSK.Takata        item = q.get()
700d58ff7ccSK.Takata        if item is None:
701d58ff7ccSK.Takata            break
702d58ff7ccSK.Takata        try:
703646e1eb7SK.Takata            func(*item)
704d58ff7ccSK.Takata        except:
705d58ff7ccSK.Takata            import traceback
706d58ff7ccSK.Takata            traceback.print_exc()
707d58ff7ccSK.Takata        q.task_done()
708d58ff7ccSK.Takata
709646e1eb7SK.Takatadef join_workers(q, threads):
710646e1eb7SK.Takata    # block until all tasks are done
711646e1eb7SK.Takata    try:
712646e1eb7SK.Takata        q.join()
713646e1eb7SK.Takata    except KeyboardInterrupt:
714646e1eb7SK.Takata        # empty the queue
715646e1eb7SK.Takata        while True:
716646e1eb7SK.Takata            try:
717646e1eb7SK.Takata                q.get_nowait()
718646e1eb7SK.Takata            except queue.Empty:
719646e1eb7SK.Takata                break
720646e1eb7SK.Takata        # try to stop workers
721646e1eb7SK.Takata        for i in range(NUM_WORKER_THREADS):
722646e1eb7SK.Takata            q.put(None)
723646e1eb7SK.Takata        for t in threads:
724646e1eb7SK.Takata            t.join(timeout=2)
725646e1eb7SK.Takata        # exit regardless that workers are stopped
726646e1eb7SK.Takata        sys.exit(1)
727646e1eb7SK.Takata
728646e1eb7SK.Takata    # stop workers
729646e1eb7SK.Takata    for i in range(NUM_WORKER_THREADS):
730646e1eb7SK.Takata        q.put(None)
731646e1eb7SK.Takata    for t in threads:
732646e1eb7SK.Takata        t.join()
733646e1eb7SK.Takata
734d58ff7ccSK.Takatadef accepted_file(fname):
735d58ff7ccSK.Takata    # Ignore backup files
736d58ff7ccSK.Takata    return not fname.endswith('~')
737d58ff7ccSK.Takata
738d58ff7ccSK.Takatadef run_dir(category, base_dir, build_base_dir):
739d58ff7ccSK.Takata    #
740d58ff7ccSK.Takata    # Filtered by CATEGORIES
741d58ff7ccSK.Takata    #
742d58ff7ccSK.Takata    if len(CATEGORIES) > 0 and not category in CATEGORIES:
743d58ff7ccSK.Takata        return False
744d58ff7ccSK.Takata
745d58ff7ccSK.Takata    print("\nCategory: " + category)
746d58ff7ccSK.Takata    line()
747d58ff7ccSK.Takata
748646e1eb7SK.Takata    (q, threads) = create_thread_queue(run_tcase)
749d58ff7ccSK.Takata
750d58ff7ccSK.Takata    for finput in glob.glob(base_dir + '/*.[dbtiv]/input.*'):
751d58ff7ccSK.Takata        finput = finput.replace('\\', '/')  # for Windows
752d58ff7ccSK.Takata        if not accepted_file(finput):
753d58ff7ccSK.Takata            continue
754d58ff7ccSK.Takata
755d58ff7ccSK.Takata        dname = os.path.dirname(finput)
756d58ff7ccSK.Takata        extra_inputs = sorted(map(lambda x: x.replace('\\', '/'), # for Windows
757d58ff7ccSK.Takata            filter(accepted_file,
758d58ff7ccSK.Takata                glob.glob(dname + '/input[-_][0-9].*') +
759d58ff7ccSK.Takata                glob.glob(dname + '/input[-_][0-9][-_]*.*')
760d58ff7ccSK.Takata            )))
761d58ff7ccSK.Takata
762d58ff7ccSK.Takata        tcase_dir = dname
763d58ff7ccSK.Takata        build_tcase_dir = build_base_dir + remove_prefix(tcase_dir, base_dir)
764d58ff7ccSK.Takata        ret = re.match(r'^.*/(.*)\.([dbtiv])$', tcase_dir)
765d58ff7ccSK.Takata        (name, tclass) = ret.group(1, 2)
766d58ff7ccSK.Takata        q.put((finput, tcase_dir, name, tclass, category, build_tcase_dir, extra_inputs))
767d58ff7ccSK.Takata
768646e1eb7SK.Takata    join_workers(q, threads)
769d58ff7ccSK.Takata
770d58ff7ccSK.Takatadef run_show_diff_output(units_dir, t):
771d58ff7ccSK.Takata    print("\t", end='')
772d58ff7ccSK.Takata    line('.')
773d58ff7ccSK.Takata    for fn in glob.glob(units_dir + '/' + t + '.*/' + _DIFF_OUTPUT_NAME):
774d58ff7ccSK.Takata        with open(fn, 'r') as f:
775d58ff7ccSK.Takata            for l in f:
776d58ff7ccSK.Takata                print("\t" + l, end='')
777d58ff7ccSK.Takata    print()
778d58ff7ccSK.Takata
779d58ff7ccSK.Takatadef run_show_stderr_output(units_dir, t):
780d58ff7ccSK.Takata    print("\t", end='')
781d58ff7ccSK.Takata    line('.')
782d58ff7ccSK.Takata    for fn in glob.glob(units_dir + '/' + t + '.*/' + _STDERR_OUTPUT_NAME):
783d58ff7ccSK.Takata        with open(fn, 'r') as f:
784d58ff7ccSK.Takata            lines = f.readlines()
785d58ff7ccSK.Takata            for l in lines[-50:]:
786d58ff7ccSK.Takata                print("\t" + l, end='')
787d58ff7ccSK.Takata    print()
788d58ff7ccSK.Takata
789d892ff97SMasatake YAMATOdef run_show_valgrind_output(units_dir, t):
790d892ff97SMasatake YAMATO    print("\t", end='')
791d892ff97SMasatake YAMATO    line('.')
792d892ff97SMasatake YAMATO    for fn in glob.glob(units_dir + '/' + t + '.*/' + _VALGRIND_OUTPUT_NAME):
793d892ff97SMasatake YAMATO        with open(fn, 'r') as f:
794d892ff97SMasatake YAMATO            for l in f:
795d892ff97SMasatake YAMATO                print("\t" + l, end='')
796d892ff97SMasatake YAMATO    print()
797d892ff97SMasatake YAMATO
798d58ff7ccSK.Takatadef run_summary(build_dir):
799d58ff7ccSK.Takata    print()
800d58ff7ccSK.Takata    print('Summary (see CMDLINE.tmp to reproduce without test harness)')
801d58ff7ccSK.Takata    line()
802d58ff7ccSK.Takata
803d58ff7ccSK.Takata    fmt = '  %-40s%d'
804d58ff7ccSK.Takata    print(fmt % ('#passed:', len(L_PASSED)))
805d58ff7ccSK.Takata
806d58ff7ccSK.Takata    print(fmt % ('#FIXED:', len(L_FIXED)))
807d58ff7ccSK.Takata    for t in L_FIXED:
808d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
809d58ff7ccSK.Takata
810d58ff7ccSK.Takata    print(fmt % ('#FAILED (broken args.ctags?):', len(L_BROKEN_ARGS_CTAGS)))
811d58ff7ccSK.Takata    for t in L_BROKEN_ARGS_CTAGS:
812d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
813d58ff7ccSK.Takata
814d58ff7ccSK.Takata    print(fmt % ('#FAILED (unexpected-exit-status):', len(L_FAILED_BY_STATUS)))
815d58ff7ccSK.Takata    for t in L_FAILED_BY_STATUS:
816d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
817d58ff7ccSK.Takata        if SHOW_DIFF_OUTPUT:
818d58ff7ccSK.Takata            run_show_stderr_output(build_dir, remove_prefix(t, _DEFAULT_CATEGORY + '/'))
819d58ff7ccSK.Takata
820d58ff7ccSK.Takata    print(fmt % ('#FAILED (unexpected-output):', len(L_FAILED_BY_DIFF)))
821d58ff7ccSK.Takata    for t in L_FAILED_BY_DIFF:
822d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
823d58ff7ccSK.Takata        if SHOW_DIFF_OUTPUT:
824d58ff7ccSK.Takata            run_show_stderr_output(build_dir, remove_prefix(t, _DEFAULT_CATEGORY + '/'))
825d58ff7ccSK.Takata            run_show_diff_output(build_dir, remove_prefix(t, _DEFAULT_CATEGORY + '/'))
826d58ff7ccSK.Takata
827d58ff7ccSK.Takata    if WITH_TIMEOUT != 0:
828d58ff7ccSK.Takata        print(fmt % ('#TIMED-OUT (' + str(WITH_TIMEOUT) + 's):', len(L_FAILED_BY_TIMEED_OUT)))
829d58ff7ccSK.Takata        for t in L_FAILED_BY_TIMEED_OUT:
830d58ff7ccSK.Takata            print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
831d58ff7ccSK.Takata
832d58ff7ccSK.Takata    print(fmt % ('#skipped (features):', len(L_SKIPPED_BY_FEATURES)))
833d58ff7ccSK.Takata    for t in L_SKIPPED_BY_FEATURES:
834d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
835d58ff7ccSK.Takata
836d58ff7ccSK.Takata    print(fmt % ('#skipped (languages):', len(L_SKIPPED_BY_LANGUAGES)))
837d58ff7ccSK.Takata    for t in L_SKIPPED_BY_LANGUAGES:
838d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
839d58ff7ccSK.Takata
840d58ff7ccSK.Takata    if WITH_TIMEOUT == 0:
841d58ff7ccSK.Takata        print(fmt % ('#skipped (infinite-loop):', len(L_SKIPPED_BY_ILOOP)))
842d58ff7ccSK.Takata        for t in L_SKIPPED_BY_ILOOP:
843d58ff7ccSK.Takata            print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
844d58ff7ccSK.Takata
845d58ff7ccSK.Takata    print(fmt % ('#known-bugs:', len(L_KNOWN_BUGS)))
846d58ff7ccSK.Takata    for t in L_KNOWN_BUGS:
847d58ff7ccSK.Takata        print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
848d58ff7ccSK.Takata
849d58ff7ccSK.Takata    if WITH_VALGRIND:
850d58ff7ccSK.Takata        print(fmt % ('#valgrind-error:', len(L_VALGRIND)))
851d58ff7ccSK.Takata        for t in L_VALGRIND:
852d58ff7ccSK.Takata            print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
853d892ff97SMasatake YAMATO        if SHOW_DIFF_OUTPUT:
854d892ff97SMasatake YAMATO            print(fmt % ('##valgrind-error:', len(L_VALGRIND)))
855d892ff97SMasatake YAMATO            for t in L_VALGRIND:
856d892ff97SMasatake YAMATO                print("\t" + remove_prefix(t, _DEFAULT_CATEGORY + '/'))
857d892ff97SMasatake YAMATO                run_show_valgrind_output(build_dir, remove_prefix(t, _DEFAULT_CATEGORY + '/'))
858d58ff7ccSK.Takata
859d58ff7ccSK.Takatadef make_pretense_map(arg):
860d58ff7ccSK.Takata    r = ''
861d58ff7ccSK.Takata    for p in arg.split(','):
862d58ff7ccSK.Takata        ret = re.match(r'(.*)/(.*)', p)
863d58ff7ccSK.Takata        if not ret:
864d58ff7ccSK.Takata            error_exit(1, 'wrong format of --_pretend option arg')
865d58ff7ccSK.Takata
866d58ff7ccSK.Takata        (newlang, oldlang) = ret.group(1, 2)
867d58ff7ccSK.Takata        if newlang == '':
868d58ff7ccSK.Takata            error_exit(1, 'newlang part of --_pretend option arg is empty')
869d58ff7ccSK.Takata        if oldlang == '':
870d58ff7ccSK.Takata            error_exit(1, 'oldlang part of --_pretend option arg is empty')
871d58ff7ccSK.Takata
872d58ff7ccSK.Takata        r += ' --_pretend-' + newlang + '=' + oldlang
873d58ff7ccSK.Takata
874d58ff7ccSK.Takata    return r
875d58ff7ccSK.Takata
876d58ff7ccSK.Takatadef action_run(parser, action, *args):
877d58ff7ccSK.Takata    global CATEGORIES
878d58ff7ccSK.Takata    global CTAGS
879d58ff7ccSK.Takata    global UNITS
880d58ff7ccSK.Takata    global LANGUAGES
881d58ff7ccSK.Takata    global WITH_TIMEOUT
882d58ff7ccSK.Takata    global WITH_VALGRIND
883d58ff7ccSK.Takata    global COLORIZED_OUTPUT
884d58ff7ccSK.Takata    global RUN_SHRINK
885d58ff7ccSK.Takata    global SHOW_DIFF_OUTPUT
886d58ff7ccSK.Takata    global PRETENSE_OPTS
887646e1eb7SK.Takata    global NUM_WORKER_THREADS
8883e288efeSK.Takata    global SHELL
889d58ff7ccSK.Takata
8907a85ed06SK.Takata    parser.add_argument('--categories', metavar='CATEGORY1[,CATEGORY2,...]',
8917a85ed06SK.Takata            help='run only CATEGORY* related cases.')
8927a85ed06SK.Takata    parser.add_argument('--ctags',
8937a85ed06SK.Takata            help='ctags executable file for testing')
8947a85ed06SK.Takata    parser.add_argument('--units', metavar='UNITS1[,UNITS2,...]',
8957a85ed06SK.Takata            help='run only UNIT(S).')
8967a85ed06SK.Takata    parser.add_argument('--languages', metavar='PARSER1[,PARSER2,...]',
8977a85ed06SK.Takata            help='run only PARSER* related cases.')
8987a85ed06SK.Takata    parser.add_argument('--with-timeout', type=int, default=0,
8997a85ed06SK.Takata            metavar='DURATION',
9007a85ed06SK.Takata            help='run a test case with specified timeout in seconds. 0 means no timeout (default).')
9017a85ed06SK.Takata    parser.add_argument('--with-valgrind', action='store_true', default=False,
9027a85ed06SK.Takata            help='run a test case under valgrind')
9037a85ed06SK.Takata    parser.add_argument('--colorized-output', choices=['yes', 'no'], default='yes',
9047a85ed06SK.Takata            help='print the result in color.')
9057a85ed06SK.Takata    parser.add_argument('--run-shrink', action='store_true', default=False,
9067a85ed06SK.Takata            help='(TODO: NOT IMPLEMENTED YET)')
9077a85ed06SK.Takata    parser.add_argument('--show-diff-output', action='store_true', default=False,
908d892ff97SMasatake YAMATO            help='show diff output (and valgrind errors) for failed test cases in the summary.')
9097a85ed06SK.Takata    parser.add_argument('--with-pretense-map',
9107a85ed06SK.Takata            metavar='NEWLANG0/OLDLANG0[,...]',
9117a85ed06SK.Takata            help='make NEWLANG parser pretend OLDLANG.')
9127a85ed06SK.Takata    parser.add_argument('--threads', type=int, default=NUM_WORKER_THREADS,
9137a85ed06SK.Takata            help='number of worker threads')
9143e288efeSK.Takata    parser.add_argument('--shell',
9153e288efeSK.Takata            help='shell to be used.')
9167a85ed06SK.Takata    parser.add_argument('units_dir',
9177a85ed06SK.Takata            help='Units directory.')
9187a85ed06SK.Takata    parser.add_argument('build_dir', nargs='?', default='',
9197a85ed06SK.Takata            help='Build directory. If not given, units_dir is used.')
920d58ff7ccSK.Takata
921d58ff7ccSK.Takata    res = parser.parse_args(args)
922d58ff7ccSK.Takata    if res.categories:
923d58ff7ccSK.Takata        CATEGORIES = [x if x == 'ROOT' or x.endswith('.r') else x + '.r'
924d58ff7ccSK.Takata                for x in res.categories.split(',')]
925d58ff7ccSK.Takata    if res.ctags:
926d58ff7ccSK.Takata        CTAGS = res.ctags
927d58ff7ccSK.Takata    if res.units:
928d58ff7ccSK.Takata        UNITS = res.units.split(',')
929d58ff7ccSK.Takata    if res.languages:
930d58ff7ccSK.Takata        LANGUAGES = res.languages.split(',')
931d58ff7ccSK.Takata    WITH_TIMEOUT = res.with_timeout
932d58ff7ccSK.Takata    WITH_VALGRIND = res.with_valgrind
933d58ff7ccSK.Takata    COLORIZED_OUTPUT = (res.colorized_output == 'yes')
934d58ff7ccSK.Takata    RUN_SHRINK = res.run_shrink
935d58ff7ccSK.Takata    SHOW_DIFF_OUTPUT = res.show_diff_output
936d58ff7ccSK.Takata    if res.with_pretense_map:
937d58ff7ccSK.Takata        PRETENSE_OPTS = make_pretense_map(res.with_pretense_map)
938646e1eb7SK.Takata    NUM_WORKER_THREADS = res.threads
9393e288efeSK.Takata    if res.shell:
9403e288efeSK.Takata        SHELL = res.shell
941d58ff7ccSK.Takata    if res.build_dir == '':
942d58ff7ccSK.Takata        res.build_dir = res.units_dir
943d58ff7ccSK.Takata
944d58ff7ccSK.Takata    if WITH_VALGRIND:
945d58ff7ccSK.Takata        check_availability('valgrind')
946d58ff7ccSK.Takata    check_availability('diff')
947d58ff7ccSK.Takata    init_features()
948d58ff7ccSK.Takata
949d58ff7ccSK.Takata    if isabs(res.build_dir):
950d58ff7ccSK.Takata        build_dir = res.build_dir
951d58ff7ccSK.Takata    else:
952d58ff7ccSK.Takata        build_dir = os.path.realpath(res.build_dir)
953d58ff7ccSK.Takata
954d58ff7ccSK.Takata    category = _DEFAULT_CATEGORY
955d58ff7ccSK.Takata    if len(CATEGORIES) == 0 or (category in CATEGORIES):
956d58ff7ccSK.Takata        run_dir(category, res.units_dir, build_dir)
957d58ff7ccSK.Takata
958d58ff7ccSK.Takata    for d in glob.glob(res.units_dir + '/*.r'):
959d58ff7ccSK.Takata        d = d.replace('\\', '/')    # for Windows
960d58ff7ccSK.Takata        if not os.path.isdir(d):
961d58ff7ccSK.Takata            continue
962d58ff7ccSK.Takata        category = os.path.basename(d)
963d58ff7ccSK.Takata        build_d = res.build_dir + '/' + category
964d58ff7ccSK.Takata        run_dir(category, d, build_d)
965d58ff7ccSK.Takata
966d58ff7ccSK.Takata    run_summary(build_dir)
967d58ff7ccSK.Takata
968d58ff7ccSK.Takata    if L_FAILED_BY_STATUS or L_FAILED_BY_DIFF or \
969454960aaSMasatake YAMATO            L_FAILED_BY_TIMEED_OUT or L_BROKEN_ARGS_CTAGS or \
970454960aaSMasatake YAMATO            L_VALGRIND:
971d58ff7ccSK.Takata        return 1
972d58ff7ccSK.Takata    else:
973d58ff7ccSK.Takata        return 0
974d58ff7ccSK.Takata
975099a0013SK.Takatadef action_clean(parser, action, *args):
9767a85ed06SK.Takata    parser.add_argument('units_dir',
9777a85ed06SK.Takata            help='Build directory for units testing.')
978099a0013SK.Takata
979099a0013SK.Takata    res = parser.parse_args(args)
980099a0013SK.Takata    units_dir = res.units_dir
981099a0013SK.Takata
982099a0013SK.Takata    if not os.path.isdir(units_dir):
9837d4fe82aSK.Takata        error_exit(0, 'No such directory: ' + units_dir)
984099a0013SK.Takata
985099a0013SK.Takata    for bundles in glob.glob(units_dir + '/**/BUNDLES', recursive=True):
986099a0013SK.Takata        clean_bundles(bundles)
987099a0013SK.Takata
988099a0013SK.Takata    for fn in glob.glob(units_dir + '/**/*.tmp', recursive=True):
989099a0013SK.Takata        os.remove(fn)
990099a0013SK.Takata    for fn in glob.glob(units_dir + '/**/*.TMP', recursive=True):
991099a0013SK.Takata        os.remove(fn)
992099a0013SK.Takata    return 0
993099a0013SK.Takata
994646e1eb7SK.Takatadef tmain_compare_result(build_topdir):
995646e1eb7SK.Takata    for fn in glob.glob(build_topdir + '/*/*-diff.txt'):
996646e1eb7SK.Takata        print(fn)
997646e1eb7SK.Takata        print()
998646e1eb7SK.Takata        with open(fn, 'r', errors='replace') as f:
999646e1eb7SK.Takata            for l in f:
1000646e1eb7SK.Takata                print("\t" + l, end='')
1001646e1eb7SK.Takata        print()
1002646e1eb7SK.Takata
1003646e1eb7SK.Takata    for fn in glob.glob(build_topdir + '/*/gdb-backtrace.txt'):
1004646e1eb7SK.Takata        with open(fn, 'r', errors='replace') as f:
1005646e1eb7SK.Takata            for l in f:
1006646e1eb7SK.Takata                print("\t" + l, end='')
1007646e1eb7SK.Takata
1008646e1eb7SK.Takatadef tmain_compare(subdir, build_subdir, aspect, file):
1009646e1eb7SK.Takata    msg = '%-59s ' % (aspect)
1010646e1eb7SK.Takata    generated = build_subdir + '/' + aspect + '-diff.txt'
1011646e1eb7SK.Takata    actual = build_subdir + '/' + aspect + '-actual.txt'
1012646e1eb7SK.Takata    expected = subdir + '/' + aspect + '-expected.txt'
1013646e1eb7SK.Takata    if os.path.isfile(actual) and os.path.isfile(expected) and \
1014646e1eb7SK.Takata            filecmp.cmp(actual, expected):
1015646e1eb7SK.Takata        run_result('ok', msg, None, file=file)
10162b622dadSMasatake YAMATO        # When successful, remove files generated in the last
10172b622dadSMasatake YAMATO        # failure to make the directory clean.
10182b622dadSMasatake YAMATO        # Unlike other generated files like gdb-backtrace.txt
10192b622dadSMasatake YAMATO        # misc/review script looks at the -diff.txt file.
10202b622dadSMasatake YAMATO        # Therefore we handle -diff.txt specially here.
10212b622dadSMasatake YAMATO        if os.path.isfile(generated):
10222b622dadSMasatake YAMATO            os.remove(generated)
1023646e1eb7SK.Takata        return True
1024646e1eb7SK.Takata    else:
10253e288efeSK.Takata        with open(generated, 'wb') as f:
1026ad8e6505SMasatake YAMATO            subprocess.run(['diff', '-U',
1027ad8e6505SMasatake YAMATO                str(DIFF_U_NUM), '--strip-trailing-cr',
1028*a91961f0SHiroo HAYASHI                expected, actual],
10293e288efeSK.Takata                stdout=f, stderr=subprocess.STDOUT)
1030646e1eb7SK.Takata        run_result('error', msg, None, 'diff: ' + generated, file=file)
1031646e1eb7SK.Takata        return False
1032646e1eb7SK.Takata
1033646e1eb7SK.Takatadef failed_git_marker(fn):
1034646e1eb7SK.Takata    if shutil.which('git'):
10353e288efeSK.Takata        ret = subprocess.run(['git', 'ls-files', '--', fn],
10363e288efeSK.Takata            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
10373e288efeSK.Takata        if ret.returncode == 0 and ret.stdout == b'':
1038646e1eb7SK.Takata            return '<G>'
1039646e1eb7SK.Takata    return ''
1040646e1eb7SK.Takata
1041646e1eb7SK.Takatadef is_crashed(fn):
1042646e1eb7SK.Takata    with open(fn, 'r') as f:
1043646e1eb7SK.Takata        if 'core dump' in f.read():
1044646e1eb7SK.Takata            return True
1045646e1eb7SK.Takata    return False
1046646e1eb7SK.Takata
1047646e1eb7SK.Takatadef print_backtraces(ctags_exe, cores, fn):
10483e288efeSK.Takata    with open(fn, 'wb') as f:
1049646e1eb7SK.Takata        for coref in cores:
10503e288efeSK.Takata            subprocess.run(['gdb', ctags_exe, '-c', coref, '-ex', 'where', '-batch'],
10513e288efeSK.Takata                stdout=f, stderr=subprocess.DEVNULL)
1052646e1eb7SK.Takata
1053646e1eb7SK.Takatadef tmain_sub(test_name, basedir, subdir, build_subdir):
1054646e1eb7SK.Takata    global TMAIN_STATUS
1055646e1eb7SK.Takata    global TMAIN_FAILED
1056646e1eb7SK.Takata
1057646e1eb7SK.Takata    CODE_FOR_IGNORING_THIS_TMAIN_TEST = 77
1058646e1eb7SK.Takata
1059646e1eb7SK.Takata    os.makedirs(build_subdir, exist_ok=True)
1060646e1eb7SK.Takata
1061646e1eb7SK.Takata    for fn in glob.glob(build_subdir + '/*-actual.txt'):
1062646e1eb7SK.Takata        os.remove(fn)
1063646e1eb7SK.Takata
1064646e1eb7SK.Takata    strbuf = io.StringIO()
1065646e1eb7SK.Takata    print("\nTesting " + test_name, file=strbuf)
1066646e1eb7SK.Takata    line('-', file=strbuf)
1067646e1eb7SK.Takata
1068646e1eb7SK.Takata    if isabs(CTAGS):
1069646e1eb7SK.Takata        ctags_path = CTAGS
1070646e1eb7SK.Takata    else:
1071646e1eb7SK.Takata        ctags_path = os.path.join(basedir, CTAGS)
1072646e1eb7SK.Takata
1073646e1eb7SK.Takata    if isabs(READTAGS):
1074646e1eb7SK.Takata        readtags_path = READTAGS
1075646e1eb7SK.Takata    else:
1076646e1eb7SK.Takata        readtags_path = os.path.join(basedir, READTAGS)
1077646e1eb7SK.Takata
1078d8f6b0edSMasatake YAMATO    if isabs(OPTSCRIPT):
1079d8f6b0edSMasatake YAMATO        optscript_path = OPTSCRIPT
1080d8f6b0edSMasatake YAMATO    else:
1081d8f6b0edSMasatake YAMATO        optscript_path = os.path.join(basedir, OPTSCRIPT)
1082d8f6b0edSMasatake YAMATO
10833e288efeSK.Takata    start = time.time()
10843e288efeSK.Takata    ret = subprocess.run([SHELL, 'run.sh',
10853e288efeSK.Takata            ctags_path,
10863e288efeSK.Takata            build_subdir,
1087d8f6b0edSMasatake YAMATO            readtags_path,
1088d8f6b0edSMasatake YAMATO            optscript_path],
1089646e1eb7SK.Takata            cwd=subdir,
10903e288efeSK.Takata            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
10913e288efeSK.Takata    #print('execute time: %f' % (time.time() - start), file=strbuf)
1092646e1eb7SK.Takata
1093646e1eb7SK.Takata    encoding = 'utf-8'
1094646e1eb7SK.Takata    try:
1095646e1eb7SK.Takata        stdout = ret.stdout.decode(encoding).replace("\r\n", "\n")
1096646e1eb7SK.Takata    except UnicodeError:
1097646e1eb7SK.Takata        encoding = 'iso-8859-1'
1098646e1eb7SK.Takata        stdout = ret.stdout.decode(encoding).replace("\r\n", "\n")
1099646e1eb7SK.Takata    stderr = ret.stderr.decode('utf-8').replace("\r\n", "\n")
1100646e1eb7SK.Takata    if os.path.basename(CTAGS) != 'ctags':
1101646e1eb7SK.Takata        # program name needs to be canonicalized
1102646e1eb7SK.Takata        stderr = re.sub('(?m)^' + os.path.basename(CTAGS) + ':', 'ctags:', stderr)
1103646e1eb7SK.Takata
1104646e1eb7SK.Takata    if ret.returncode == CODE_FOR_IGNORING_THIS_TMAIN_TEST:
1105646e1eb7SK.Takata        run_result('skip', '', None, stdout.replace("\n", ''), file=strbuf)
1106646e1eb7SK.Takata        print(strbuf.getvalue(), end='')
1107646e1eb7SK.Takata        sys.stdout.flush()
1108646e1eb7SK.Takata        strbuf.close()
1109646e1eb7SK.Takata        return True
1110646e1eb7SK.Takata
1111646e1eb7SK.Takata    with open(build_subdir + '/exit-actual.txt', 'w', newline='\n') as f:
1112646e1eb7SK.Takata        print(ret.returncode, file=f)
1113646e1eb7SK.Takata    with open(build_subdir + '/stdout-actual.txt', 'w', newline='\n', encoding=encoding) as f:
1114646e1eb7SK.Takata        print(stdout, end='', file=f)
1115646e1eb7SK.Takata    with open(build_subdir + '/stderr-actual.txt', 'w', newline='\n') as f:
1116646e1eb7SK.Takata        print(stderr, end='', file=f)
1117646e1eb7SK.Takata
1118646e1eb7SK.Takata    if os.path.isfile(build_subdir + '/tags'):
1119646e1eb7SK.Takata        os.rename(build_subdir + '/tags', build_subdir + '/tags-actual.txt')
1120646e1eb7SK.Takata
1121646e1eb7SK.Takata    for aspect in ['stdout', 'stderr', 'exit', 'tags']:
1122646e1eb7SK.Takata        expected_txt = subdir + '/' + aspect + '-expected.txt'
1123646e1eb7SK.Takata        actual_txt = build_subdir + '/' + aspect + '-actual.txt'
1124646e1eb7SK.Takata        if os.path.isfile(expected_txt):
1125646e1eb7SK.Takata            if tmain_compare(subdir, build_subdir, aspect, strbuf):
1126646e1eb7SK.Takata                os.remove(actual_txt)
1127646e1eb7SK.Takata            else:
1128646e1eb7SK.Takata                TMAIN_FAILED += [test_name + '/' + aspect + '-compare' +
1129646e1eb7SK.Takata                        failed_git_marker(expected_txt)]
1130646e1eb7SK.Takata                TMAIN_STATUS = False
1131646e1eb7SK.Takata                if aspect == 'stderr' and \
1132646e1eb7SK.Takata                        is_crashed(actual_txt) and \
1133646e1eb7SK.Takata                        shutil.which('gdb'):
1134646e1eb7SK.Takata                    print_backtraces(ctags_path,
1135646e1eb7SK.Takata                            glob.glob(build_subdir + '/core*'),
1136646e1eb7SK.Takata                            build_subdir + '/gdb-backtrace.txt')
1137646e1eb7SK.Takata        elif os.path.isfile(actual_txt):
1138646e1eb7SK.Takata            os.remove(actual_txt)
1139646e1eb7SK.Takata
1140646e1eb7SK.Takata    print(strbuf.getvalue(), end='')
1141646e1eb7SK.Takata    sys.stdout.flush()
1142646e1eb7SK.Takata    strbuf.close()
1143646e1eb7SK.Takata    return True
1144646e1eb7SK.Takata
1145646e1eb7SK.Takatadef tmain_run(topdir, build_topdir, units):
1146646e1eb7SK.Takata    global TMAIN_STATUS
1147646e1eb7SK.Takata
1148646e1eb7SK.Takata    TMAIN_STATUS = True
1149646e1eb7SK.Takata
1150646e1eb7SK.Takata    (q, threads) = create_thread_queue(tmain_sub)
1151646e1eb7SK.Takata
1152646e1eb7SK.Takata    basedir = os.getcwd()
1153646e1eb7SK.Takata    for subdir in glob.glob(topdir + '/*.d'):
1154646e1eb7SK.Takata        test_name = os.path.basename(subdir)[:-2]
1155646e1eb7SK.Takata
1156646e1eb7SK.Takata        if len(units) > 0 and not test_name in units:
1157646e1eb7SK.Takata            continue
1158646e1eb7SK.Takata
1159646e1eb7SK.Takata        build_subdir = build_topdir + '/' + os.path.basename(subdir)
1160646e1eb7SK.Takata        q.put((test_name, basedir, subdir, build_subdir))
1161646e1eb7SK.Takata
1162646e1eb7SK.Takata    join_workers(q, threads)
1163646e1eb7SK.Takata
1164646e1eb7SK.Takata    print()
1165646e1eb7SK.Takata    if not TMAIN_STATUS:
1166646e1eb7SK.Takata        print('Failed tests')
1167646e1eb7SK.Takata        line('=')
1168646e1eb7SK.Takata        for f in TMAIN_FAILED:
1169646e1eb7SK.Takata            print(re.sub('<G>', ' (not committed/cached yet)', f))
1170646e1eb7SK.Takata        print()
1171646e1eb7SK.Takata
1172646e1eb7SK.Takata        if SHOW_DIFF_OUTPUT:
1173646e1eb7SK.Takata            print('Detail [compare]')
1174646e1eb7SK.Takata            line('-')
1175646e1eb7SK.Takata            tmain_compare_result(build_topdir)
1176646e1eb7SK.Takata
1177646e1eb7SK.Takata    return TMAIN_STATUS
1178646e1eb7SK.Takata
1179646e1eb7SK.Takatadef action_tmain(parser, action, *args):
1180646e1eb7SK.Takata    global CTAGS
1181646e1eb7SK.Takata    global COLORIZED_OUTPUT
1182646e1eb7SK.Takata    global WITH_VALGRIND
1183646e1eb7SK.Takata    global SHOW_DIFF_OUTPUT
1184646e1eb7SK.Takata    global READTAGS
1185d8f6b0edSMasatake YAMATO    global OPTSCRIPT
1186646e1eb7SK.Takata    global UNITS
1187646e1eb7SK.Takata    global NUM_WORKER_THREADS
11883e288efeSK.Takata    global SHELL
1189646e1eb7SK.Takata
11907a85ed06SK.Takata    parser.add_argument('--ctags',
11917a85ed06SK.Takata            help='ctags executable file for testing')
11927a85ed06SK.Takata    parser.add_argument('--colorized-output', choices=['yes', 'no'], default='yes',
11937a85ed06SK.Takata            help='print the result in color.')
11947a85ed06SK.Takata    parser.add_argument('--with-valgrind', action='store_true', default=False,
11957a85ed06SK.Takata            help='(not implemented) run a test case under valgrind')
11967a85ed06SK.Takata    parser.add_argument('--show-diff-output', action='store_true', default=False,
11977a85ed06SK.Takata            help='how diff output for failed test cases in the summary.')
11987a85ed06SK.Takata    parser.add_argument('--readtags',
11997a85ed06SK.Takata            help='readtags executable file for testing')
1200d8f6b0edSMasatake YAMATO    parser.add_argument('--optscript',
1201d8f6b0edSMasatake YAMATO            help='optscript executable file for testing')
12027a85ed06SK.Takata    parser.add_argument('--units', metavar='UNITS1[,UNITS2,...]',
12037a85ed06SK.Takata            help='run only Tmain/UNIT*.d (.d is not needed)')
12047a85ed06SK.Takata    parser.add_argument('--threads', type=int, default=NUM_WORKER_THREADS,
12057a85ed06SK.Takata            help='number of worker threads')
12063e288efeSK.Takata    parser.add_argument('--shell',
12073e288efeSK.Takata            help='shell to be used.')
12087a85ed06SK.Takata    parser.add_argument('tmain_dir',
12097a85ed06SK.Takata            help='Tmain directory.')
12107a85ed06SK.Takata    parser.add_argument('build_dir', nargs='?', default='',
12117a85ed06SK.Takata            help='Build directory. If not given, tmain_dir is used.')
1212646e1eb7SK.Takata
1213646e1eb7SK.Takata    res = parser.parse_args(args)
1214646e1eb7SK.Takata    if res.ctags:
1215646e1eb7SK.Takata        CTAGS = res.ctags
1216646e1eb7SK.Takata    COLORIZED_OUTPUT = (res.colorized_output == 'yes')
1217646e1eb7SK.Takata    WITH_VALGRIND = res.with_valgrind
1218646e1eb7SK.Takata    SHOW_DIFF_OUTPUT = res.show_diff_output
1219646e1eb7SK.Takata    if res.readtags:
1220646e1eb7SK.Takata        READTAGS = res.readtags
1221d8f6b0edSMasatake YAMATO    if res.optscript:
1222d8f6b0edSMasatake YAMATO        OPTSCRIPT = res.optscript
1223646e1eb7SK.Takata    if res.units:
1224646e1eb7SK.Takata        UNITS = res.units.split(',')
1225646e1eb7SK.Takata    NUM_WORKER_THREADS = res.threads
12263e288efeSK.Takata    if res.shell:
12273e288efeSK.Takata        SHELL = res.shell
1228646e1eb7SK.Takata    if res.build_dir == '':
1229646e1eb7SK.Takata        res.build_dir = res.tmain_dir
1230646e1eb7SK.Takata
1231646e1eb7SK.Takata    #check_availability('awk')
1232646e1eb7SK.Takata    check_availability('diff')
1233646e1eb7SK.Takata
1234646e1eb7SK.Takata    if isabs(res.build_dir):
1235646e1eb7SK.Takata        build_dir = res.build_dir
1236646e1eb7SK.Takata    else:
1237646e1eb7SK.Takata        build_dir = os.path.realpath(res.build_dir)
1238646e1eb7SK.Takata
1239646e1eb7SK.Takata    ret = tmain_run(res.tmain_dir, build_dir, UNITS)
1240646e1eb7SK.Takata    if ret:
1241646e1eb7SK.Takata        return 0
1242646e1eb7SK.Takata    else:
1243646e1eb7SK.Takata        return 1
1244646e1eb7SK.Takata
1245099a0013SK.Takatadef action_clean_tmain(parser, action, *args):
12467a85ed06SK.Takata    parser.add_argument('tmain_dir',
12477a85ed06SK.Takata            help='Build directory for tmain testing.')
1248099a0013SK.Takata
1249099a0013SK.Takata    res = parser.parse_args(args)
1250099a0013SK.Takata    tmain_dir = res.tmain_dir
1251099a0013SK.Takata
1252099a0013SK.Takata    if not os.path.isdir(tmain_dir):
12537d4fe82aSK.Takata        error_exit(0, 'No such directory: ' + tmain_dir)
1254099a0013SK.Takata
1255099a0013SK.Takata    for obj in ['stdout', 'stderr', 'exit', 'tags']:
1256099a0013SK.Takata        for typ in ['actual', 'diff']:
1257099a0013SK.Takata            for fn in glob.glob(tmain_dir + '/**/' + obj + '-' + typ + '.txt', recursive=True):
1258099a0013SK.Takata                os.remove(fn)
1259099a0013SK.Takata    for fn in glob.glob(tmain_dir + '/**/gdb-backtrace.txt', recursive=True):
1260099a0013SK.Takata        os.remove(fn)
1261d58ff7ccSK.Takata    return 0
1262d58ff7ccSK.Takata
1263d58ff7ccSK.Takatadef prepare_environment():
1264d58ff7ccSK.Takata    global _PREPERE_ENV
1265d58ff7ccSK.Takata
1266d58ff7ccSK.Takata    os.environ['LC_ALL'] = 'C'
1267db6404a9SMasatake YAMATO    os.environ['MSYS2_ARG_CONV_EXCL'] = '--regex-;--_scopesep;--exclude;--exclude-exception'
1268d58ff7ccSK.Takata
1269d58ff7ccSK.Takata    _PREPERE_ENV = """LC_ALL="C"; export LC_ALL
1270db6404a9SMasatake YAMATOMSYS2_ARG_CONV_EXCL='--regex-;--_scopesep;--exclude;--exclude-exception' export MSYS2_ARG_CONV_EXCL
1271d58ff7ccSK.Takata"""
1272d58ff7ccSK.Takata
12735edfa40bSK.Takata# enable ANSI escape sequences on Windows 10 1511 (10.0.10586) or later
12745edfa40bSK.Takatadef enable_esc_sequence():
12755edfa40bSK.Takata    if os.name != 'nt':
12765edfa40bSK.Takata        return
12775edfa40bSK.Takata
12785edfa40bSK.Takata    import ctypes
12795edfa40bSK.Takata
12805edfa40bSK.Takata    kernel32 = ctypes.windll.kernel32
12815edfa40bSK.Takata
12825edfa40bSK.Takata    ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
12835edfa40bSK.Takata    STD_OUTPUT_HANDLE = -11
12845edfa40bSK.Takata
12855edfa40bSK.Takata    out = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
12865edfa40bSK.Takata    mode = ctypes.c_ulong()
12875edfa40bSK.Takata    if kernel32.GetConsoleMode(out, ctypes.byref(mode)):
12885edfa40bSK.Takata        kernel32.SetConsoleMode(out,
12895edfa40bSK.Takata                mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
12905edfa40bSK.Takata
1291d58ff7ccSK.Takatadef main():
1292d58ff7ccSK.Takata    prepare_environment()
12935edfa40bSK.Takata    enable_esc_sequence()
1294d58ff7ccSK.Takata
12957a85ed06SK.Takata    parser = argparse.ArgumentParser(
12967a85ed06SK.Takata            description='Units test harness for ctags.')
12977a85ed06SK.Takata    subparsers = parser.add_subparsers(dest='action', metavar='ACTION')
1298d58ff7ccSK.Takata    cmdmap = {}
12997a85ed06SK.Takata    cmdmap['run'] = [action_run,
13007a85ed06SK.Takata            subparsers.add_parser('run', aliases=['units'],
13017a85ed06SK.Takata                description='Run all tests case under units_dir.',
13027a85ed06SK.Takata                help='Run all tests case')]
1303d58ff7ccSK.Takata    cmdmap['units'] = cmdmap['run']
13047a85ed06SK.Takata    cmdmap['clean'] = [action_clean,
13057a85ed06SK.Takata            subparsers.add_parser('clean',
13067a85ed06SK.Takata                description='Clean all files created during units testing.',
13077a85ed06SK.Takata                help='Clean all files created during units testing')]
13087a85ed06SK.Takata    cmdmap['tmain'] = [action_tmain,
13097a85ed06SK.Takata            subparsers.add_parser('tmain',
13107a85ed06SK.Takata                description='Run tests for main part of ctags.',
13117a85ed06SK.Takata                help='Run tests for main part of ctags')]
13127a85ed06SK.Takata    cmdmap['clean-tmain'] = [action_clean_tmain,
13137a85ed06SK.Takata            subparsers.add_parser('clean-tmain',
13147a85ed06SK.Takata                description='Clean all files created during tmain testing.',
13157a85ed06SK.Takata                help='Clean all files created during tmain testing')]
13167a85ed06SK.Takata    subparsers.add_parser('help',
13177a85ed06SK.Takata            help='show this help message and exit')
1318d58ff7ccSK.Takata    cmdmap['help'] = [action_help, parser]
1319d58ff7ccSK.Takata
1320d58ff7ccSK.Takata    if len(sys.argv) < 2:
1321d58ff7ccSK.Takata        parser.print_help()
1322d58ff7ccSK.Takata        sys.exit(1)
1323d58ff7ccSK.Takata
1324d58ff7ccSK.Takata    res = parser.parse_args(sys.argv[1:2])
1325d58ff7ccSK.Takata    (func, subparser) = cmdmap[res.action]
1326d58ff7ccSK.Takata    sys.exit(func(subparser, *sys.argv[1:]))
1327d58ff7ccSK.Takata
1328d58ff7ccSK.Takataif __name__ == '__main__':
1329d58ff7ccSK.Takata    main()
1330