xref: /Universal-ctags/misc/units (revision aba929e40409f1000e07a83f8abfdb4501eabda4)
1#!/bin/sh
2#
3# units - Units test harness for ctags
4#
5# Copyright (C) 2014 Masatake YAMATO
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
19#
20if test -n "${ZSH_VERSION+set}"; then
21    set -o SH_WORD_SPLIT
22    set +o NOMATCH
23fi
24
25#
26# Global Parameters
27#
28SHELL=/bin/sh
29CTAGS=./ctags
30READTAGS=./readtags
31WITH_TIMEOUT=
32WITH_VALGRIND=
33COLORIZED_OUTPUT=yes
34[ -f /dev/stdout ] && COLORIZED_OUTPUT=no
35CATEGORIES=
36UNITS=
37LANGUAGES=
38PRETENSE_OPTS=
39RUN_SHRINK=
40QUIET=
41SHOW_DIFF_OUTPUT=
42VALIDATORS=
43
44#
45# Internal variables and constants
46#
47_CMDLINE=
48_CMDLINE_FOR_SHRINKING=
49_PREPERE_ENV=
50_FEATURE_LIST=
51readonly _DEFAULT_CATEGORY=ROOT
52readonly _TIMEOUT_EXIT=124
53readonly _VG_TIMEOUT_FACTOR=10
54readonly _VALGRIND_EXIT=58
55readonly _BASH_INTERRUPT_EXIT=59
56readonly _LINE_SPLITTER=$(if type dos2unix > /dev/null 2>&1; then echo "dos2unix"; else echo "cat"; fi)
57readonly _STDERR_OUTPUT_NAME="STDERR.tmp"
58readonly _DIFF_OUTPUT_NAME="DIFF.tmp"
59readonly _NOISE_REPORT_MAX_COLUMN=50
60readonly _VALIDATION_EXIT_INVALID=2
61readonly _NOOP_VALIDATOR="NONE"
62readonly _KNOWN_INVALIDATION_VALIDATOR="KNOWN-INVALIDATION"
63if stat --help >/dev/null 2>&1; then
64    readonly _FSIZE="stat -c %s" # GNU coreutils
65else
66    readonly _FSIZE="stat -f %z" # BSD based OSes including macOS
67fi
68
69_RUNNABLE_VALIDATORS=
70_UNAVAILABLE_VALIDATORS=
71
72#
73# Result files and results
74#
75readonly R_PASSED="_PASSED.result"
76readonly R_FIXED="_FIXED.result"
77readonly R_FAILED_BY_STATUS="_FAILED_BY_STATUS.result"
78readonly R_FAILED_BY_DIFF="_FAILED_BY_DIFF.result"
79readonly R_SKIPPED_BY_FEATURES="_SKIPPED_BY_FEATURES.result"
80readonly R_SKIPPED_BY_LANGUAGES="_SKIPPED_BY_LANGUAGES.result"
81readonly R_SKIPPED_BY_ILOOP="_SKIPPED_BY_ILOOP.result"
82readonly R_KNOWN_BUGS="_KNOWN_BUGS.result"
83readonly R_FAILED_BY_TIMEED_OUT="_FAILED_BY_TIMEED_OUT.result"
84readonly R_BROKEN_ARGS_CTAGS="_BROKEN_ARGS_CTAGS.result"
85readonly R_VALGRIND="_VALGRIND.result"
86L_PASSED=
87L_FIXED=
88L_FAILED_BY_STATUS=
89L_FAILED_BY_DIFF=
90L_SKIPPED_BY_FEATURES=
91L_SKIPPED_BY_LANGUAGES=
92L_SKIPPED_BY_ILOOP=
93L_KNOWN_BUGS=
94L_FAILED_BY_TIMEED_OUT=
95L_BROKEN_ARGS_CTAGS=
96L_VALGRIND=
97
98V_VALID=0
99V_INVALID=0
100V_SKIP_VALIDATOR_UNAVAILABLE=0
101V_SKIP_KNOWN_INVALIDATION=0
102
103#
104# TODO
105#
106#  * write new class 'r' (category directory) to units.rst.
107#  * write new class 'v' (skip the checking by valgrind) to units.rst.
108#
109action_help ()
110{
111    cat <<EOF
112Usage:
113	$(help_help)
114
115	$(help_run)
116
117	$(help_clean)
118
119	$(help_fuzz)
120
121	$(help_shrink)
122
123	$(help_noise)
124
125	$(help_tmain)
126
127	$(help_chop)
128
129	$(help_validate_input)
130
131	$(help_clean_tmain)
132EOF
133}
134
135help_help()
136{
137    echo "$0 help|--help"
138}
139
140ERROR ()
141{
142    local status_="$1"
143    local msg="$2"
144    shift 2
145    echo "$msg" 1>&2
146    exit $status_
147}
148
149line()
150{
151    local c=${1:--}
152    local no_newline="${2}"
153    local i=0
154    while [ $i -lt 60 ]; do
155	printf "%c" "$c"
156	i=$(( i + 1 ))
157    done
158
159    if ! [ "${no_newline}" = "--no-newline" ]; then
160	echo
161    fi
162}
163
164count_list ()
165{
166    echo $#
167}
168
169member_p ()
170{
171    local elt="$1"
172    shift
173    local x
174
175    for x in "$@"; do
176	if [ "$x" = "$elt" ]; then
177	    return 0
178	fi
179    done
180
181    return 1
182}
183
184clean_tcase ()
185{
186    local d="$1"
187    local bundles="$2"
188    local b
189
190    if [ -d "$d" ]; then
191	if [ -f "${bundles}" ]; then
192	    while read b; do
193		rm -rf "${b}"
194	    done < "${bundles}"
195	    rm ${bundles}
196	fi
197	rm -f "$d"/*.tmp "$d"/*.TMP
198    fi
199}
200
201check_availability()
202{
203    local cmd="$1"
204    shift
205    type "${cmd}" > /dev/null 2>&1 || ERROR 1 "${cmd} command is not available"
206}
207
208check_units ()
209{
210    local name="$1"
211    local category="$2"
212    shift 2
213    local u
214
215    for u in "$@"; do
216	if echo "${u}" | grep -q /; then
217	    if [ "${u%/*}" = "${category}" ] && [ "${u#*/}" = "${name}" ]; then
218		return 0
219	    fi
220	elif [ "${u}" = "${name}" ]; then
221	    return 0
222	fi
223    done
224    return 1
225}
226
227init_features()
228{
229    _FEATURE_LIST=$( ${CTAGS} --quiet --options=NONE \
230			      --list-features --with-list-header=no \
231			      2> /dev/null \
232		| "${_LINE_SPLITTER}" \
233		| cut -f 1 -d ' ')
234}
235
236check_features()
237{
238    local flag="$1"
239    local ffile
240    local feature
241
242    if [ "${flag}" = "-f" ]; then
243	ffile="$2"
244    elif [ "${flag}" = "-e" ]; then
245	feature="$2"
246    fi
247    shift 2
248
249    local f
250    local found
251    local found_unexpectedly
252    local expected;
253
254
255    for expected in $([ -f "$ffile" ] && cat "$ffile") ${feature}; do
256	    found=no
257	    found_unexpectedly=no
258	    for f in ${_FEATURE_LIST} ; do
259		[ "$expected" = "$f" ] && found=yes
260		[ "$expected" = '!'"$f" ] && found_unexpectedly=yes
261	    done
262	    if [ "${found_unexpectedly}" = yes ]; then
263		echo "$expected"
264		return 1
265	    elif ! [ "$found" = yes ]; then
266		echo "$expected"
267		return 1
268	    fi
269    done
270
271    return 0
272}
273
274check_languages()
275{
276    local lfile="$1"
277    shift
278
279    local l
280    local found
281    local expected;
282
283
284    #
285    # TODO: consider the value of LANGUAGES
286    #
287    while read expected; do
288	    found=no
289	    for l in $( ${_CMDLINE} --list-languages 2>/dev/null | "${_LINE_SPLITTER}" |sed -e 's/ //' ); do
290		[ "$expected" = "$l" ] && found=yes
291	    done
292	    if ! [ "$found" = yes ]; then
293		echo "$expected"
294		return 1
295	    fi
296    done < "$lfile"
297
298    return 0
299}
300
301decorate ()
302{
303    local decorator="$1"
304    local msg="$2"
305
306    case "$decorator" in
307	red)    decorator=31 ;;
308	green)  decorator=32 ;;
309	yellow) decorator=33 ;;
310	*) ERROR 1 "INTERNAL ERROR: wrong run_result function: $f"
311    esac
312
313    if [ "${COLORIZED_OUTPUT}" = 'yes' ]; then
314	printf '%b\n' "\033[${decorator}m${msg}\033[39m"
315    else
316	printf '%b\n' "${msg}"
317    fi
318}
319
320run_result ()
321{
322    local result_type="$1"
323    local msg="$2"
324    local output="$3"
325    shift 3
326    local f="run_result_${result_type}"
327    local tmp
328
329    type "$f" > /dev/null 2>&1 || ERROR 1 \
330	"${msg}INTERNAL ERROR: wrong run_result function: $f"
331
332    "$f" "${msg}" "$@"
333
334    tmp="${COLORIZED_OUTPUT}"
335    COLORIZED_OUTPUT=no
336    "$f" "${msg}" "$@" > "${output}"
337    COLORIZED_OUTPUT="${tmp}"
338}
339
340run_result_skip ()
341{
342    local msg="$1"
343    shift 1
344
345    if [ -n "$1" ]; then
346	printf '%b%b\n' "${msg}" $(decorate yellow "skipped")" ($1)"
347    else
348	printf '%b%b\n' "${msg}" $(decorate yellow "skipped")
349    fi
350}
351
352run_result_error ()
353{
354    local msg="$1"
355    shift 1
356
357    if [ ! -n "$1" ]; then
358	printf '%b%b\n' "${msg}" $(decorate red "failed")
359    else
360	printf '%b%b\n' "${msg}" $(decorate red "failed")" ($1)"
361    fi
362}
363
364run_result_ok ()
365{
366    local msg="$1"
367    shift 1
368
369    if [ ! -n "$1" ]; then
370	printf '%b%b\n' "${msg}" $(decorate green "passed")
371    else
372	printf '%b%b\n' "${msg}" $(decorate green "passed")" ($1)"
373    fi
374}
375
376run_result_known_error ()
377{
378    local msg="$1"
379    shift 1
380
381    printf '%b%b\n' "${msg}" $(decorate yellow "failed")" (KNOWN bug)"
382}
383
384run_shrink ()
385{
386    local cmdline_template="$1"
387    local input="$2"
388    local output="$3"
389    local lang="$4"
390    shift 4
391
392    echo "Shrinking ${input} as ${lang}"
393    shrink_main "${cmdline_template}" "${input}" "${output}"  1 yes
394}
395
396# filters out the directory prefix in a ctags input
397ctags_basename_filter_regex='s%\(^[^	]\{1,\}	\)\(/\{0,1\}\([^/	]\{1,\}/\)*\)%\1%'
398ctags_basename_filter()
399{
400    sed "${ctags_basename_filter_regex}"
401}
402
403# About "input" in the expression, see units.py.
404etags_basename_filter_regex='s%.*\/\(input[-._][[:print:]]\{1,\}\),\([0-9]\{1,\}$\)%\1,\2%'
405etags_basename_filter()
406{
407    sed "${etags_basename_filter_regex}"
408}
409
410xref_basename_filter_regex='s%\(.*[[:digit:]]\{1,\} \)\([^ ]\{1,\}[^ ]\{1,\}\)/\([^ ].\{1,\}.\{1,\}$\)%\1\3%'
411xref_basename_filter()
412{
413    sed "${xref_basename_filter_regex}"
414}
415
416json_basename_filter_regex='s%\("path": \)"[^"]\{1,\}/\([^/"]\{1,\}\)"%\1"\2"%'
417json_basename_filter()
418{
419    sed "${json_basename_filter_regex}"
420}
421
422run_record_cmdline ()
423{
424    local ffilter="$1"
425    local ocmdline="$2"
426
427    printf "%s\n%s \\\\\n| %s \\\\\n| %s\n"  \
428	"${_PREPERE_ENV}" \
429	"${_CMDLINE}" \
430	"sed '${tags_basename_filter_regex}'" \
431	"${ffilter}" \
432	> "${ocmdline}"
433}
434
435#
436# All files and directories other than input.*, expected.tags,
437# args.ctags, README*, features, languages, and filters under srcdir
438# are copied to builddir. These copied files are called bundles.
439#
440prepare_bundles ()
441{
442    local from=$1
443    local to=$2
444    local obundles=$3
445    local src
446    local dist
447
448    for src in ${from}/*; do
449	if [ "${from}"'/*' = "${src}" ]; then
450	    break
451	fi
452	case "${src##*/}" in
453	    input.*)
454		continue
455		;;
456	    expected.tags*)
457		continue
458		;;
459	    README*)
460		continue
461		;;
462	    features|languages|filters)
463		continue
464		;;
465	    args.ctags)
466		continue
467		;;
468	    *)
469		dist="${to}/${src##*/}"
470		if ! cp -a "${src}" "${to}"; then
471		    ERROR 1 "failure in copying bundle file \"${src}\" to \"${to}\""
472		else
473		    echo ${dist} >> ${obundles}
474		fi
475		;;
476	esac
477    done
478}
479
480direq ()
481{
482    [ "$(cd ${1} && pwd)" = "$(cd ${2} && pwd)" ]
483    return $?
484}
485
486anon_normalize ()
487{
488    local ctags=$1
489    local input_actual
490
491    if [ -n "$2" ]; then
492	input_actual=$2
493	shift 2
494
495	# TODO: "Units" should not be hardcoded.
496	local input_expected="./Units${input_actual#*/Units}"
497
498	local actual=$(${CTAGS} --quiet --options=NONE --_anonhash="${input_actual}")
499	local expected=$(${CTAGS} --quiet --options=NONE --_anonhash="${input_expected}")
500
501	sed -e s/${actual}/${expected}/g | anon_normalize "${ctags}" "$@"
502    else
503	cat
504    fi
505}
506
507run_tcase ()
508{
509    local input="$1"
510    local t="$2"
511    local name="$3"
512    local class="$4"
513    local category="$5"
514    local build_t="$6"
515    shift 6
516    # The rest of arguments ($@) are extra inputs
517
518    # I violate the naming convention of build_* to reduce typing
519    local o=${build_t}
520
521    local fargs="$t/args.ctags"
522    local ffeatures="$t/features"
523    local flanguages="$t/languages"
524    local ffilter="$t/filter"
525
526    #
527    # tags-e if for etags output(-e). TAGS is good
528    # suffix but foo.tags and foo.TAGS may be the same on Windows.
529    # tags-x is for cross reference output(-x).
530    # tags-json is for json output.
531    #
532    # fexpected must be set even if none of
533    # expected.{tags,tags-e,tags-x,tags-json} exits.
534    #
535    local fexpected="$t/expected.tags"
536    local output_type=ctags
537    local output_label=
538    local output_tflag=
539    local output_feature=
540    local output_lang_extras=
541
542    if [ -f "$t/expected.tags" ]; then
543	:
544    elif [ -f "$t/expected.tags-e" ]; then
545	fexpected=$t/expected.tags-e
546	output_type=etags
547	output_label=/${output_type}
548	output_tflag="-e  --tag-relative=no"
549    elif [ -f "$t/expected.tags-x" ]; then
550	fexpected=$t/expected.tags-x
551	output_type=xref
552	output_label=/${output_type}
553	output_tflag=-x
554    elif [ -f "$t/expected.tags-json" ]; then
555	fexpected=$t/expected.tags-json
556	output_type=json
557	output_label=/${output_type}
558	output_tflag="--output-format=json"
559	output_feature=json
560    fi
561
562    if [ $# -gt 0 ]; then
563	output_lang_extras=" (multi inputs)"
564    fi
565
566    [ -x "$ffilter" ] || ffilter=cat
567
568    #
569    # All generated file must have suffix ".tmp".
570    #
571    local ostderr="$o/${_STDERR_OUTPUT_NAME}"
572    local orawout="$o/RAWOUT.tmp"
573    local ofiltered="$o/FILTERED.tmp"
574    local odiff="$o/${_DIFF_OUTPUT_NAME}"
575    local ocmdline="$o/CMDLINE.tmp"
576    local ovalgrind="$o/VALGRIND.tmp"
577    local oresult="$o/RESULT.tmp"
578    local oshrink_template="$o/SHRINK-%s.tmp"
579    local obundles="$o/BUNDLES"
580    local oshrink
581
582    local guessed_lang
583    local guessed_lang_no_slash
584    local cmdline_template
585    local timeout_value
586    local tmp
587    local msg
588
589    local broke_args_ctags
590
591    #
592    # Filtered by UNIT
593    #
594    if [ -n "${UNITS}" ]; then
595	check_units "${name}" "${category}" ${UNITS} || return 1
596    fi
597
598    #
599    # Build _CMDLINE
600    #
601    _CMDLINE="${CTAGS} --verbose --options=NONE --fields=-T $PRETENSE_OPTS --optlib-dir=+$t/optlib -o -"
602    [ -f "${fargs}" ] && _CMDLINE="${_CMDLINE} --options=${fargs}"
603
604    if [ -f "${fargs}" ] && ! ${_CMDLINE} --_force-quit=0 > /dev/null 2>&1; then
605	broke_args_ctags=1
606    fi
607
608    #
609    # Filtered by LANGUAGES
610    #
611    guessed_lang=$( ${_CMDLINE} --print-language "$input" 2>/dev/null | sed -n 's/^.*: //p')
612    if [ -n "${LANGUAGES}" ]; then
613	member_p "${guessed_lang}" ${LANGUAGES} || return 1
614    fi
615    guessed_lang_no_slash=$(echo "${guessed_lang}" | tr '/' '-')
616    oshrink=$(printf "${oshrink_template}" "${guessed_lang_no_slash}")
617
618    clean_tcase "${o}" "${obundles}"
619    mkdir -p "${o}"
620    if ! direq "${o}" "${t}"; then
621	prepare_bundles ${t} ${o} "${obundles}"
622    fi
623
624
625    msg=$(printf '%-60s' "Testing ${name} as ${guessed_lang}${output_lang_extras}${output_label}")
626
627    if tmp=$( ( [ -n "${output_feature}" ] && ! check_features -e "${output_feature}" ) ||
628	      ( [ -f "${ffeatures}" ] && ! check_features -f "${ffeatures}" ) ); then
629	echo "${category}/${name}" >> ${R_SKIPPED_BY_FEATURES}
630	case "${tmp}" in
631	    !*) run_result skip "${msg}" "${oresult}" "unwanted feature \"${tmp#?}\" is available";;
632	    *)  run_result skip "${msg}" "${oresult}" "required feature \"${tmp}\" is not available";;
633	esac
634	return 1
635    elif [ -f "${flanguages}" ] && ! tmp=$(check_languages "${flanguages}"); then
636	echo "${category}/${name}" >> ${R_SKIPPED_BY_LANGUAGES}
637	run_result skip "${msg}" "${oresult}" "required language parser \"$tmp\" is not available"
638	return 1
639    elif [ "$WITH_TIMEOUT" = 0 ] && [ "${class}" = 'i' ]; then
640	echo "${category}/${name}" >> ${R_SKIPPED_BY_ILOOP}
641	run_result skip "${msg}" "${oresult}" "may cause an infinite loop"
642	return 1
643    elif [ "$broke_args_ctags" = 1 ]; then
644	run_result error "${msg}" '/dev/null' "broken args.ctags?"
645	echo "${category}/${name}/" >> ${R_BROKEN_ARGS_CTAGS}
646	return 1
647    fi
648
649    cmdline_template="${_CMDLINE} --language-force=${guessed_lang} %s > /dev/null 2>&1"
650    _CMDLINE="${_CMDLINE} ${output_tflag} ${input} $@"
651
652    timeout_value=$WITH_TIMEOUT
653    if [ "$WITH_VALGRIND" = yes ]; then
654	_CMDLINE="valgrind --leak-check=full --error-exitcode=${_VALGRIND_EXIT} --log-file=${ovalgrind} ${_CMDLINE}"
655	timeout_value=$(( timeout_value * ${_VG_TIMEOUT_FACTOR} ))
656    fi
657
658    if ! [ "$timeout_value" = 0 ]; then
659	_CMDLINE="timeout $timeout_value ${_CMDLINE}"
660    fi
661
662    {
663	(
664	    #
665	    # When a launched process is exited abnormally, the parent shell reports it
666	    # to stderr: See j_strsignal function call in wait_for in bash-4.2/nojobs.c.
667	    # This becomes noise; close the stderr of subshell.
668	    #
669	    exec  2>&-;
670	    #
671	    # The original bug report(#1100 by @techee):
672	    # --------------------------------------------------------------------------
673	    # When running
674	    #
675	    #     make units VG=1
676	    #
677	    # one cannot stop its execution by pressing Ctrl+C and
678	    # there doesn't seem to be any way (except for looking at
679	    # processes which run and killing them) to stop its
680	    # execution.
681	    #
682	    trap "exit ${_BASH_INTERRUPT_EXIT}" INT;
683	    ${_CMDLINE} 2> "${ostderr}" > "${orawout}"
684	)
685	tmp="$?"
686	run_record_cmdline "${ffilter}" "${ocmdline}"
687    }
688    if [ "$tmp" != 0 ]; then
689	if [ "${tmp}" = "${_BASH_INTERRUPT_EXIT}" ]; then
690	    ERROR 1 "The execution is interrupted"
691	elif ! [ "$WITH_TIMEOUT" = 0 ] && [ "${tmp}" = "${_TIMEOUT_EXIT}" ]; then
692	    echo "${category}/${name}" >> ${R_FAILED_BY_TIMEED_OUT}
693	    run_result error "${msg}" "${oresult}" "TIMED OUT"
694	    run_record_cmdline "${ffilter}" "${ocmdline}"
695	    [ "${RUN_SHRINK}" = 'yes' ] \
696		&& [ $# -eq 0 ] \
697		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
698	    return 1
699	elif [ "$WITH_VALGRIND" = 'yes' ] && [ "${tmp}" = "${_VALGRIND_EXIT}" ] && ! [ "${class}" = v ]; then
700	    echo "${category}/${name}" >> ${R_VALGRIND}
701	    run_result error "${msg}" "${oresult}" "valgrind-error"
702	    run_record_cmdline "${ffilter}" "${ocmdline}"
703	    return 1
704	elif [ "$class" = 'b' ]; then
705	    echo "${category}/${name}" >> ${R_KNOWN_BUGS}
706	    run_result known_error "${msg}" "${oresult}"
707	    run_record_cmdline "${ffilter}" "${ocmdline}"
708	    [ "${RUN_SHRINK}" = 'yes' ] \
709		&& [ $# -eq 0 ] \
710		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
711	    return 0
712	else
713	    echo "${category}/${name}" >> ${R_FAILED_BY_STATUS}
714	    run_result error "${msg}" "${oresult}" "unexpected exit status: $tmp"
715	    run_record_cmdline "${ffilter}" "${ocmdline}"
716	    [ "${RUN_SHRINK}" = 'yes' ] \
717		&& [ $# -eq 0 ] \
718		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
719	    return 1
720	fi
721    elif [ "$WITH_VALGRIND" = 'yes' ] && [ "$class" = 'v' ]; then
722	echo "${category}/${name}" >> ${R_FIXED}
723    fi
724
725    if ! [ -f "${fexpected}" ]; then
726	clean_tcase "${o}" "${obundles}"
727	if [ "$class" = 'b' ]; then
728	    echo "${category}/${name}" >> ${R_FIXED}
729	elif [ "$class" = 'i' ]; then
730	    echo "${category}/${name}" >> ${R_FIXED}
731	fi
732	echo "${category}/${name}" >> ${R_PASSED}
733	run_result ok "${msg}" '/dev/null' "\"expected.tags*\" not found"
734	return 0
735    fi
736
737    ${output_type}_basename_filter < "${orawout}" | \
738	anon_normalize "${CTAGS}" "${input}" "$@" | \
739	$ffilter > "${ofiltered}"
740
741    {
742	diff -U 0 -I '^!_TAG' --strip-trailing-cr "${fexpected}" "${ofiltered}" > "${odiff}"
743	tmp="$?"
744    }
745    if [ "${tmp}" = 0 ]; then
746	clean_tcase "${o}" "${obundles}"
747	if [ "${class}" = 'b' ]; then
748	    echo "${category}/${name}" >> ${R_FIXED}
749	elif ! [ "$WITH_TIMEOUT" = 0 ] && [ "${class}" = 'i' ]; then
750	    echo "${category}/${name}" >> ${R_FIXED}
751	fi
752
753	echo "${category}/${name}" >> ${R_PASSED}
754	run_result ok "${msg}" '/dev/null'
755	return 0
756    else
757	if [ "${class}" = 'b' ]; then
758	    echo "${category}/${name}" >> ${R_KNOWN_BUGS}
759	    run_result known_error "${msg}" "${oresult}"
760	    run_record_cmdline "${ffilter}" "${ocmdline}"
761	    return 0
762	else
763	    echo "${category}/${name}" >> ${R_FAILED_BY_DIFF}
764	    run_result error "${msg}" "${oresult}" "unexpected output"
765	    run_record_cmdline "${ffilter}" "${ocmdline}"
766	    return 1
767	fi
768    fi
769}
770
771
772failure_in_globing ()
773{
774    # skip if globing failed, also ignore backup files
775    case $1 in
776        *\~)  return 0 ;;
777        *\**) return 0 ;;
778         *)   return 1 ;;
779     esac
780}
781
782run_dir ()
783{
784    local category="$1"
785    local base_dir="$2"
786    local build_base_dir="$3"
787    shift 3
788
789    local tcase_dir
790    local build_tcase_dir
791    local input
792    local name
793    local dname
794    local class
795
796    local extra_tmp
797    local extra_inputs
798
799    #
800    # Filtered by CATEGORIES
801    #
802    if [ -n "$CATEGORIES" ] && ! member_p "${category}" $CATEGORIES; then
803	return 1
804    fi
805
806    echo
807    echo "Category: $category"
808    line
809    for input in ${base_dir}/*.[dbtiv]/input.*; do
810	if failure_in_globing "$input"; then
811	    continue
812	fi
813
814	dname=$(dirname $input)
815	extra_inputs=$(for extra_tmp in $dname/input[-_][0-9].* \
816					$dname/input[-_][0-9][-_]*.* ; do
817	    if failure_in_globing "$extra_tmp"; then
818		continue
819	    fi
820	    echo "$extra_tmp"
821	done | sort)
822
823	tcase_dir="${input%/input.*}"
824	build_tcase_dir="${build_base_dir}/${tcase_dir#${base_dir}/}"
825	name="${tcase_dir%.[dbtiv]}"
826	name="${name##*/}"
827	class="${tcase_dir#*${name}.}"
828	# Run this in parallel
829	run_tcase "${input}" "${tcase_dir}" "${name}" "${class}" "${category}" "${build_tcase_dir}" ${extra_inputs} &
830    done
831    wait
832
833    return 0
834}
835
836run_show_diff_output ()
837{
838    local units_dir="$1"
839    local t="$2"
840
841    printf "	"
842    line .
843    sed -e 's/^.*$/	&/' ${units_dir}/${t}.*/${_DIFF_OUTPUT_NAME}
844    echo
845}
846
847run_show_stderr_output ()
848{
849    local units_dir="$1"
850    local t="$2"
851
852    printf "	"
853    line .
854    sed -e 's/^.*$/	&/' ${units_dir}/${t}.*/${_STDERR_OUTPUT_NAME} | tail -50
855    echo
856}
857
858run_summary ()
859{
860    local build_dir="${1}"
861    local t
862
863    echo
864    echo "Summary (see CMDLINE.tmp to reproduce without test harness)"
865    line
866
867    printf '  %-40s' "#passed:"
868    L_PASSED=$([ -f $R_PASSED ] && cat $R_PASSED)
869    count_list $L_PASSED
870
871    printf '  %-40s' "#FIXED:"
872    L_FIXED=$([ -f $R_FIXED ] && cat $R_FIXED)
873    count_list $L_FIXED
874    for t in $L_FIXED; do
875	echo "	${t#${_DEFAULT_CATEGORY}/}"
876    done
877
878    printf '  %-40s' "#FAILED (broken args.ctags?):"
879    L_BROKEN_ARGS_CTAGS=$([ -f $R_BROKEN_ARGS_CTAGS ] && cat $R_BROKEN_ARGS_CTAGS)
880    count_list $L_BROKEN_ARGS_CTAGS
881    for t in $L_BROKEN_ARGS_CTAGS; do
882	echo "	${t#${_DEFAULT_CATEGORY}/}"
883    done
884
885    printf '  %-40s' "#FAILED (unexpected-exit-status):"
886    L_FAILED_BY_STATUS=$([ -f $R_FAILED_BY_STATUS ] && cat $R_FAILED_BY_STATUS)
887    count_list $L_FAILED_BY_STATUS
888    for t in $L_FAILED_BY_STATUS; do
889	echo "	${t#${_DEFAULT_CATEGORY}/}"
890	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
891	    run_show_stderr_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
892	fi
893    done
894
895    printf '  %-40s' "#FAILED (unexpected-output):"
896    L_FAILED_BY_DIFF=$([ -f $R_FAILED_BY_DIFF ] && cat $R_FAILED_BY_DIFF)
897    count_list $L_FAILED_BY_DIFF
898    for t in $L_FAILED_BY_DIFF; do
899	echo "	${t#${_DEFAULT_CATEGORY}/}"
900	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
901	    run_show_stderr_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
902	    run_show_diff_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
903	fi
904    done
905
906    if ! [ "$WITH_TIMEOUT" = 0 ]; then
907	printf '  %-40s' "#TIMED-OUT (${WITH_TIMEOUT}s)"
908	L_FAILED_BY_TIMEED_OUT=$([ -f $R_FAILED_BY_TIMEED_OUT ] && cat $R_FAILED_BY_TIMEED_OUT)
909	count_list $L_FAILED_BY_TIMEED_OUT
910	for t in $L_FAILED_BY_TIMEED_OUT; do
911	    echo "	${t#${_DEFAULT_CATEGORY}/}"
912	done
913    fi
914
915    printf '  %-40s' "#skipped (features):"
916    L_SKIPPED_BY_FEATURES=$([ -f $R_SKIPPED_BY_FEATURES ] && cat $R_SKIPPED_BY_FEATURES)
917    count_list $L_SKIPPED_BY_FEATURES
918    for t in $L_SKIPPED_BY_FEATURES; do
919	echo "	${t#${_DEFAULT_CATEGORY}/}"
920    done
921
922    printf '  %-40s' "#skipped (languages):"
923    L_SKIPPED_BY_LANGUAGES=$([ -f $R_SKIPPED_BY_LANGUAGES ] && cat $R_SKIPPED_BY_LANGUAGES)
924    count_list $L_SKIPPED_BY_LANGUAGES
925    for t in $L_SKIPPED_BY_LANGUAGES; do
926	echo "	${t#${_DEFAULT_CATEGORY}/}"
927    done
928
929    if [ "$WITH_TIMEOUT" = 0 ]; then
930	printf '  %-40s' "#skipped (infinite-loop):"
931	L_SKIPPED_BY_ILOOP=$([ -f $R_SKIPPED_BY_ILOOP ] && cat $R_SKIPPED_BY_ILOOP)
932	count_list $L_SKIPPED_BY_ILOOP
933	for t in $L_SKIPPED_BY_ILOOP; do
934	    echo "	${t#${_DEFAULT_CATEGORY}/}"
935	done
936    fi
937
938    printf '  %-40s' "#known-bugs:"
939    L_KNOWN_BUGS=$([ -f $R_KNOWN_BUGS ] && cat $R_KNOWN_BUGS)
940    count_list $L_KNOWN_BUGS
941    for t in $L_KNOWN_BUGS; do
942	echo "	${t#${_DEFAULT_CATEGORY}/}"
943    done
944
945    if [ "$WITH_VALGRIND" = yes ]; then
946	printf '  %-40s' "#valgrind-error:"
947	L_VALGRIND=$([ -f $R_VALGRIND ] && cat $R_VALGRIND)
948	count_list $L_VALGRIND
949	for t in $L_VALGRIND; do
950	    echo "	${t#${_DEFAULT_CATEGORY}/}"
951	done
952    fi
953}
954
955make_pretense_map ()
956{
957    local ifs=$IFS
958    local p
959    local r
960
961    IFS=,
962    for p in $1; do
963	newlang=${p%/*}
964	oldlang=${p#*/}
965
966	if [ -z "$newlang" ]; then
967	    ERROR 1 "newlang part of --pretend option arg is empty"
968	fi
969	if [ -z "$oldlang" ]; then
970	    ERROR 1 "oldlang part of --pretend option arg is empty"
971	fi
972
973	r="$r --_pretend-$newlang=$oldlang"
974    done
975    IFS=$ifs
976    echo $r
977}
978
979delete_result_files ()
980{
981    rm -f ${R_PASSED} ${R_FIXED} ${R_FAILED_BY_STATUS} ${R_FAILED_BY_DIFF} \
982	${R_SKIPPED_BY_FEATURES} ${R_SKIPPED_BY_LANGUAGES} \
983	${R_SKIPPED_BY_ILOOP} ${R_KNOWN_BUGS} ${R_FAILED_BY_TIMEED_OUT} \
984	${R_BROKEN_ARGS_CTAGS}
985}
986
987action_run ()
988{
989    local action="$1"
990    shift
991
992    local units_dir
993    local build_dir
994    local d
995    local build_d
996    local category
997
998    local c
999
1000    while [ $# -gt 0 ]; do
1001	case $1 in
1002	    --ctags)
1003		shift
1004		CTAGS="$1"
1005		shift
1006		;;
1007	    --ctags=*)
1008		CTAGS="${1#--ctags=}"
1009		shift
1010		;;
1011	    --categories)
1012		shift
1013		for c in $(echo "$1" | tr ',' ' '); do
1014		    if [ "$c" = "ROOT" ]; then
1015			CATEGORIES="$CATEGORIES ROOT"
1016		    else
1017			CATEGORIES="$CATEGORIES ${c%.r}.r"
1018		    fi
1019		done
1020		shift
1021		;;
1022	    --categories=*)
1023		for c in $(echo "${1#--categories=}" | tr ',' ' '); do
1024		    if [ "$c" = "ROOT" ]; then
1025			CATEGORIES="$CATEGORIES ROOT"
1026		    else
1027			CATEGORIES="$CATEGORIES ${c%.r}.r"
1028		    fi
1029		done
1030		shift
1031		;;
1032	    --units)
1033		shift
1034		UNITS=$(echo "$1" | tr ',' ' ')
1035		shift
1036		;;
1037	    --units=*)
1038		UNITS=$(echo "${1#--units=}" | tr ',' ' ')
1039		shift
1040		;;
1041	    --languages)
1042		shift
1043		LANGUAGES=$(echo "${1}" | tr ',' ' ')
1044		shift
1045		;;
1046	    --languages=*)
1047		LANGUAGES=$(echo "${1#--languages=}" | tr ',' ' ')
1048		shift
1049		;;
1050	    --with-timeout)
1051		shift
1052		WITH_TIMEOUT="$1"
1053		shift
1054		;;
1055	    --with-timeout=*)
1056		WITH_TIMEOUT="${1#--with-timeout=}"
1057		shift
1058		;;
1059	    --with-valgrind)
1060		shift
1061		WITH_VALGRIND=yes
1062		;;
1063	    --colorized-output)
1064		shift
1065		COLORIZED_OUTPUT="$1"
1066		shift
1067		;;
1068	    --colorized-output=*)
1069		COLORIZED_OUTPUT="${1#--colorized-output=}"
1070		shift
1071		;;
1072	    --run-shrink)
1073		RUN_SHRINK=yes
1074		shift
1075		;;
1076	    --show-diff-output)
1077		SHOW_DIFF_OUTPUT=yes
1078		shift
1079		;;
1080	    --with-pretense-map)
1081		shift
1082		PRETENSE_OPTS=$(make_pretense_map "$1")
1083		shift
1084		;;
1085	    --with-pretense-map=*)
1086		PRETENSE_OPTS=$(make_pretense_map "${1#--with-pretense-map=}")
1087		shift
1088		;;
1089	    -*)
1090		ERROR 1 "unknown option \"${1}\" for ${action} action"
1091		;;
1092	    *)
1093		units_dir="$1"
1094		shift
1095		build_dir="${1:-${units_dir}}"
1096		shift
1097		break
1098		;;
1099	esac
1100    done
1101
1102    if [ $# -gt 0 ]; then
1103	ERROR 1 "too many arguments for ${action} action: $*"
1104    elif [ -z "$units_dir" ]; then
1105	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
1106    fi
1107
1108    if ! [ -d "$units_dir" ]; then
1109	ERROR 1 "No such directory: ${units_dir}"
1110    fi
1111
1112    case "${build_dir}" in
1113	/*) ;;
1114	*) build_dir=$(pwd)/${build_dir} ;;
1115    esac
1116
1117    if ! [ -d "$build_dir" ]; then
1118	ERROR 1 "No such directory(build_dir): ${build_dir}"
1119    fi
1120
1121    if ! [ -f "${CTAGS}" ]; then
1122	ERROR 1 "no such file: ${CTAGS}"
1123    elif ! [ -e "${CTAGS}" ]; then
1124	ERROR 1 "${CTAGS} is not an executable file"
1125    fi
1126
1127    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
1128	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
1129    fi
1130
1131    : ${WITH_TIMEOUT:=0}
1132    [ "$WITH_TIMEOUT" = 0 ] || check_availability timeout
1133    [ "$WITH_VALGRIND" = 'yes' ] && check_availability valgrind
1134    [ "$MSYSTEM" != '' ] && check_availability dos2unix
1135    check_availability grep
1136    check_availability diff
1137    init_features
1138
1139    delete_result_files
1140
1141    category="${_DEFAULT_CATEGORY}"
1142    if [ -z "$CATEGORIES" ] \
1143	|| ( [ -n "$CATEGORIES" ] && member_p "${category}" $CATEGORIES ); then
1144	run_dir "${category}" "${units_dir}" "${build_dir}"
1145    fi
1146
1147    for d in ${units_dir}/*.r; do
1148	[ -d "$d" ] || continue
1149	category="${d##*/}"
1150	build_d=${build_dir}/${category}
1151	run_dir "${category}" "$d" "${build_d}"
1152    done
1153
1154    run_summary "${build_dir}"
1155    delete_result_files
1156
1157    if [ -n "${L_FAILED_BY_STATUS}" ] ||
1158	   [ -n "${L_FAILED_BY_DIFF}" ] ||
1159	   [ -n "${L_FAILED_BY_TIMEED_OUT}" ] ||
1160	   [ -n "${L_BROKEN_ARGS_CTAGS}" ]; then
1161	return 1
1162    else
1163	return 0
1164    fi
1165}
1166
1167help_run ()
1168{
1169cat <<EOF
1170$0 run [OPTIONS] UNITS-DIR
1171
1172	   Run all tests case under UNITS-DIR.
1173
1174	   OPTIONS:
1175		--ctags CTAGS: ctags executable file for testing
1176		--categories CATEGORY1[,CATEGORY2,...]: run only CATEGORY* related cases.
1177							Category selection is done in upper
1178							layer than unit selection. This
1179							means even if a unit is specified
1180							with --units, it can be ignored
1181							is a category the units doesn't
1182							belong to is specified with
1183							--categories option.
1184		--colorized-output yes|no: print the result in color.
1185		--skip NAME: skip the case NAME (TODO: NOT IMPLEMENTED YET)
1186		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases.
1187		--units UNITS1[,UNITS2,...]: run only UNIT(S).
1188		--with-timeout DURATION: run a test case under timeout
1189					 command with SECOND.
1190					 0 means no timeout(default).
1191		--with-valgrind: run a test case under valgrind
1192			       If this option given, DURATION is changed to
1193			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
1194		--show-diff-output: show diff output for failed test cases in the summary.
1195		--with-pretense-map=NEWLANG0/OLDLANG0[,...]: make NEWLANG parser pretend
1196							     OLDLANG.
1197EOF
1198}
1199
1200action_clean ()
1201{
1202    local action="$1"
1203    shift
1204
1205    local units_dir=$1
1206    shift
1207
1208    local bundles
1209    local b
1210
1211    if [ $# -gt 0 ]; then
1212	ERROR 1 "too many arguments for ${action} action: $*"
1213    elif [ -z "$units_dir" ]; then
1214	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
1215    fi
1216
1217    if ! [ -d "$units_dir" ]; then
1218	ERROR 0 "No such directory: ${units_dir}"
1219    fi
1220
1221    check_availability find
1222    check_availability rm
1223
1224    for bundles in $(find "$units_dir" -name "BUNDLES"); do
1225	while read b; do
1226	    rm -rf "${b}"
1227	done < ${bundles}
1228	rm ${bundles}
1229    done
1230
1231    rm -f $(find "$units_dir" -name '*.tmp')
1232    rm -f $(find "$units_dir" -name '*.TMP')
1233    return 0
1234}
1235
1236help_clean ()
1237{
1238cat <<EOF
1239$0 clean UNITS-DIR
1240
1241	   Clean all files created during units testing
1242EOF
1243
1244}
1245
1246shrink_prepare ()
1247{
1248    local output="$1"
1249    local input="$2"
1250    local start="$3"
1251    local len="$4"
1252
1253
1254    dd bs=1 count="${len}" skip="${start}" < "${input}" 2>/dev/null > "${output}"
1255}
1256
1257shrink_test ()
1258{
1259    local cmdline="$1"
1260    local input="$2"
1261    local start="$3"
1262    local len="$4"
1263    local output="$5"
1264    local r
1265    local msg
1266
1267    shrink_prepare "${output}" "${input}" "${start}" "${len}"
1268    [ "${QUIET}" = 'yes' ] || printf "[%-5u %6u]..." "${start}" $(( start + len )) 1>&2
1269    eval "${cmdline}" > /dev/null 2>&1
1270    r="$?"
1271    if [ "$r" -eq 0 ]; then
1272	msg='ok'
1273    elif [ "$r" -eq "${_TIMEOUT_EXIT}" ]; then
1274	msg='timeout'
1275    else
1276	msg='failed'
1277    fi
1278    [ "${QUIET}" = 'yes' ] || printf "%s(%u)\n" "$msg" "$r" 1>&2
1279    return $r
1280}
1281
1282shrink_bisect ()
1283{
1284    local cmdline="$1"
1285    local input="$2"
1286    local len="$3"
1287    local output="$4"
1288
1289    local end
1290    local start
1291    local step
1292    local delta
1293
1294    local failed
1295    local successful
1296
1297    end="${len}"
1298    failed="${len}"
1299    successful=0
1300
1301    step=0
1302    while true; do
1303	delta=$((len >> (step + 1)))
1304	if [ "${delta}" -eq 0 ]; then
1305	    delta=1
1306	fi
1307	if shrink_test "${cmdline}" "${input}" 0 "${end}" "${output}"; then
1308	    successful="${end}"
1309	    if [ $(( end + 1 )) -eq "${failed}" ]; then
1310		end="${failed}"
1311		break
1312	    else
1313		end=$((end + delta))
1314	    fi
1315	else
1316	    failed="$end"
1317	    if [ $(( successful + 1 )) -eq "${end}" ]; then
1318		break
1319	    else
1320		end=$((end - delta))
1321	    fi
1322	fi
1323	step=$((step + 1 ))
1324    done
1325
1326    len="${end}"
1327    start=0
1328    failed=0
1329    successful="${end}"
1330    step=0
1331    while true; do
1332	delta=$((len >> (step + 1)))
1333	if [ "${delta}" -eq 0 ]; then
1334	    delta=1
1335	fi
1336	if shrink_test "${cmdline}" "${input}" "${start}" $((end - start)) "${output}"; then
1337	    successful="${start}"
1338	    if [ $(( start - 1 )) -eq "${failed}" ]; then
1339		start=$((start - 1))
1340		break
1341	    else
1342		start=$((start - delta))
1343	    fi
1344	else
1345	    failed="${start}"
1346	    if [ $((successful - 1)) -eq "${start}" ]; then
1347		break
1348	    else
1349		start=$((start + delta))
1350	    fi
1351	fi
1352	step=$((step + 1))
1353    done
1354
1355    len=$((end - start))
1356    shrink_prepare "${output}" "${input}" "${start}" "${len}"
1357    [ "${QUIET}" = 'yes' ] || echo "Minimal badinput: ${output}"
1358    [ "${QUIET}" = 'yes' ] || line .
1359    cat "${output}"
1360    echo
1361
1362    return 0
1363}
1364
1365shrink_main ()
1366{
1367    local cmdline_template="$1"
1368    local cmdline
1369    local input="$2"
1370    local len
1371    local output="$3"
1372    local duration="$4"
1373    local foreground="$5"
1374
1375    if ! [ -f "${input}" ]; then
1376	ERROR 1 "No such file: ${input}"
1377    elif ! [ -r "${input}" ]; then
1378	ERROR 1 "Cannot read a file: ${input}"
1379    fi
1380
1381    if ! cat < /dev/null > "${output}"; then
1382	ERROR 1 "Cannot modify a file: ${output}"
1383    fi
1384
1385    cmdline=$(printf "${cmdline_template}" "${output}")
1386    if [ -n "${duration}" ] && ! [ "${duration}" -eq 0 ]; then
1387	if [ "${foreground}" = 'yes' ]; then
1388	    cmdline="timeout --foreground ${duration} ${cmdline}"
1389	else
1390	    cmdline="timeout ${duration} ${cmdline}"
1391	fi
1392    fi
1393
1394    len=$(${_FSIZE} "${input}")
1395
1396    if shrink_test "${cmdline}" "${input}" 0 "${len}" "${output}"; then
1397	printf "the target command line exits normally against the original input\n" 1>&2
1398	return 1
1399    fi
1400
1401    if ! shrink_test "${cmdline}" "${input}" 0 0 "${output}"; then
1402	printf "the target command line exits abnormally against the empty input\n" 1>&2
1403	return 1
1404    fi
1405
1406    shrink_bisect "${cmdline}" "${input}" "${len}" "${output}"
1407}
1408
1409action_shrink ()
1410{
1411    local action="$1"
1412    shift
1413
1414    local cmdline_template
1415    local input
1416    local output
1417
1418    local timeout
1419    local duration
1420    local foreground
1421
1422
1423    while [ $# -gt 0 ]; do
1424	case $1 in
1425	    --timeout)
1426		shift
1427		duration=$1
1428		shift
1429		;;
1430	    --timeout=*)
1431		duration="${1#--timeout=}"
1432		shift
1433		;;
1434	    --foreground)
1435		foreground=yes
1436		shift
1437		;;
1438	    --quiet)
1439		QUIET=yes
1440		shift
1441		;;
1442	    -*)
1443		ERROR 1 "unknown option \"${1}\" for ${action} action"
1444		;;
1445	    *)
1446		break
1447		;;
1448	    esac
1449    done
1450
1451    if [ $# -lt 3 ]; then
1452	ERROR 1 "too few arguments for ${action} action: $*"
1453    elif [ $# -gt 3 ]; then
1454	ERROR 1 "too many arguments for ${action} action: $*"
1455    fi
1456
1457    if [ -n "${foreground}" ] && [ -z "${duration}" ]; then
1458	ERROR 1 "--foreground option is meaningful only if --timeout option is specified."
1459    fi
1460
1461    cmdline_template=$1
1462    input=$2
1463    output=$3
1464    shift 3
1465
1466    shrink_main "${cmdline_template}" "${input}" "${output}" ${duration} ${foreground}
1467    return $?
1468}
1469
1470help_shrink ()
1471{
1472cat <<EOF
1473$0 shrink [OPTIONS] CMD_TEMPLATE INPUT OUTPUT
1474
1475	   Shrink the input while the execution of CMD_TEMPLATE is failed
1476	   and find minimal unwanted input.
1477
1478	   OPTIONS:
1479		--timeout N: Run CMD under timeout command with duration N
1480		--foreground: add --foreground option to timeout command.
1481			      can be used with --timeout option.
1482	   EXAMPLES:
1483		misc/units shrink "u-ctags -o - %s" original-input.js  /tmp/anyname.js
1484EOF
1485}
1486#action_shrink shrink --timeout=1 --foreground "./a.out  < %s" input.txt output.txt
1487
1488fuzz_shrink ()
1489{
1490    local cmdline_template="$1"
1491    local input="$2"
1492    local output="$3"
1493    local lang="$4"
1494    shift 4
1495
1496    [ "${QUIET}" = 'yes' ] || {
1497	echo "Shrinking ${input} as ${lang}"
1498	line .
1499    }
1500    shrink_main "${cmdline_template}" "${input}" "${output}"  1 yes
1501}
1502
1503fuzz_lang_file ()
1504{
1505    local lang="$1"
1506    local file="$2"
1507    shift 2
1508    local r
1509
1510    local dir="${file%/*}"
1511    local ovalgrind="${dir}/VALGRIND-${lang}.tmp"
1512    local ocmdline="${dir}/CMDLINE-${lang}.tmp"
1513    local oshrink="${dir}/SHRINK-${lang}.tmp"
1514
1515    local cmdline
1516    local cmdline_for_shirking
1517
1518    rm -f "${ovalgrind}" "${ocmdline}" "${oshrink}"
1519
1520
1521    if [ "${WITH_VALGRIND}" = 'yes' ]; then
1522	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${file}" "${ovalgrind}" )
1523    else
1524	cmdline="${_CMDLINE} --language-force=${lang} ${file}"
1525
1526    fi
1527    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"
1528
1529
1530    [ "${QUIET}" = 'yes' ] || printf "."
1531    echo "${cmdline}" > "${ocmdline}"
1532    ${cmdline} > /dev/null
1533    r=$?
1534
1535    case $r in
1536	0)
1537	    rm -f "${ovalgrind}" "${ocmdline}"
1538	    return 0
1539	    ;;
1540	${_TIMEOUT_EXIT})
1541	    [ "${QUIET}" = 'yes' ] || echo
1542	    printf '%-40s' "[timeout $lang]"
1543	    echo "$f"
1544	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${file}" "${oshrink}" "${lang}"
1545	    return 1
1546	    ;;
1547	${_VALGRIND_EXIT})
1548	    [ "${QUIET}" = 'yes' ] || echo
1549	    printf '%-40s' "[valgrind-error $lang]"
1550	    echo "$f"
1551	    return 1
1552	    ;;
1553	*)
1554	    [ "${QUIET}" = 'yes' ] || echo
1555	    printf '%-40s' "[unexpected-status($r) $lang]"
1556	    echo "$f"
1557	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${file}" "${oshrink}" "${lang}"
1558	    return 1
1559	    ;;
1560    esac
1561
1562    return $r
1563}
1564
1565fuzz_lang ()
1566{
1567    local lang="$1"
1568    local dir="$2"
1569    shift 2
1570    local f
1571    local r=0
1572
1573    [ "${QUIET}" = 'yes' ] || printf '%-60s\n' "Semi-fuzzing (${lang})"
1574    for f in $(find "${dir}" -type f -name 'input.*'); do
1575	if ! fuzz_lang_file "${lang}" "${f}"; then
1576	    r=1
1577	    break
1578	fi
1579    done
1580    [ "${QUIET}" = 'yes' ] || echo
1581    return $r
1582}
1583
1584action_fuzz ()
1585{
1586    action_fuzz_common fuzz_lang "$@"
1587}
1588
1589action_fuzz_common ()
1590{
1591    local fn="$1"
1592    local action="$2"
1593    shift 2
1594
1595    local units_dir
1596    local cmdline
1597    local lang
1598    local r
1599
1600    while [ $# -gt 0 ]; do
1601	case $1 in
1602	    --ctags)
1603		shift
1604		CTAGS="$1"
1605		shift
1606		;;
1607	    --ctags=*)
1608		CTAGS="${1#--ctags=}"
1609		shift
1610		;;
1611	    --languages)
1612		shift
1613		LANGUAGES=$(echo "${1}" | tr ',' ' ')
1614		shift
1615		;;
1616	    --languages=*)
1617		LANGUAGES=$(echo "${1#--languages=}" | tr ',' ' ')
1618		shift
1619		;;
1620	    --quiet)
1621		QUIET=yes
1622		shift
1623		;;
1624	    --with-timeout)
1625		shift
1626		WITH_TIMEOUT="$1"
1627		shift
1628		;;
1629	    --with-timeout=*)
1630		WITH_TIMEOUT="${1#--with-timeout=}"
1631		shift
1632		;;
1633	    --with-valgrind)
1634		shift
1635		WITH_VALGRIND=yes
1636		;;
1637	    --colorized-output)
1638		shift
1639		COLORIZED_OUTPUT="$1"
1640		shift
1641		;;
1642	    --colorized-output=*)
1643		COLORIZED_OUTPUT="${1#--colorized-output=}"
1644		shift
1645		;;
1646	    --run-shrink)
1647		RUN_SHRINK=yes
1648		shift
1649		;;
1650	    -*)
1651		ERROR 1 "unknown option \"${1}\" for ${action} action"
1652		;;
1653	    *)
1654		units_dir="$1"
1655		shift
1656		break;
1657		;;
1658	esac
1659    done
1660
1661    if [ $# -gt 0 ]; then
1662	ERROR 1 "too many arguments for ${action} action: $*"
1663    elif [ -z "$units_dir" ]; then
1664	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
1665    fi
1666
1667    if ! [ -d "$units_dir" ]; then
1668	ERROR 0 "No such directory: ${units_dir}"
1669    fi
1670
1671    if ! [ -f "${CTAGS}" ]; then
1672	ERROR 1 "no such file: ${CTAGS}"
1673    elif ! [ -e "${CTAGS}" ]; then
1674	ERROR 1 "${CTAGS} is not an executable file"
1675    fi
1676
1677    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
1678	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
1679    fi
1680
1681    : ${WITH_TIMEOUT:=2}
1682    [ "$WITH_TIMEOUT" = 0 ] || check_availability timeout
1683    [ "$WITH_VALGRIND" = 'yes' ] && check_availability valgrind
1684    check_availability find
1685
1686    cmdline="${CTAGS} --quiet --options=NONE --kinds-all=* --fields=*"
1687    _CMDLINE="${cmdline} -G -o - "
1688    _CMDLINE_FOR_SHRINKING="${_CMDLINE}"
1689    if [ "$WITH_VALGRIND" = yes ]; then
1690	_CMDLINE="valgrind --leak-check=full --error-exitcode=${_VALGRIND_EXIT} --log-file=%s ${_CMDLINE}"
1691	WITH_TIMEOUT=$(( WITH_TIMEOUT * ${_VG_TIMEOUT_FACTOR} ))
1692    fi
1693
1694    if ! [ "$WITH_TIMEOUT" = 0 ]; then
1695	_CMDLINE="timeout --foreground $WITH_TIMEOUT ${_CMDLINE}"
1696	_CMDLINE_FOR_SHRINKING="timeout --foreground 1 ${_CMDLINE_FOR_SHRINKING}"
1697    fi
1698
1699    for lang in $( ${cmdline} --list-languages 2>/dev/null | "${_LINE_SPLITTER}" |sed -e 's/ //' ) ; do
1700	if [ -n "${LANGUAGES}" ] && ! member_p "${lang}" ${LANGUAGES}; then
1701	    continue
1702	fi
1703	"${fn}" "${lang}" "${units_dir}"
1704	r=$?
1705    done
1706
1707    return $r
1708}
1709
1710help_fuzz ()
1711{
1712cat <<EOF
1713$0 fuzz [OPTIONS] UNITS-DIR
1714
1715	   Run all tests case under UNITS-DIR.
1716
1717	   OPTIONS:
1718		--ctags CTAGS: ctags executable file for testing
1719		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
1720		--quiet: don't print dots as passed test cases.
1721		--with-timeout DURATION: run a test case under timeout
1722					 command with SECOND.
1723					 0 means no timeout.
1724					 default is 1.
1725		--with-valgrind: run a test case under valgrind
1726			       If this option given, DURATION is changed to
1727			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
1728EOF
1729}
1730
1731noise_reduce ()
1732{
1733    local input="$1"
1734    local len="$2"
1735    local pos="$3"
1736    shift 3
1737
1738    dd bs=1 count=$pos skip=0 if="$input"
1739    dd bs=1 count=$(( len - pos - 1 )) skip=$(( pos + 1 )) if="$input"
1740}
1741
1742noise_inject ()
1743{
1744    local input="$1"
1745    local len="$2"
1746    local pos="$3"
1747    local c="$4"
1748    shift 4
1749
1750    dd bs=1 count=$pos skip=0 if="$input"
1751    printf "%c" "$c"
1752    dd bs=1 count=$(( len - pos )) skip=$pos if="$input"
1753}
1754
1755noise_report_line ()
1756{
1757    local pos="$1"
1758    local len="$2"
1759    local status_="$3"
1760    local how="$4"
1761
1762    local progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
1763    local nspace
1764
1765
1766    if [ $((pos + 1)) -eq "${len}" ] || [ $status_ -gt 0 ]; then
1767	nspace=0
1768	while [ $nspace -lt $(( _NOISE_REPORT_MAX_COLUMN - progress_offset - 1)) ]; do
1769	    printf ' '
1770	    nspace=$((nspace + 1))
1771	done
1772	printf " %s %d/%d" "${how}" "$pos" "${len}"
1773    fi
1774}
1775
1776noise_lang_file_noisespec ()
1777{
1778    local input="$1"
1779    local len="$2"
1780    local pos="$3"
1781    local c="$4"
1782    local genfn="$5"
1783    local lang="$6"
1784    local how="$7"
1785    shift 7
1786
1787    local msg
1788    if [ "${how}" = + ]; then
1789	msg=INJECTED
1790	how="${how}${c}"
1791    else
1792	msg=REDUCED
1793	how="${how} "
1794    fi
1795
1796    local dir="${input%/*}"
1797    local onoised=$(printf "%s/NOISE-INPUT-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
1798    local ocmdline=$(printf "%s/NOISE-CMDLINE-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
1799    local ovalgrind=$(printf "%s/NOISE-VALGRIND-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
1800    local oshrink=$(printf "%s/NOISE-SHRINK-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
1801
1802    local cmdline
1803    local cmdline_for_shirking
1804    local progress_offset
1805    local r
1806
1807    rm -f "${ocmdline}" "${ovalgrind}" "${onoised}" "${oshrink}"
1808    if [ "${WITH_VALGRIND}" = 'yes' ]; then
1809	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${onoised}" "${ovalgrind}" )
1810	else
1811	cmdline="${_CMDLINE} --language-force=${lang} ${onoised}"
1812    fi
1813    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"
1814
1815    "${genfn}" "${input}" "${len}" "$pos" "$c" > "$onoised"  2> /dev/null
1816
1817    progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
1818    if [ "${progress_offset}" -eq 0 ]; then
1819	[ $pos -gt 0 ] && printf " %s %d/%d" "${how}" "$pos" "${len}"
1820	echo
1821    fi
1822
1823    echo "${cmdline}" > "${ocmdline}"
1824    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
1825    r=$?
1826    case $r in
1827	    0)
1828	    printf 'o'
1829	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
1830	    rm "${onoised}"
1831	    rm -f "${ovalgrind}" "${ocmdline}"
1832	    ;;
1833	${_TIMEOUT_EXIT})
1834	    printf "T"
1835	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
1836	    printf '\n%-20s\n' "[timeout $lang]" "$onoised"
1837	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${onoised}" "${oshrink}" "${lang}"
1838	    ;;
1839	${_VALGRIND_EXIT})
1840	    printf "V"
1841	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
1842	    printf '\n%-20s %s\n' "[valgrind-error $lang]" "$onoised"
1843	    ;;
1844	*)
1845	    printf "!"
1846	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
1847	    printf '\n%-20s %s\n' "[unexpected-status($r) $lang]" "$onoised"
1848	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${onoised}" "${oshrink}" "${lang}"
1849	    ;;
1850    esac
1851    return $r
1852}
1853
1854noise_lang_file ()
1855{
1856    local lang="$1"
1857    local input="$2"
1858    shift 2
1859
1860
1861    local cmdline
1862    local cmdline_for_shirking
1863    local len=$(${_FSIZE} "${input}")
1864    local r
1865    local i
1866    local c
1867    local guessed_lang
1868
1869    guessed_lang=$( ${_CMDLINE_FOR_SHRINKING} --print-language "${input}" 2>/dev/null | sed -n 's/^.*: //p')
1870    if [ "${lang}" !=  "${guessed_lang}" ]; then
1871	return 0
1872    fi
1873
1874    i=0
1875    c='!'
1876    echo "Testing cases derived from: ${input}"
1877    line '.' --no-newline
1878    while [ "$i" -lt "$len" ]; do
1879	if noise_lang_file_noisespec "${input}" "${len}" "$i" "$c" noise_reduce "${lang}" -; then
1880	    i=$(( i + 1 ))
1881	else
1882	    echo
1883	    return 1
1884	fi
1885    done
1886
1887    for c in 'a' '0'						\
1888	'!' '@' '#' '$' '%' '^' '&' '*' '(' ')' '-' '=' '_'	\
1889	'+' '|'  '[' ']' '{' '}' '\' ';' "'" ':' '"' ',' '.'    \
1890	'/' '<' '>' '?' '`' '~'; do
1891	i=0
1892	while [ "$i" -lt "$len" ]; do
1893	    if noise_lang_file_noisespec "${input}" "${len}" "$i" "$c" noise_inject "${lang}" +; then
1894		i=$(( i + 1 ))
1895	    else
1896		echo
1897		return 1
1898	    fi
1899	done
1900    done
1901    echo
1902    return 0
1903}
1904
1905noise_lang ()
1906{
1907    local lang="$1"
1908    local dir="$2"
1909    shift 2
1910    local f
1911    local r
1912    printf '%-60s\n' "Noised-fuzzing (${lang})"
1913    line '-'
1914
1915    r=0
1916    for f in $(find "${dir}" -type f -name 'input.*'); do
1917	if ! noise_lang_file "${lang}" "${f}"; then
1918	    r=1
1919	    break
1920	fi
1921    done
1922    echo
1923    return $r
1924}
1925
1926action_noise ()
1927{
1928    action_fuzz_common noise_lang "$@"
1929}
1930
1931help_noise ()
1932{
1933cat <<EOF
1934$0 noise [OPTIONS] UNITS-DIR
1935
1936	   Run all tests case for LANGUAGE with "noise" for
1937	   finding unexpected behavior like entering an
1938	   infinite loop.
1939	   Here "noise" means removing one byte from
1940	   somewhere file position of the original test case;
1941	   or adding something one byte to
1942	   somewhere file position of the original test case.
1943
1944	   OPTIONS:
1945		--ctags CTAGS: ctags executable file for testing
1946		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
1947		--quiet: don't print dots as passed test cases.
1948		--with-timeout DURATION: run a test case under timeout
1949					 command with SECOND.
1950					 0 means no timeout.
1951					 default is 1.
1952		--with-valgrind: run a test case under valgrind
1953			       If this option given, DURATION is changed to
1954			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
1955EOF
1956}
1957
1958
1959tmain_compare_result()
1960{
1961    local build_topdir=$1
1962    local f
1963
1964    for f in ${build_topdir}/*/*-diff.txt; do
1965	if [ -f "$f" ]; then
1966	    echo "$f"
1967	    echo
1968	    cat "$f" | sed -e 's|.*|	&|'
1969	    echo
1970	fi
1971    done
1972    if [ -f ${build_topdir}/*/gdb-backtrace.txt ]; then
1973	cat ${build_topdir}/*/gdb-backtrace.txt
1974    fi
1975}
1976
1977tmain_compare()
1978{
1979    local subdir=$1
1980    local build_subdir=$2
1981    local aspect=$3
1982    local generated
1983    local msg
1984
1985    msg=$(printf '%-60s' "${aspect}")
1986    generated=${build_subdir}/${aspect}-diff.txt
1987    if diff -U 0 --strip-trailing-cr \
1988	    ${subdir}/${aspect}-expected.txt \
1989	    ${build_subdir}/${aspect}-actual.txt \
1990	    > ${generated} 2>&1; then
1991	run_result ok "${msg}" '/dev/null'
1992	rm ${generated}
1993	return 0
1994    else
1995	run_result error "${msg}" '/dev/null' "diff: ${generated}"
1996	return 1
1997    fi
1998}
1999
2000failed_git_marker ()
2001{
2002    local f=$1
2003    local l
2004
2005    if type "git" > /dev/null 2>&1; then
2006	l=$(git ls-files -- "$f")
2007	if [ -z "$l" ]; then
2008	    echo '<G>'
2009	fi
2010    fi
2011}
2012
2013is_crashed ()
2014{
2015    local f=$1
2016
2017    grep -q -i "core dump" "$f"
2018}
2019
2020print_backtraces()
2021{
2022    local ctags_exe=$1
2023    shift 1
2024
2025    local coref
2026    for coref in "$@"; do
2027	if [ -f "${coref}" ]; then
2028	    gdb "${ctags_exe}" -c "${coref}" -ex where -batch
2029	else
2030	    echo "no such file: ${coref}"
2031	fi
2032    done
2033}
2034
2035CODE_FOR_IGNORING_THIS_TMAIN_TEST=77
2036tmain_run ()
2037{
2038    local topdir=$1
2039    local build_topdir=$2
2040    shift 2
2041    local units="$@"
2042
2043    local subdir
2044    local basedir
2045
2046    local test_name
2047    local r_failed="_failed.result"
2048    local failed
2049    local f
2050
2051    local aspect
2052    local engine
2053
2054    local r
2055    local a
2056    local status_=0
2057    local msg
2058
2059    local need_rearrange
2060
2061    if ! [ $(basename "${CTAGS}") = 'ctags' ]; then
2062	need_rearrange=yes
2063    fi
2064
2065    rm -f ${r_failed}
2066    basedir=$(pwd)
2067    for subdir in ${topdir}/*.d; do
2068	if [ "${subdir}" = ${topdir}/'*.d' ]; then
2069	    return 1
2070	fi
2071
2072	test_name=$(basename ${subdir} .d)
2073
2074	if [ -n "${units}" ] && ! member_p "${test_name}" ${units}; then
2075	    continue
2076	fi
2077
2078	build_subdir=${build_topdir}/$(basename ${subdir})
2079	if ! mkdir -p ${build_subdir}; then
2080	    return 1
2081	fi
2082
2083	# Run this block in parallel
2084	(
2085	    rm -f ${build_subdir}/*-actual.txt
2086
2087	    msg="\nTesting ${test_name}"
2088	    msg="${msg}\n$(line '-')"
2089	    (
2090		cd ${subdir}
2091		${SHELL} run.sh \
2092			 ${basedir}/${CTAGS} \
2093			 ${build_subdir} \
2094			 ${basedir}/${READTAGS}
2095	    ) > ${build_subdir}/stdout-actual.txt 2> ${build_subdir}/stderr-actual.txt
2096	    r=$?
2097	    echo $r > ${build_subdir}/exit-actual.txt
2098
2099	    if [ -n "${need_rearrange}" ]; then
2100		sed -i -e 's|^'$(basename "${CTAGS}")':|ctags:|' ${build_subdir}/stderr-actual.txt
2101	    fi
2102
2103	    if [ $r = $CODE_FOR_IGNORING_THIS_TMAIN_TEST ]; then
2104		msg="${msg}\n$(run_result skip "" '/dev/null' "$(cat ${build_subdir}/stdout-actual.txt)")"
2105		for a in ${build_subdir}/*-actual.txt; do
2106		    if [ -f "$a" ]; then
2107			rm $a
2108		    fi
2109		done
2110		printf "%b\n" "${msg}"
2111		exit
2112	    fi
2113
2114	    if [ -f ${build_subdir}/tags ]; then
2115		mv ${build_subdir}/tags ${build_subdir}/tags-actual.txt
2116	    fi
2117	    for aspect in stdout stderr exit tags; do
2118		if [ -f ${subdir}/${aspect}-expected.txt ]; then
2119		    engine=compare
2120		    msg="${msg}\n$(tmain_${engine} ${subdir} ${build_subdir} ${aspect})"
2121		    if [ $? -eq 0 ]; then
2122			rm ${build_subdir}/${aspect}-actual.txt
2123		    else
2124			echo "${test_name}/${aspect}-${engine}$(failed_git_marker ${subdir}/${aspect}-expected.txt)" >> ${r_failed}
2125			if [ ${aspect} = stderr ] &&
2126			       is_crashed ${build_subdir}/${aspect}-actual.txt &&
2127			       type "gdb" > /dev/null 2>&1; then
2128			    print_backtraces "${basedir}/${CTAGS}" \
2129					     ${build_subdir}/core* \
2130					     > ${build_subdir}/gdb-backtrace.txt
2131			fi
2132		    fi
2133		elif [ -f ${build_subdir}/${aspect}-actual.txt ]; then
2134		    rm ${build_subdir}/${aspect}-actual.txt
2135		fi
2136	    done
2137
2138	    printf "%b\n" "${msg}"
2139	) &
2140    done
2141    wait
2142
2143    if [ -f "${r_failed}" ]; then
2144	status_=1
2145	echo
2146	echo Failed tests
2147	line '='
2148	failed=$(cat ${r_failed})
2149	for f in ${failed}; do
2150	    echo $f | sed -e 's|<G>| (not committed/cached yet)|'
2151	done
2152	echo
2153
2154	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
2155	    engine=compare
2156	    echo Detail "[$engine]"
2157	    line '-'
2158	    tmain_${engine}_result ${build_topdir}
2159	fi
2160	rm ${r_failed}
2161    fi
2162
2163    return $status_
2164}
2165
2166action_tmain ()
2167{
2168    local action="$1"
2169    shift
2170    local tmain_dir
2171    local build_dir
2172
2173    while [ $# -gt 0 ]; do
2174	case $1 in
2175	    --ctags)
2176		shift
2177		CTAGS="$1"
2178		shift
2179		;;
2180	    --ctags=*)
2181		CTAGS="${1#--ctags=}"
2182		shift
2183		;;
2184	    --colorized-output)
2185		shift
2186		COLORIZED_OUTPUT="$1"
2187		shift
2188		;;
2189	    --colorized-output=*)
2190		COLORIZED_OUTPUT="${1#--colorized-output=}"
2191		shift
2192		;;
2193	    --with-valgrind)
2194		shift
2195		WITH_VALGRIND=yes
2196		;;
2197	    --show-diff-output)
2198		SHOW_DIFF_OUTPUT=yes
2199		shift
2200		;;
2201	    --readtags=*)
2202		READTAGS="${1#--readtags=}"
2203		shift
2204		;;
2205	    --units)
2206		shift
2207		UNITS=$(echo "$1" | tr ',' ' ')
2208		shift
2209		;;
2210	    --units=*)
2211		UNITS=$(echo "${1#--units=}" | tr ',' ' ')
2212		shift
2213		;;
2214	    -*)
2215		ERROR 1 "unknown option \"${1}\" for ${action} action"
2216		;;
2217	    *)
2218		tmain_dir="$1"
2219		shift
2220		build_dir=${1:-${tmain_dir}}
2221		if [ -n "$1" ]; then
2222		    shift
2223		fi
2224		break
2225		;;
2226	esac
2227    done
2228
2229    if [ $# -gt 0 ]; then
2230	ERROR 1 "too many arguments for ${action} action: $*"
2231    elif [ -z "$tmain_dir" ]; then
2232	ERROR 1 "TMAIN_DIR parameter is not given in ${action} action"
2233    fi
2234
2235    if ! [ -d "$tmain_dir" ]; then
2236	ERROR 1 "No such directory(tmain_dir): ${tmain_dir}"
2237    fi
2238
2239    case "${build_dir}" in
2240	/*) ;;
2241	*) build_dir=$(pwd)/${build_dir} ;;
2242    esac
2243    if ! [ -d "$build_dir" ]; then
2244	ERROR 1 "No such directory(build_dir): ${build_dir}"
2245    fi
2246
2247    if ! [ -f "${CTAGS}" ]; then
2248	ERROR 1 "no such file: ${CTAGS}"
2249    elif ! [ -e "${CTAGS}" ]; then
2250	ERROR 1 "${CTAGS} is not an executable file"
2251    fi
2252
2253    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
2254	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
2255    fi
2256
2257    check_availability awk
2258    check_availability diff
2259
2260    tmain_run ${tmain_dir} ${build_dir} ${UNITS}
2261    return $?
2262}
2263
2264help_tmain ()
2265{
2266    cat <<EOF
2267$0 tmain [OPTIONS] TMAIN-DIR [BUILD-DIR]
2268
2269	   Run tests for main part of ctags.
2270	   If BUILD-DIR is not given, TMAIN-DIR is reused as BUILD-DIR.
2271
2272	   OPTIONS:
2273
2274		--ctags CTAGS: ctags executable file for testing
2275		--colorized-output yes|no: print the result in color.
2276		--with-valgrind: (not implemented) run a test case under valgrind
2277		--show-diff-output: (not implemented)show diff output for failed test cases in the summary.
2278		--units UNITS1[,UNITS2,...]: run only Tmain/UNIT*.d (.d is not needed)
2279EOF
2280}
2281
2282action_clean_tmain()
2283{
2284    local action="$1"
2285    shift
2286
2287    local tmain_dir=$1
2288    shift
2289
2290    if [ $# -gt 0 ]; then
2291	ERROR 1 "too many arguments for ${action} action: $*"
2292    elif [ -z "$tmain_dir" ]; then
2293	ERROR 1 "TMAIN_DIR parameter is not given in ${action} action"
2294    fi
2295
2296    if ! [ -d "$tmain_dir" ]; then
2297	ERROR 1 "No such directory: ${tmain_dir}"
2298    fi
2299
2300    check_availability find
2301    check_availability rm
2302
2303    local object
2304    local type
2305    for object in stdout stderr exit tags; do
2306	for type in actual diff; do
2307	    rm -f $(find "$tmain_dir" -name ${object}-${type}.txt)
2308	    rm -f $(find "$tmain_dir" -name gdb-backtrace.txt)
2309	done
2310    done
2311    return 0
2312}
2313
2314help_clean_tmain ()
2315{
2316    cat <<EOF
2317$0 clean_tmain TMAIN-DIR
2318
2319	   Clean all files created during tmain testing
2320EOF
2321
2322}
2323
2324help_chop ()
2325{
2326    cat <<EOF
2327$0 chop|slap [OPTIONS] UNITS-DIR
2328
2329	   OPTIONS:
2330
2331		--ctags CTAGS: ctags executable file for testing
2332		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
2333		--quiet: don't print dots as passed test cases.
2334		--with-timeout DURATION: run a test case under timeout
2335					 command with SECOND.
2336					 0 means no timeout.
2337					 default is 1.
2338		--with-valgrind: run a test case under valgrind
2339			       If this option given, DURATION is changed to
2340			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
2341EOF
2342}
2343
2344action_chop ()
2345{
2346    if [ "$1" = "chop" ]; then
2347	action_fuzz_common chop_lang "$@"
2348    else
2349	action_fuzz_common slap_lang "$@"
2350    fi
2351}
2352
2353chop_lang()
2354{
2355    chop_lang_common "tail" "$@"
2356}
2357
2358slap_lang()
2359{
2360    chop_lang_common "head" "$@"
2361}
2362
2363chop_lang_common ()
2364{
2365    local endpoint=$1
2366    shift 1
2367
2368    local lang="$1"
2369    local dir="$2"
2370    shift 2
2371    local f
2372    local r
2373
2374    printf '%-60s\n' "Fuzzing by truncating input from ${endpoint} (${lang})"
2375    line '-'
2376
2377    r=0
2378    for f in $(find "${dir}" -type f -name 'input.*'); do
2379	if ! chop_lang_file "$1" "${lang}" "${f}"; then
2380	    r=1
2381	    break
2382	fi
2383    done
2384    echo
2385    return $r
2386}
2387
2388chop_lang_file ()
2389{
2390    local endpoint=$1
2391    shift 1
2392
2393    local lang="$1"
2394    local input="$2"
2395    shift 2
2396
2397    local r
2398    local cmdline
2399    local cmdline_for_shirking
2400    local len=$(${_FSIZE} "${input}")
2401
2402    local guessed_lang
2403    guessed_lang=$( ${_CMDLINE_FOR_SHRINKING} --print-language "${input}" 2>/dev/null | sed -n 's/^.*: //p')
2404    if [ "${lang}" !=  "${guessed_lang}" ]; then
2405	return 0
2406    fi
2407
2408    i=0
2409    echo "Testing cases derived from: ${input}"
2410    line '.' --no-newline
2411
2412    r=0
2413    while [ "$i" -lt "$len" ]; do
2414	if chop_lang_file_chopspec "${endpoint}" "${input}" "${len}" "$i" "${lang}"; then
2415	    i=$(( i + 1 ))
2416	else
2417	    r=1
2418	    break
2419	fi
2420    done
2421    echo
2422    return $r
2423}
2424
2425chop()
2426{
2427    local endpoint=$1
2428    local input=$2
2429    local pos=$3
2430    local len=$4
2431
2432    if [ "${endpoint}" = "tail" ]; then
2433	dd if=$input bs=1 count=$pos
2434    else
2435	dd if=$input bs=1 count=$((len - pos)) skip=$pos
2436    fi
2437}
2438
2439chop_lang_file_chopspec()
2440{
2441    local endpoint=$1
2442    shift 1
2443
2444    local input="$1"
2445    local len="$2"
2446    local pos="$3"
2447    local lang="$4"
2448    shift 4
2449
2450    local dir="${input%/*}"
2451    local ochopped=$(printf "%s/CHOP-INPUT-%s.tmp" "${dir}" "$pos")
2452    local ocmdline=$(printf "%s/CHOP-CMDLINE-%s.tmp" "${dir}" "$pos")
2453    local ovalgrind=$(printf "%s/CHOP-VALGRIND-%s.tmp" "${dir}" "$pos")
2454    local oshrink=$(printf "%s/CHOP-SHRINK-%s.tmp" "${dir}" "$pos")
2455
2456    local cmdline
2457    local cmdline_for_shirking
2458    local progress_offset
2459    local r
2460
2461    rm -f "${ocmdline}" "${ovalgrind}" "${ochopped}" "${oshrink}"
2462
2463    if [ "${WITH_VALGRIND}" = 'yes' ]; then
2464	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${ochopped}" "${ovalgrind}" )
2465    else
2466	cmdline="${_CMDLINE} --language-force=${lang} ${ochopped}"
2467    fi
2468    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"
2469
2470    chop "${endpoint}" "${input}" "${pos}" "${len}" > "$ochopped"  2> /dev/null
2471
2472    progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
2473    if [ "${progress_offset}" -eq 0 ]; then
2474	[ $pos -gt 0 ] && printf " %d/%d" "$pos" "${len}"
2475	echo
2476    fi
2477
2478    echo "${cmdline}" > "${ocmdline}"
2479    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
2480    r=$?
2481    case $r in
2482	0)
2483	    printf 'o'
2484	    noise_report_line "${pos}" "${len}" "${r}" ""
2485	    rm "${ochopped}"
2486	    rm -f "${ovalgrind}" "${ocmdline}"
2487	    ;;
2488	${_TIMEOUT_EXIT})
2489	    printf "T"
2490	    noise_report_line "${pos}" "${len}" "${r}" ""
2491	    printf '\n%-20s\n' "[timeout $lang]" "$ochopped"
2492	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${ochopped}" "${oshrink}" "${lang}"
2493	    ;;
2494	${_VALGRIND_EXIT})
2495	    printf "V"
2496	    noise_report_line "${pos}" "${len}" "${r}" ""
2497	    printf '\n%-20s %s\n' "[valgrind-error $lang]" "$ochopped"
2498	    ;;
2499	*)
2500	    printf "!"
2501	    noise_report_line "${pos}" "${len}" "${r}" ""
2502	    printf '\n%-20s %s\n' "[unexpected-status($r) $lang]" "$ochopped"
2503	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${ochopped}" "${oshrink}" "${lang}"
2504	    ;;
2505    esac
2506    return $r
2507}
2508
2509help_validate_input ()
2510{
2511    cat <<EOF
2512$0 validate-input [OPTIONS] UNITS-DIR VALIDATORS-DIR
2513
2514	Validate the input files (only for the test cases specifying validators.)
2515
2516	OPTIONS:
2517		--validators=validator[,...]: Validate test cases specifying
2518					    given validators.
2519		--colorized-output: yes|no: print the result in color.
2520EOF
2521}
2522
2523has_validator_acceptable_name ()
2524{
2525    local validator=$1
2526    if [ -z "${validator}" ]; then
2527	return 1
2528    fi
2529
2530    echo "${validator}" | grep -q "^[-a-zA-Z+#0-9]\+$"
2531}
2532
2533is_validator_runnable ()
2534{
2535    local v=$1
2536    local d=$2
2537    shift 2
2538
2539    "$d/"validator-"$v" is_runnable
2540}
2541
2542update_validator_list ()
2543{
2544    local type=$1
2545    local v=$2
2546
2547    case $type in
2548	runnable)
2549	    if ! member_p "$v" ${_RUNNABLE_VALIDATORS}; then
2550		_RUNNABLE_VALIDATORS="${_RUNNABLE_VALIDATORS} $v"
2551	    fi
2552	    ;;
2553	unavailable)
2554	    if ! member_p "$v" ${_UNAVAILABLE_VALIDATORS}; then
2555		_UNAVAILABLE_VALIDATORS="${_UNAVAILABLE_VALIDATORS} $v"
2556	    fi
2557	    ;;
2558	*)
2559	    ERROR 1 "INTERNAL ERROR: wrong validator list type: ${type}"
2560	    ;;
2561    esac
2562}
2563
2564#
2565# Whether a validator can be run or not.
2566#
2567# This runs "is_runnable" subcommand of the validator.
2568#
2569validate_validator ()
2570{
2571    local v=$1
2572    local validators_dir=$2
2573    local make_error=$3
2574
2575    if member_p "$v" ${_RUNNABLE_VALIDATORS}; then
2576	return 0
2577    elif member_p "$v" ${_UNAVAILABLE_VALIDATORS}; then
2578	return 1
2579    fi
2580
2581    if ! has_validator_acceptable_name "$v"; then
2582	if [ "${make_error}" = "error" ]; then
2583	    ERROR 1 "Unacceptable validator name: $v"
2584	else
2585	    update_validator_list unavailable "$v"
2586	    return 1
2587	fi
2588    elif ! [ -f "${validators_dir}/validator-$v" ]; then
2589	if [ "${make_error}" = "error" ]; then
2590	    ERROR 1 "No such validator: $v (${validators_dir}/validator-$v)"
2591	else
2592	    update_validator_list unavailable "$v"
2593	    return 1
2594	fi
2595    elif ! [ -x "${validators_dir}/validator-$v" ]; then
2596	if [ "$make_error" = "error" ]; then
2597	    ERROR 1 "Not executable: $v (${validators_dir}/validator-$v)"
2598	else
2599	    update_validator_list unavailable "$v"
2600	    return 1
2601	fi
2602    elif ! is_validator_runnable "$v" "${validators_dir}"; then
2603	if [ "${make_error}" = "error" ]; then
2604	    ERROR 1 "$v (${validators_dir}/validator-$v) is not ready to run"
2605	else
2606	    update_validator_list unavailable "$v"
2607	    return 1
2608	fi
2609    fi
2610
2611    update_validator_list runnable "$v"
2612    return 0
2613}
2614
2615#
2616# Choose a validator suitable for the current context.
2617#
2618# Return value
2619# 0: The caller should run echo'ed validator.
2620#
2621# 1: The caller should skip the input. It means
2622#    - the suitable validator is not listed in VALIDATORS, or
2623#    - no expected.tags and no validator file exist.
2624#    The caller doesn't have to update any validation counters.
2625# 2: The caller should skip the input and update the unavailable
2626#    counter.
2627#
2628resolve_validator ()
2629{
2630    local validator_file=$1
2631    local default_validator=$2
2632    local has_expected_tags=$3
2633    local validators_dir=$4
2634
2635    shift 4
2636
2637    local candidate_validator
2638    local local_validator
2639
2640    if [ -r "$validator_file" ]; then
2641	local_validator=$(cat "${validator_file}" | grep -v '#')
2642	if [ -z "${local_validator}" ]; then
2643	    ERROR 1 "Empty validator specfile: ${local_validator}"
2644	else
2645	    candidate_validator=${local_validator}
2646	fi
2647    elif [ "$has_expected_tags" = no ]; then
2648	return 1
2649    else
2650	candidate_validator=${default_validator}
2651    fi
2652
2653    if [ -z "${candidate_validator}" ]; then
2654	return 2
2655    elif [ -z "${VALIDATORS}" ] || member_p "${candidate_validator}" ${VALIDATORS}; then
2656	echo "${candidate_validator}"
2657	if validate_validator "${candidate_validator}" "${validators_dir}" noerror; then
2658	    return 0
2659	else
2660	    return 2
2661	fi
2662    else
2663	return 1
2664    fi
2665}
2666
2667#
2668# Report the result of validation for INPUT with decoration
2669# The validation result counters are updated here.
2670#
2671validate_report_one ()
2672{
2673    local input=$1
2674    local validator=$2
2675    local status=$3
2676    shift 3
2677
2678    if [ "${validator}" = "${_NOOP_VALIDATOR}" ]; then
2679	return
2680    fi
2681
2682    local i=$(basename $input)
2683    local d=$(basename $(dirname $input))
2684
2685    printf '%-65s' "$d/$i with ${validator}"
2686    case "${status}" in
2687	valid)
2688	    if [ "${validator}" = "${_KNOWN_INVALIDATION_VALIDATOR}" ]; then
2689		printf '%b\n' $(decorate yellow "known-invalidation")
2690		V_SKIP_KNOWN_INVALIDATION=$(( V_SKIP_KNOWN_INVALIDATION + 1 ))
2691	    else
2692		printf '%b\n' $(decorate green "valid")
2693		V_VALID=$(( V_VALID + 1))
2694	    fi
2695	    ;;
2696	invalid)
2697	    printf '%b\n' $(decorate red "invalid")
2698	    V_INVALID=$(( V_INVALID + 1))
2699	    ;;
2700	unavailable)
2701	    printf '%b\n' $(decorate yellow "unavailable")
2702	    V_SKIP_VALIDATOR_UNAVAILABLE=$(( V_SKIP_VALIDATOR_UNAVAILABLE + 1))
2703	    ;;
2704	*)
2705	    ERROR 1 "INTERNAL ERROR: wrong validation status: ${status}"
2706	    ;;
2707    esac
2708}
2709
2710#
2711# Run a validator for the given input
2712#
2713# This runs "validate" subcommand of the validator.
2714#
2715validate_file ()
2716{
2717    local input=$1
2718    local validator=$2
2719    local validators_dir=$3
2720    shift 3
2721
2722    ${validators_dir}/validator-${validator} validate $input
2723    return $?
2724}
2725
2726#
2727# Validate input files under *.[dbtiv].
2728#
2729validate_dir ()
2730{
2731    local base_dir=$1
2732    local default_validator=$2
2733    local validators_dir=$3
2734    shift 3
2735
2736    local f
2737    local t
2738
2739    local v0 s0 inputs0
2740    local v s inputs
2741
2742    #
2743    # No expected.tags* implies the input is invalid.
2744    # We don't have to run any validator as far as no
2745    # ./validator explicitly is given.
2746    #
2747    local has_expected_tags=no
2748    for f in "${base_dir}"/expected.tags*; do
2749	[ -r "$f" ] || continue
2750	failure_in_globing "$f" && continue
2751	has_expected_tags=yes
2752	break
2753    done
2754
2755    # A validator specified in ./validator is used for validating ./input.foo.
2756    # It will be used for validating ./input[-_]*.foo, too if ./validator[-_]*.
2757    # doesn't exit.
2758    inputs0=$(for f in "${base_dir}"/input.*; do
2759		  [ -r "$f" ] || continue
2760		  failure_in_globing "$f" && continue
2761		  echo $f
2762	      done | sort)
2763    v0=$(resolve_validator "${base_dir}"/validator \
2764			  "${default_validator}" \
2765			  "${has_expected_tags}" \
2766			  "${validators_dir}")
2767    s0=$?
2768
2769    case "$s0" in
2770	0)
2771	    update_validator_list runnable "$v0"
2772	    for f in $inputs0; do
2773		if validate_file "$f" "$v0" "${validators_dir}"; then
2774		    validate_report_one "$f" "$v0" valid
2775		else
2776		    validate_report_one "$f" "$v0" invalid
2777		fi
2778	    done
2779	    default_validator=$v0
2780	    ;;
2781	1)
2782	    # no action needed
2783	    ;;
2784	2)
2785	    update_validator_list unavailable "$v0"
2786	    for f in $inputs0; do
2787		validate_report_one "$f" "$v0" unavailable
2788	    done
2789	    default_validator=
2790	    ;;
2791    esac
2792
2793    inputs=$(for f in "${base_dir}"/input[-_][0-9].* \
2794		      "${base_dir}"/input[-_][0-9][-_]*.*; do
2795		 [ -r "$f" ] || continue
2796		 failure_in_globing "$f" && continue
2797		 echo $f
2798	     done | sort)
2799
2800    for f in $inputs; do
2801	t=$(basename $f)
2802	t=${t#input}; t=${t%.*}
2803	v=$(resolve_validator "${base_dir}"/validator"$t" \
2804			     "$default_validator" \
2805			     "$has_expected_tags" \
2806			     "$validators_dir")
2807	s=$?
2808	case "$s" in
2809	    0)
2810		update_validator_list runnable "$v0"
2811		if validate_file "$f" "$v" "${validators_dir}"; then
2812		    validate_report_one "$f" "$v" valid
2813		else
2814		    validate_report_one "$f" "$v" invalid
2815		fi
2816		;;
2817	    1)
2818		# no action needed
2819		;;
2820	    2)
2821		update_validator_list unavailable "$v0"
2822		validate_report_one "$f" "$v" unavailable
2823		;;
2824	esac
2825    done
2826}
2827
2828#
2829# Validate input files under *.r.
2830#
2831validate_category ()
2832{
2833    local category=$1
2834    local base_dir=$2
2835    local validators_dir=$3
2836    shift 3
2837
2838    local d
2839    local default_validator=${_NOOP_VALIDATOR}
2840
2841    if [ -r "${base_dir}/validator" ]; then
2842	default_validator=$(cat "${base_dir}/validator" | grep -v '#')
2843	if [ -z "${default_validator}" ]; then
2844	    ERROR 1 "Empty validator specfile (in ${base_dir}/validator)"
2845	fi
2846
2847	validate_validator "${default_validator}" "${validators_dir}" noerror
2848    fi
2849
2850    for d in "${base_dir}"/*.[dbtiv]; do
2851	[ -d "$d" ] || continue
2852	validate_dir "$d" "${default_validator}" "${validators_dir}"
2853    done
2854}
2855
2856#
2857# Report the summary of validations
2858#
2859validate_summary ()
2860{
2861    echo
2862    echo "Summary"
2863    line
2864
2865    printf '  %-40s' "#valid:"
2866    printf '%b\n' $(decorate green "${V_VALID}")
2867
2868    printf '  %-40s' "#invalid:"
2869    if [ "${V_INVALID}" = 0 ]; then
2870	echo "${V_INVALID}"
2871    else
2872	printf '%b\n' $(decorate red "${V_INVALID}")
2873    fi
2874
2875    printf '  %-40s' "#skipped (known invalidation)"
2876    if [ "${V_SKIP_KNOWN_INVALIDATION}" = 0 ]; then
2877	echo 0
2878    else
2879	printf '%b\n' $(decorate yellow "${V_SKIP_KNOWN_INVALIDATION}")
2880    fi
2881
2882    printf '  %-40s' "#skipped (validator unavailable)"
2883    if [ "${V_SKIP_VALIDATOR_UNAVAILABLE}" = 0 ]; then
2884	echo 0
2885    else
2886	local u
2887	printf '%b\n' $(decorate yellow "${V_SKIP_VALIDATOR_UNAVAILABLE}")
2888
2889	echo
2890	echo "Unavailable validators"
2891	line
2892	for u in ${_UNAVAILABLE_VALIDATORS}; do
2893	    echo "	$u"
2894	done
2895    fi
2896
2897    if [ "${V_INVALID}" = 0 ]; then
2898	return 0
2899    else
2900	return "${_VALIDATION_EXIT_INVALID}"
2901    fi
2902}
2903
2904action_validate_input ()
2905{
2906    local action=$1
2907    shift
2908
2909    local units_dir
2910    local validators_dir
2911    local validators
2912
2913    local v
2914    local d
2915
2916    while [ $# -gt 0 ]; do
2917	case $1 in
2918	    --validators)
2919		shift
2920		validators=$1
2921		shift
2922		;;
2923	    --validators=*)
2924		validators=${1#--validators=}
2925		shift
2926		;;
2927	    --colorized-output)
2928		shift
2929		COLORIZED_OUTPUT=$1
2930		shift
2931		;;
2932	    --colorized-output=*)
2933		COLORIZED_OUTPUT=${1#--colorized-output=}
2934		shift
2935		;;
2936	    -*)
2937		ERROR 1 "unknown option \"${1}\" for ${action} action"
2938		;;
2939	    *)
2940		units_dir=$1
2941		shift
2942		validators_dir=$1
2943		shift
2944		break;
2945		;;
2946	esac
2947    done
2948
2949    if [ $# -gt 0 ]; then
2950	ERROR 1 "too many arguments for ${action} action: $*"
2951    elif [ -z "${units_dir}" ]; then
2952	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
2953    elif [ -z "${validators_dir}" ]; then
2954	ERROR 1 "VALIDATORS_DIR parameter is not given in ${action} action"
2955    fi
2956
2957    if ! [ -d "${units_dir}" ]; then
2958	ERROR 1 "No such directory: ${units_dir}"
2959    elif ! [ -d "${validators_dir}" ]; then
2960	ERROR 1 "No such directory: ${units_dir}"
2961    fi
2962
2963    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
2964	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
2965    fi
2966
2967    if [ -n "${validators}" ]; then
2968	VALIDATORS=$(echo "${validators}" | tr ',' ' ')
2969	for v in ${VALIDATORS}; do
2970	    validate_validator "$v" "${validators_dir}" error
2971	done
2972    fi
2973
2974    for d in ${units_dir}/*.r; do
2975	[ -d "$d" ] || continue
2976	category=${d##*/}
2977	echo
2978	echo "Category: ${category}"
2979	line
2980	validate_category "${category}" "$d" "${validators_dir}"
2981    done
2982
2983    echo
2984    echo "Category: ${_DEFAULT_CATEGORY}"
2985    line
2986    for d in ${units_dir}/*.[dbtiv]; do
2987	[ -d "$d" ] || continue
2988	validate_dir "$d" "${_NOOP_VALIDATOR}" "$validators_dir"
2989    done
2990
2991    validate_summary
2992    return $?
2993}
2994
2995# * Avoid issues between sed and the locale settings by overriding it using
2996#   LC_ALL, which takes precedence over all other locale configurations:
2997#   https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
2998#
2999# * Avoid unexpected pathname conversion on MSYS2.
3000#   https://github.com/msys2/msys2/wiki/Porting#filesystem-namespaces
3001prepare_environment ()
3002{
3003    _PREPERE_ENV=$(cat <<'EOF'
3004LC_ALL="C"; export LC_ALL
3005MSYS2_ARG_CONV_EXCL='--regex-;--_scopesep' export MSYS2_ARG_CONV_EXCL
3006EOF
3007)
3008    eval ${_PREPERE_ENV}
3009}
3010
3011main ()
3012{
3013    if [ $# = 0 ]; then
3014	action_help 1>&2
3015	exit 1
3016    fi
3017
3018    case $1 in
3019	help|-h|--help)
3020	    action_help
3021	    return 0
3022	    ;;
3023	run)
3024	    action_run "$@"
3025	    return $?
3026	    ;;
3027	clean)
3028	    action_clean "$@"
3029	    return $?
3030	    ;;
3031	fuzz)
3032	    action_fuzz "$@"
3033	    return $?
3034	    ;;
3035	noise)
3036	    action_noise "$@"
3037	    return $?
3038	    ;;
3039	tmain)
3040	    action_tmain "$@"
3041	    return $?
3042	    ;;
3043	clean-tmain)
3044	    action_clean_tmain "$@"
3045	    return $?
3046	    ;;
3047	shrink)
3048	    action_shrink "$@"
3049	    return $?
3050	    ;;
3051	chop)
3052	    action_chop "$@"
3053	    return $?
3054	    ;;
3055	slap)
3056	    action_chop "$@"
3057	    return $?
3058	    ;;
3059	validate-input)
3060	    action_validate_input "$@"
3061	    return $?
3062	    ;;
3063	*)
3064	    ERROR 1 "unknown action: $1"
3065	    ;;
3066    esac
3067}
3068
3069prepare_environment
3070main "$@"
3071exit $?
3072