#!/bin/sh
#
# units - Units test harness for ctags
#
# Copyright (C) 2014 Masatake YAMATO
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
if test -n "${ZSH_VERSION+set}"; then
    set -o SH_WORD_SPLIT
    set +o NOMATCH
fi

#
# Global Parameters
#
SHELL=/bin/sh
CTAGS=./ctags
READTAGS=./readtags
WITH_TIMEOUT=
WITH_VALGRIND=
COLORIZED_OUTPUT=yes
[ -f /dev/stdout ] && COLORIZED_OUTPUT=no
CATEGORIES=
UNITS=
LANGUAGES=
PRETENSE_OPTS=
RUN_SHRINK=
QUIET=
SHOW_DIFF_OUTPUT=
VALIDATORS=

#
# Internal variables and constants
#
_CMDLINE=
_CMDLINE_FOR_SHRINKING=
_PREPERE_ENV=
readonly _DEFAULT_CATEGORY=ROOT
readonly _TIMEOUT_EXIT=124
readonly _VG_TIMEOUT_FACTOR=10
readonly _VALGRIND_EXIT=58
readonly _BASH_INTERRUPT_EXIT=59
readonly _LINE_SPLITTER=$(if type dos2unix > /dev/null 2>&1; then echo "dos2unix"; else echo "cat"; fi)
readonly _STDERR_OUTPUT_NAME="STDERR.tmp"
readonly _DIFF_OUTPUT_NAME="DIFF.tmp"
readonly _NOISE_REPORT_MAX_COLUMN=50
readonly _VALIDATION_EXIT_INVALID=2
readonly _NOOP_VALIDATOR="NONE"
readonly _KNOWN_INVALIDATION_VALIDATOR="KNOWN-INVALIDATION"

_RUNNABLE_VALIDATORS=
_UNAVAILABLE_VALIDATORS=

#
# Results
#
L_PASSED=
L_FIXED=
L_FAILED_BY_STATUS=
L_FAILED_BY_DIFF=
L_SKIPPED_BY_FEATURES=
L_SKIPPED_BY_LANGUAGES=
L_SKIPPED_BY_ILOOP=
L_KNOWN_BUGS=
L_FAILED_BY_TIMEED_OUT=
L_BROKEN_ARGS_CTAGS=

V_VALID=0
V_INVALID=0
V_SKIP_VALIDATOR_UNAVAILABLE=0
V_SKIP_KNOWN_INVALIDATION=0

#
# TODO
#
#  * write new class 'r' (category directory) to units.rst.
#  * write new class 'v' (skip the checking by valgrind) to units.rst.
#
action_help ()
{
    cat <<EOF
Usage:
	$(help_help)

	$(help_run)

	$(help_clean)

	$(help_fuzz)

	$(help_shrink)

	$(help_noise)

	$(help_tmain)

	$(help_chop)

	$(help_validate_input)

	$(help_clean_tmain)
EOF
}

help_help()
{
    echo "$0 help|--help"
}

ERROR ()
{
    local status_="$1"
    local msg="$2"
    shift 2
    echo "$msg" 1>&2
    exit $status_
}

line()
{
    local c=${1:--}
    local no_newline="${2}"
    local i=0
    while [ $i -lt 60 ]; do
	printf "%c" "$c"
	i=$(( i + 1 ))
    done

    if ! [ "${no_newline}" = "--no-newline" ]; then
	echo
    fi
}

count_list ()
{
    echo $#
}

member_p ()
{
    local elt="$1"
    shift
    local x

    for x in "$@"; do
	if [ "$x" = "$elt" ]; then
	    return 0
	fi
    done

    return 1
}

clean_tcase ()
{
    local d="$1"
    local bundles="$2"
    local b

    if [ -d "$d" ]; then
	if [ -f "${bundles}" ]; then
	    while read b; do
		rm -rf "${b}"
	    done < "${bundles}"
	    rm ${bundles}
	fi
	rm -f "$d"/*.tmp "$d"/*.TMP
    fi
}

check_availability()
{
    local cmd="$1"
    shift
    type "${cmd}" > /dev/null 2>&1 || ERROR 1 "${cmd} command is not available"
}

check_units ()
{
    local name="$1"
    local category="$2"
    shift 2
    local u

    for u in "$@"; do
	if echo "${u}" | grep -q /; then
	    if [ "${u%/*}" = "${category}" ] && [ "${u#*/}" = "${name}" ]; then
		return 0
	    fi
	elif [ "${u}" = "${name}" ]; then
	    return 0
	fi
    done
    return 1
}

check_features()
{
    local flag="$1"
    local ffile
    local feature

    if [ "${flag}" = "-f" ]; then
	ffile="$2"
    elif [ "${flag}" = "-e" ]; then
	feature="$2"
    fi
    shift 2

    local f
    local found
    local found_unexpectedly
    local expected;


    for expected in $([ -f "$ffile" ] && cat "$ffile") ${feature}; do
	    found=no
	    found_unexpectedly=no
	    for f in $( ${CTAGS} --quiet --options=NONE \
				 --list-features --with-list-header=no \
				 2> /dev/null \
			    | "${_LINE_SPLITTER}" \
			    | cut -f 1 -d ' ') ; do
		[ "$expected" = "$f" ] && found=yes
		[ "$expected" = '!'"$f" ] && found_unexpectedly=yes
	    done
	    if [ "${found_unexpectedly}" = yes ]; then
		echo "$expected"
		return 1
	    elif ! [ "$found" = yes ]; then
		echo "$expected"
		return 1
	    fi
    done

    return 0
}

check_languages()
{
    local lfile="$1"
    shift

    local l
    local found
    local expected;


    #
    # TODO: consider the value of LANGUAGES
    #
    while read expected; do
	    found=no
	    for l in $( ${_CMDLINE} --list-languages 2>/dev/null | "${_LINE_SPLITTER}" |sed -e 's/ //' ); do
		[ "$expected" = "$l" ] && found=yes
	    done
	    if ! [ "$found" = yes ]; then
		echo "$expected"
		return 1
	    fi
    done < "$lfile"

    return 0
}

decorate ()
{
    local decorator="$1"
    local msg="$2"

    case "$decorator" in
	red)    decorator=31 ;;
	green)  decorator=32 ;;
	yellow) decorator=33 ;;
	*) ERROR 1 "INTERNAL ERROR: wrong run_result function: $f"
    esac

    if [ "${COLORIZED_OUTPUT}" = 'yes' ]; then
	printf '%b\n' "\033[${decorator}m${msg}\033[39m"
    else
	printf '%b\n' "${msg}"
    fi
}

run_result ()
{
    local result_type="$1"
    local output="$2"
    shift 2
    local f="run_result_${result_type}"
    local tmp

    type "$f" > /dev/null 2>&1 || ERROR 1 \
	"INTERNAL ERROR: wrong run_result function: $f"

    "$f" "$@"

    tmp="${COLORIZED_OUTPUT}"
    COLORIZED_OUTPUT=no
    "$f" "$@" > "${output}"
    COLORIZED_OUTPUT="${tmp}"
}

run_result_skip ()
{
    if [ -n "$1" ]; then
	printf '%b\n' $(decorate yellow "skipped")" ($1)"
    else
	printf '%b\n' $(decorate yellow "skipped")
    fi
}

run_result_error ()
{
    if [ ! -n "$1" ]; then
	printf '%b\n' $(decorate red "failed")
    else
	printf '%b\n' $(decorate red "failed")" ($1)"
    fi
}

run_result_ok ()
{
    if [ ! -n "$1" ]; then
	printf '%b\n' $(decorate green "passed")
    else
	printf '%b\n' $(decorate green "passed")" ($1)"
    fi
}

run_result_known_error ()
{
    printf '%b\n' $(decorate yellow "failed")" (KNOWN bug)"
}

run_shrink ()
{
    local cmdline_template="$1"
    local input="$2"
    local output="$3"
    local lang="$4"
    shift 4

    echo "Shrinking ${input} as ${lang}"
    shrink_main "${cmdline_template}" "${input}" "${output}"  1 yes
}

# filters out the directory prefix in a ctags input
ctags_basename_filter_regex='s%\(^[^	]\{1,\}	\)\(/\{0,1\}\([^/	]\{1,\}/\)*\)%\1%'
ctags_basename_filter()
{
    sed "${ctags_basename_filter_regex}"
}

etags_basename_filter_regex='s%.*\/\([[:print:]]\{1,\}\),\([0-9]\{1,\}$\)%\1,\2%'
etags_basename_filter()
{
    sed "${etags_basename_filter_regex}"
}

xref_basename_filter_regex='s%\(.*[[:digit:]]\{1,\} \)\([^ ]\{1,\}[^ ]\{1,\}\)/\([^ ].\{1,\}.\{1,\}$\)%\1\3%'
xref_basename_filter()
{
    sed "${xref_basename_filter_regex}"
}

json_basename_filter_regex='s%\("path": \)"[^"]\{1,\}/\([^/"]\{1,\}\)"%\1"\2"%'
json_basename_filter()
{
    sed "${json_basename_filter_regex}"
}

run_record_cmdline ()
{
    local ffilter="$1"
    local ocmdline="$2"

    printf "%s\n%s \\\\\n| %s \\\\\n| %s\n"  \
	"${_PREPERE_ENV}" \
	"${_CMDLINE}" \
	"sed '${tags_basename_filter_regex}'" \
	"${ffilter}" \
	> "${ocmdline}"
}

#
# All files and directories other than input.*, expected.tags,
# args.ctags, README*, features, languages, and filters under srcdir
# are copied to builddir. These copied files are called bundles.
#
prepare_bundles ()
{
    local from=$1
    local to=$2
    local obundles=$3
    local src
    local dist

    for src in ${from}/*; do
	if [ "${from}"'/*' = "${src}" ]; then
	    break
	fi
	case $(basename "$src") in
	    input.*)
		continue
		;;
	    expected.tags*)
		continue
		;;
	    README*)
		continue
		;;
	    features|languages|filters)
		continue
		;;
	    args.ctags)
		continue
		;;
	    *)
		dist="${to}/$(basename ${src})"
		if ! cp -a "${src}" "${to}"; then
		    ERROR 1 "failure in copying bundle file \"${src}\" to \"${to}\""
		else
		    echo ${dist} >> ${obundles}
		fi
		;;
	esac
    done
}

direq ()
{
    [ "$(cd ${1} && pwd)" = "$(cd ${2} && pwd)" ]
    return $?
}

anon_normalize ()
{
    local ctags=$1
    local input_actual

    if [ -n "$2" ]; then
	input_actual=$2
	shift 2

	# TODO: "Units" should not be hardcoded.
	local input_expected="./Units${input_actual#*/Units}"

	local actual=$(${CTAGS} --quiet --options=NONE --_anonhash="${input_actual}")
	local expected=$(${CTAGS} --quiet --options=NONE --_anonhash="${input_expected}")

	sed -e s/${actual}/${expected}/g | anon_normalize "${ctags}" "$@"
    else
	cat
    fi
}

run_tcase ()
{
    local input="$1"
    local t="$2"
    local name="$3"
    local class="$4"
    local category="$5"
    local build_t="$6"
    shift 6
    # The rest of arguments ($@) are extra inputs

    # I violate the naming convention of build_* to reduce typing
    local o=${build_t}

    local fargs="$t/args.ctags"
    local ffeatures="$t/features"
    local flanguages="$t/languages"
    local ffilter="$t/filter"

    #
    # tags-e if for etags output(-e). TAGS is good
    # suffix but foo.tags and foo.TAGS may be the same on Windows.
    # tags-x is for cross reference output(-x).
    # tags-json is for json output.
    #
    # fexpected must be set even if none of
    # expected.{tags,tags-e,tags-x,tags-json} exits.
    #
    local fexpected="$t/expected.tags"
    local output_type=ctags
    local output_label=
    local output_tflag=
    local output_feature=
    local output_lang_extras=

    if [ -f "$t/expected.tags" ]; then
	:
    elif [ -f "$t/expected.tags-e" ]; then
	fexpected=$t/expected.tags-e
	output_type=etags
	output_label=/${output_type}
	output_tflag="-e  --tag-relative=no"
    elif [ -f "$t/expected.tags-x" ]; then
	fexpected=$t/expected.tags-x
	output_type=xref
	output_label=/${output_type}
	output_tflag=-x
    elif [ -f "$t/expected.tags-json" ]; then
	fexpected=$t/expected.tags-json
	output_type=json
	output_label=/${output_type}
	output_tflag="--output-format=json"
	output_feature=json
    fi

    if [ $# -gt 0 ]; then
	output_lang_extras=" (multi inputs)"
    fi

    [ -x "$ffilter" ] || ffilter=cat

    #
    # All generated file must have suffix ".tmp".
    #
    local ostderr="$o/${_STDERR_OUTPUT_NAME}"
    local orawout="$o/RAWOUT.tmp"
    local ofiltered="$o/FILTERED.tmp"
    local odiff="$o/${_DIFF_OUTPUT_NAME}"
    local ocmdline="$o/CMDLINE.tmp"
    local ovalgrind="$o/VALGRIND.tmp"
    local oresult="$o/RESULT.tmp"
    local oshrink_template="$o/SHRINK-%s.tmp"
    local obundles="$o/BUNDLES"
    local oshrink

    local guessed_lang
    local guessed_lang_no_slash
    local cmdline_template
    local timeout_value
    local tmp

    local broke_args_ctags

    #
    # Filtered by UNIT
    #
    if [ -n "${UNITS}" ]; then
	check_units "${name}" "${category}" ${UNITS} || return 1
    fi

    #
    # Build _CMDLINE
    #
    _CMDLINE="${CTAGS} --verbose --options=NONE $PRETENSE_OPTS --optlib-dir=+$t/optlib -o -"
    [ -f "${fargs}" ] && _CMDLINE="${_CMDLINE} --options=${fargs}"

    if [ -f "${fargs}" ] && ! ${_CMDLINE} --_force-quit=0 > /dev/null 2>&1; then
	broke_args_ctags=1
    fi

    #
    # Filtered by LANGUAGES
    #
    guessed_lang=$( ${_CMDLINE} --print-language "$input" 2>/dev/null | sed -n 's/^.*: //p')
    if [ -n "${LANGUAGES}" ]; then
	member_p "${guessed_lang}" ${LANGUAGES} || return 1
    fi
    guessed_lang_no_slash=$(echo "${guessed_lang}" | tr '/' '-')
    oshrink=$(printf "${oshrink_template}" "${guessed_lang_no_slash}")

    clean_tcase "${o}" "${obundles}"
    mkdir -p "${o}"
    if ! direq "${o}" "${t}"; then
	prepare_bundles ${t} ${o} "${obundles}"
    fi


    printf '%-60s' "Testing ${name} as ${guessed_lang}${output_lang_extras}${output_label}"

    if tmp=$( ( [ -n "${output_feature}" ] && ! check_features -e "${output_feature}" ) ||
	      ( [ -f "${ffeatures}" ] && ! check_features -f "${ffeatures}" ) ); then
	L_SKIPPED_BY_FEATURES="$L_SKIPPED_BY_FEATURES ${category}/${name}"
	case "${tmp}" in
	    !*) run_result skip "${oresult}" "unwanted feature \"${tmp#?}\" is available";;
	    *)  run_result skip "${oresult}" "required feature \"${tmp}\" is not available";;
	esac
	return 1
    elif [ -f "${flanguages}" ] && ! tmp=$(check_languages "${flanguages}"); then
	L_SKIPPED_BY_LANGUAGES="$L_SKIPPED_BY_LANGUAGES ${category}/${name}"
	run_result skip "${oresult}" "required language parser \"$tmp\" is not available"
	return 1
    elif [ "$WITH_TIMEOUT" = 0 ] && [ "${class}" = 'i' ]; then
	L_SKIPPED_BY_ILOOP="$L_SKIPPED_BY_ILOOP ${category}/${name}"
	run_result skip "${oresult}" "may cause an infinite loop"
	return 1
    elif [ "$broke_args_ctags" = 1 ]; then
	run_result error '/dev/null' "broken args.ctags?"
	L_BROKEN_ARGS_CTAGS="${L_BROKEN_ARGS_CTAGS} ${category}/${name}/"
	return 1
    fi

    cmdline_template="${_CMDLINE} --language-force=${guessed_lang} %s > /dev/null 2>&1"
    _CMDLINE="${_CMDLINE} ${output_tflag} ${input} $@"

    timeout_value=$WITH_TIMEOUT
    if [ "$WITH_VALGRIND" = yes ]; then
	_CMDLINE="valgrind --leak-check=full --error-exitcode=${_VALGRIND_EXIT} --log-file=${ovalgrind} ${_CMDLINE}"
	timeout_value=$(( timeout_value * ${_VG_TIMEOUT_FACTOR} ))
    fi

    if ! [ "$timeout_value" = 0 ]; then
	_CMDLINE="timeout $timeout_value ${_CMDLINE}"
    fi

    {
	(
	    #
	    # When a launched process is exited abnormally, the parent shell reports it
	    # to stderr: See j_strsignal function call in wait_for in bash-4.2/nojobs.c.
	    # This becomes noise; close the stderr of subshell.
	    #
	    exec  2>&-;
	    #
	    # The original bug report(#1100 by @techee):
	    # --------------------------------------------------------------------------
	    # When running
	    #
	    #     make units VG=1
	    #
	    # one cannot stop its execution by pressing Ctrl+C and
	    # there doesn't seem to be any way (except for looking at
	    # processes which run and killing them) to stop its
	    # execution.
	    #
	    trap "exit ${_BASH_INTERRUPT_EXIT}" INT;
	    ${_CMDLINE} 2> "${ostderr}" > "${orawout}"
	)
	tmp="$?"
	run_record_cmdline "${ffilter}" "${ocmdline}"
    }
    if [ "$tmp" != 0 ]; then
	if [ "${tmp}" = "${_BASH_INTERRUPT_EXIT}" ]; then
	    ERROR 1 "The execution is interrupted"
	elif ! [ "$WITH_TIMEOUT" = 0 ] && [ "${tmp}" = "${_TIMEOUT_EXIT}" ]; then
	    L_FAILED_BY_TIMEED_OUT="${L_FAILED_BY_TIMEED_OUT} ${category}/${name}"
	    run_result error "${oresult}" "TIMED OUT"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    [ "${RUN_SHRINK}" = 'yes' ] \
		&& [ $# -eq 0 ] \
		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
	    return 1
	elif [ "$WITH_VALGRIND" = 'yes' ] && [ "${tmp}" = "${_VALGRIND_EXIT}" ] && ! [ "${class}" = v ]; then
	    L_VALGRIND="${L_VALGRIND} ${category}/${name}"
	    run_result error "${oresult}" "valgrind-error"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    return 1
	elif [ "$class" = 'b' ]; then
	    L_KNOWN_BUGS="$L_KNOWN_BUGS ${category}/${name}"
	    run_result known_error "${oresult}"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    [ "${RUN_SHRINK}" = 'yes' ] \
		&& [ $# -eq 0 ] \
		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
	    return 0
	else
	    L_FAILED_BY_STATUS="$L_FAILED_BY_STATUS ${category}/${name}"
	    run_result error  "${oresult}" "unexpected exit status: $tmp"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    [ "${RUN_SHRINK}" = 'yes' ] \
		&& [ $# -eq 0 ] \
		&& run_shrink "${cmdline_template}" "${input}" "${oshrink}" "${guessed_lang}"
	    return 1
	fi
    elif [ "$WITH_VALGRIND" = 'yes' ] && [ "$class" = 'v' ]; then
	L_FIXED="$L_FIXED ${category}/${name}"
    fi

    if ! [ -f "${fexpected}" ]; then
	clean_tcase "${o}" "${obundles}"
	if [ "$class" = 'b' ]; then
	    L_FIXED="$L_FIXED ${category}/${name}"
	elif [ "$class" = 'i' ]; then
	    L_FIXED="$L_FIXED ${category}/${name}"
	fi
	L_PASSED="$L_PASSED ${category}/${name}"
	run_result ok '/dev/null' "\"expected.tags*\" not found"
	return 0
    fi

    ${output_type}_basename_filter < "${orawout}" | \
	anon_normalize "${CTAGS}" "${input}" "$@" | \
	$ffilter > "${ofiltered}"

    {
	diff -U 0 -I '^!_TAG' --strip-trailing-cr "${fexpected}" "${ofiltered}" > "${odiff}"
	tmp="$?"
    }
    if [ "${tmp}" = 0 ]; then
	clean_tcase "${o}" "${obundles}"
	if [ "${class}" = 'b' ]; then
	    L_FIXED="$L_FIXED ${category}/${name}"
	elif ! [ "$WITH_TIMEOUT" = 0 ] && [ "${class}" = 'i' ]; then
	    L_FIXED="$L_FIXED ${category}/${name}"
	fi

	L_PASSED="$L_PASSED ${category}/${name}"
	run_result ok '/dev/null'
	return 0
    else
	if [ "${class}" = 'b' ]; then
	    L_KNOWN_BUGS="$L_KNOWN_BUGS ${category}/${name}"
	    run_result known_error "${oresult}"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    return 0
	else
	    L_FAILED_BY_DIFF="$L_FAILED_BY_DIFF ${category}/${name}"
	    run_result error "${oresult}" "unexpected output"
	    run_record_cmdline "${ffilter}" "${ocmdline}"
	    return 1
	fi
    fi
}


unwanted_file ()
{
    local file=$1
    # ignore backup files
    if echo "$file" | grep -q '~$'; then
	return 0
    elif echo "$file" | grep -q '\*'; then
	return 0
    fi
    return 1
}

run_dir ()
{
    local category="$1"
    local base_dir="$2"
    local build_base_dir="$3"
    shift 3

    local tcase_dir
    local build_tcase_dir
    local input
    local name
    local class

    local extra_tmp
    local extra_inputs

    #
    # Filtered by CATEGORIES
    #
    if [ -n "$CATEGORIES" ] && ! member_p "${category}" $CATEGORIES; then
	return 1
    fi

    echo
    echo "Category: $category"
    line
    for input in ${base_dir}/*.[dbtiv]/input.*; do
	if unwanted_file "$input"; then
	    continue
	fi

	extra_inputs=$(for extra_tmp in $(dirname $input)/input[-_][0-9].* \
					$(dirname $input)/input[-_][0-9][-_]*.* ; do
	    if unwanted_file "$extra_tmp"; then
		continue
	    fi
	    echo "$extra_tmp"
	done | sort)

	tcase_dir="${input%/input.*}"
	build_tcase_dir="${build_base_dir}/${tcase_dir#${base_dir}/}"
	name="${tcase_dir%.[dbtiv]}"
	name="${name##*/}"
	class="${tcase_dir#*${name}.}"
	run_tcase "${input}" "${tcase_dir}" "${name}" "${class}" "${category}" "${build_tcase_dir}" ${extra_inputs}
    done

    return 0
}

run_show_diff_output ()
{
    local units_dir="$1"
    local t="$2"

    printf "	"
    line .
    sed -e 's/^.*$/	&/' ${units_dir}/${t}.*/${_DIFF_OUTPUT_NAME}
    echo
}

run_show_stderr_output ()
{
    local units_dir="$1"
    local t="$2"

    printf "	"
    line .
    sed -e 's/^.*$/	&/' ${units_dir}/${t}.*/${_STDERR_OUTPUT_NAME} | tail -50
    echo
}

run_summary ()
{
    local build_dir="${1}"
    local t

    echo
    echo "Summary (see CMDLINE.tmp to reproduce without test harness)"
    line

    printf '  %-40s' "#passed:"
    count_list $L_PASSED

    printf '  %-40s' "#FIXED:"
    count_list $L_FIXED
    for t in $L_FIXED; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    printf '  %-40s' "#FAILED (broken args.ctags?):"
    count_list $L_BROKEN_ARGS_CTAGS
    for t in $L_BROKEN_ARGS_CTAGS; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    printf '  %-40s' "#FAILED (unexpected-exit-status):"
    count_list $L_FAILED_BY_STATUS
    for t in $L_FAILED_BY_STATUS; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
	    run_show_stderr_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
	fi
    done

    printf '  %-40s' "#FAILED (unexpected-output):"
    count_list $L_FAILED_BY_DIFF
    for t in $L_FAILED_BY_DIFF; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
	    run_show_stderr_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
	    run_show_diff_output "${build_dir}" "${t#${_DEFAULT_CATEGORY}/}"
	fi
    done

    if ! [ "$WITH_TIMEOUT" = 0 ]; then
	printf '  %-40s' "#TIMED-OUT (${WITH_TIMEOUT}s)"
	count_list $L_FAILED_BY_TIMEED_OUT
	for t in $L_FAILED_BY_TIMEED_OUT; do
	    echo "	${t#${_DEFAULT_CATEGORY}/}"
	done
    fi

    printf '  %-40s' "#skipped (features):"
    count_list $L_SKIPPED_BY_FEATURES
    for t in $L_SKIPPED_BY_FEATURES; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    printf '  %-40s' "#skipped (languages):"
    count_list $L_SKIPPED_BY_LANGUAGES
    for t in $L_SKIPPED_BY_LANGUAGES; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    if [ "$WITH_TIMEOUT" = 0 ]; then
	printf '  %-40s' "#skipped (infinite-loop):"
	count_list $L_SKIPPED_BY_ILOOP
	for t in $L_SKIPPED_BY_ILOOP; do
	    echo "	${t#${_DEFAULT_CATEGORY}/}"
	done
    fi

    printf '  %-40s' "#known-bugs:"
    count_list $L_KNOWN_BUGS
    for t in $L_KNOWN_BUGS; do
	echo "	${t#${_DEFAULT_CATEGORY}/}"
    done

    if [ "$WITH_VALGRIND" = yes ]; then
	printf '  %-40s' "#valgrind-error:"
	count_list $L_VALGRIND
	for t in $L_VALGRIND; do
	    echo "	${t#${_DEFAULT_CATEGORY}/}"
	done
    fi
}

make_pretense_map ()
{
    local ifs=$IFS
    local p
    local r

    IFS=,
    for p in $1; do
	newlang=${p%/*}
	oldlang=${p#*/}

	if [ -z "$newlang" ]; then
	    ERROR 1 "newlang part of --pretend option arg is empty"
	fi
	if [ -z "$oldlang" ]; then
	    ERROR 1 "oldlang part of --pretend option arg is empty"
	fi

	r="$r --_pretend-$newlang=$oldlang"
    done
    IFS=$ifs
    echo $r
}

action_run ()
{
    local action="$1"
    shift

    local units_dir
    local build_dir
    local d
    local build_d
    local category

    local c

    while [ $# -gt 0 ]; do
	case $1 in
	    --ctags)
		shift
		CTAGS="$1"
		shift
		;;
	    --ctags=*)
		CTAGS="${1#--ctags=}"
		shift
		;;
	    --categories)
		shift
		for c in $(echo "$1" | tr ',' ' '); do
		    CATEGORIES="$CATEGORIES ${c%.r}.r"
		done
		shift
		;;
	    --categories=*)
		for c in $(echo "${1#--categories=}" | tr ',' ' '); do
		    CATEGORIES="$CATEGORIES ${c%.r}.r"
		done
		shift
		;;
	    --units)
		shift
		UNITS=$(echo "$1" | tr ',' ' ')
		shift
		;;
	    --units=*)
		UNITS=$(echo "${1#--units=}" | tr ',' ' ')
		shift
		;;
	    --languages)
		shift
		LANGUAGES=$(echo "${1}" | tr ',' ' ')
		shift
		;;
	    --languages=*)
		LANGUAGES=$(echo "${1#--languages=}" | tr ',' ' ')
		shift
		;;
	    --with-timeout)
		shift
		WITH_TIMEOUT="$1"
		shift
		;;
	    --with-timeout=*)
		WITH_TIMEOUT="${1#--with-timeout=}"
		shift
		;;
	    --with-valgrind)
		shift
		WITH_VALGRIND=yes
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT="$1"
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT="${1#--colorized-output=}"
		shift
		;;
	    --run-shrink)
		RUN_SHRINK=yes
		shift
		;;
	    --show-diff-output)
		SHOW_DIFF_OUTPUT=yes
		shift
		;;
	    --with-pretense-map)
		shift
		PRETENSE_OPTS=$(make_pretense_map "$1")
		shift
		;;
	    --with-pretense-map=*)
		PRETENSE_OPTS=$(make_pretense_map "${1#--with-pretense-map=}")
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		units_dir="$1"
		shift
		build_dir="${1:-${units_dir}}"
		shift
		break
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$units_dir" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$units_dir" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    fi

    case "${build_dir}" in
	/*) ;;
	*) build_dir=$(pwd)/${build_dir} ;;
    esac

    if ! [ -d "$build_dir" ]; then
	ERROR 1 "No such directory(build_dir): ${build_dir}"
    fi

    if ! [ -f "${CTAGS}" ]; then
	ERROR 1 "no such file: ${CTAGS}"
    elif ! [ -e "${CTAGS}" ]; then
	ERROR 1 "${CTAGS} is not an executable file"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    : ${WITH_TIMEOUT:=0}
    [ "$WITH_TIMEOUT" = 0 ] || check_availability timeout
    [ "$WITH_VALGRIND" = 'yes' ] && check_availability valgrind
    [ "$MSYSTEM" != '' ] && check_availability dos2unix
    check_availability grep
    check_availability diff


    category="${_DEFAULT_CATEGORY}"
    if [ -z "$CATEGORIES" ] \
	|| ( [ -n "$CATEGORIES" ] && member_p "${category}" $CATEGORIES ); then
	run_dir "${category}" "${units_dir}" "${build_dir}"
    fi

    for d in ${units_dir}/*.r; do
	[ -d "$d" ] || continue
	category="${d##*/}"
	build_d=${build_dir}/${category}
	run_dir "${category}" "$d" "${build_d}"
    done

    run_summary "${build_dir}"

    if [ -n "${L_FAILED_BY_STATUS}" ] ||
	   [ -n "${L_FAILED_BY_DIFF}" ] ||
	   [ -n "${L_FAILED_BY_TIMEED_OUT}" ] ||
	   [ -n "${L_BROKEN_ARGS_CTAGS}" ]; then
	return 1
    else
	return 0
    fi
}

help_run ()
{
cat <<EOF
$0 run [OPTIONS] UNITS-DIR

	   Run all tests case under UNITS-DIR.

	   OPTIONS:
		--ctags CTAGS: ctags executable file for testing
		--categories CATEGORY1[,CATEGORY2,...]: run only CATEGORY* related cases.
							Category selection is done in upper
							layer than unit selection. This
							means even if a unit is specified
							with --units, it can be ignored
							is a category the units doesn't
							belong to is specified with
							--categories option.
		--colorized-output yes|no: print the result in color.
		--skip NAME: skip the case NAME (TODO: NOT IMPLEMENTED YET)
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases.
		--units UNITS1[,UNITS2,...]: run only UNIT(S).
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout(default).
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
		--show-diff-output: show diff output for failed test cases in the summary.
		--with-pretense-map=NEWLANG0/OLDLANG0[,...]: make NEWLANG parser pretend
							     OLDLANG.
EOF
}

action_clean ()
{
    local action="$1"
    shift

    local units_dir=$1
    shift

    local bundles
    local b

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$units_dir" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$units_dir" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    fi

    check_availability find
    check_availability rm

    for bundles in $(find "$units_dir" -name "BUNDLES"); do
	while read b; do
	    rm -rf "${b}"
	done < ${bundles}
	rm ${bundles}
    done

    rm -f $(find "$units_dir" -name '*.tmp')
    rm -f $(find "$units_dir" -name '*.TMP')
    return 0
}

help_clean ()
{
cat <<EOF
$0 clean UNITS-DIR

	   Clean all files created during units testing
EOF

}

shrink_prepare ()
{
    local output="$1"
    local input="$2"
    local start="$3"
    local len="$4"


    dd bs=1 count="${len}" skip="${start}" < "${input}" 2>/dev/null > "${output}"
}

shrink_test ()
{
    local cmdline="$1"
    local input="$2"
    local start="$3"
    local len="$4"
    local output="$5"
    local r
    local msg

    shrink_prepare "${output}" "${input}" "${start}" "${len}"
    [ "${QUIET}" = 'yes' ] || printf "[%-5u %6u]..." "${start}" $(( start + len )) 1>&2
    eval "${cmdline}" > /dev/null 2>&1
    r="$?"
    if [ "$r" -eq 0 ]; then
	msg='ok'
    elif [ "$r" -eq "${_TIMEOUT_EXIT}" ]; then
	msg='timeout'
    else
	msg='failed'
    fi
    [ "${QUIET}" = 'yes' ] || printf "%s(%u)\n" "$msg" "$r" 1>&2
    return $r
}

shrink_bisect ()
{
    local cmdline="$1"
    local input="$2"
    local len="$3"
    local output="$4"

    local end
    local start
    local step
    local delta

    local failed
    local successful

    end="${len}"
    failed="${len}"
    successful=0

    step=0
    while true; do
	delta=$((len >> (step + 1)))
	if [ "${delta}" -eq 0 ]; then
	    delta=1
	fi
	if shrink_test "${cmdline}" "${input}" 0 "${end}" "${output}"; then
	    successful="${end}"
	    if [ $(( end + 1 )) -eq "${failed}" ]; then
		end="${failed}"
		break
	    else
		end=$((end + delta))
	    fi
	else
	    failed="$end"
	    if [ $(( successful + 1 )) -eq "${end}" ]; then
		break
	    else
		end=$((end - delta))
	    fi
	fi
	step=$((step + 1 ))
    done

    len="${end}"
    start=0
    failed=0
    successful="${end}"
    step=0
    while true; do
	delta=$((len >> (step + 1)))
	if [ "${delta}" -eq 0 ]; then
	    delta=1
	fi
	if shrink_test "${cmdline}" "${input}" "${start}" $((end - start)) "${output}"; then
	    successful="${start}"
	    if [ $(( start - 1 )) -eq "${failed}" ]; then
		start=$((start - 1))
		break
	    else
		start=$((start - delta))
	    fi
	else
	    failed="${start}"
	    if [ $((successful - 1)) -eq "${start}" ]; then
		break
	    else
		start=$((start + delta))
	    fi
	fi
	step=$((step + 1))
    done

    len=$((end - start))
    shrink_prepare "${output}" "${input}" "${start}" "${len}"
    [ "${QUIET}" = 'yes' ] || echo "Minimal badinput: ${output}"
    [ "${QUIET}" = 'yes' ] || line .
    cat "${output}"
    echo

    return 0
}

shrink_main ()
{
    local cmdline_template="$1"
    local cmdline
    local input="$2"
    local len
    local output="$3"
    local duration="$4"
    local foreground="$5"

    if ! [ -f "${input}" ]; then
	ERROR 1 "No such file: ${input}"
    elif ! [ -r "${input}" ]; then
	ERROR 1 "Cannot read a file: ${input}"
    fi

    if ! cat < /dev/null > "${output}"; then
	ERROR 1 "Cannot modify a file: ${output}"
    fi

    cmdline=$(printf "${cmdline_template}" "${output}")
    if [ -n "${duration}" ] && ! [ "${duration}" -eq 0 ]; then
	if [ "${foreground}" = 'yes' ]; then
	    cmdline="timeout --foreground ${duration} ${cmdline}"
	else
	    cmdline="timeout ${duration} ${cmdline}"
	fi
    fi

    len=$(stat -c %s "${input}")

    if shrink_test "${cmdline}" "${input}" 0 "${len}" "${output}"; then
	printf "the target command line exits normally against the original input\n" 1>&2
	return 1
    fi

    if ! shrink_test "${cmdline}" "${input}" 0 0 "${output}"; then
	printf "the target command line exits abnormally against the empty input\n" 1>&2
	return 1
    fi

    shrink_bisect "${cmdline}" "${input}" "${len}" "${output}"
}

action_shrink ()
{
    local action="$1"
    shift

    local cmdline_template
    local input
    local output

    local timeout
    local duration
    local foreground


    while [ $# -gt 0 ]; do
	case $1 in
	    --timeout)
		shift
		duration=$1
		shift
		;;
	    --timeout=*)
		duration="${1#--timeout=}"
		shift
		;;
	    --foreground)
		foreground=yes
		shift
		;;
	    --quiet)
		QUIET=yes
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		break
		;;
	    esac
    done

    if [ $# -lt 3 ]; then
	ERROR 1 "too few arguments for ${action} action: $*"
    elif [ $# -gt 3 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    fi

    if [ -n "${foreground}" ] && [ -z "${duration}" ]; then
	ERROR 1 "--foreground option is meaningful only if --timeout option is specified."
    fi

    cmdline_template=$1
    input=$2
    output=$3
    shift 3

    shrink_main "${cmdline_template}" "${input}" "${output}" ${duration} ${foreground}
    return $?
}

help_shrink ()
{
cat <<EOF
$0 shrink [OPTIONS] CMD_TEMPLATE INPUT OUTPUT

	   Shrink the input while the execution of CMD_TEMPLATE is failed
	   and find minimal unwanted input.

	   OPTIONS:
		--timeout N: Run CMD under timeout command with duration N
		--foreground: add --foreground option to timeout command.
			      can be used with --timeout option.
	   EXAMPLES:
		misc/units shrink "u-ctags -o - %s" original-input.js  /tmp/anyname.js
EOF
}
#action_shrink shrink --timeout=1 --foreground "./a.out  < %s" input.txt output.txt

fuzz_shrink ()
{
    local cmdline_template="$1"
    local input="$2"
    local output="$3"
    local lang="$4"
    shift 4

    [ "${QUIET}" = 'yes' ] || {
	echo "Shrinking ${input} as ${lang}"
	line .
    }
    shrink_main "${cmdline_template}" "${input}" "${output}"  1 yes
}

fuzz_lang_file ()
{
    local lang="$1"
    local file="$2"
    shift 2
    local r

    local dir="${file%/*}"
    local ovalgrind="${dir}/VALGRIND-${lang}.tmp"
    local ocmdline="${dir}/CMDLINE-${lang}.tmp"
    local oshrink="${dir}/SHRINK-${lang}.tmp"

    local cmdline
    local cmdline_for_shirking

    rm -f "${ovalgrind}" "${ocmdline}" "${oshrink}"


    if [ "${WITH_VALGRIND}" = 'yes' ]; then
	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${file}" "${ovalgrind}" )
    else
	cmdline="${_CMDLINE} --language-force=${lang} ${file}"

    fi
    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"


    [ "${QUIET}" = 'yes' ] || printf "."
    echo "${cmdline}" > "${ocmdline}"
    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
    r=$?

    case $r in
	0)
	    rm -f "${ovalgrind}" "${ocmdline}"
	    return 0
	    ;;
	${_TIMEOUT_EXIT})
	    [ "${QUIET}" = 'yes' ] || echo
	    printf '%-40s' "[timeout $lang]"
	    echo "$f"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${file}" "${oshrink}" "${lang}"
	    return 1
	    ;;
	${_VALGRIND_EXIT})
	    [ "${QUIET}" = 'yes' ] || echo
	    printf '%-40s' "[valgrind-error $lang]"
	    echo "$f"
	    return 1
	    ;;
	*)
	    [ "${QUIET}" = 'yes' ] || echo
	    printf '%-40s' "[unexpected-status($r) $lang]"
	    echo "$f"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${file}" "${oshrink}" "${lang}"
	    return 1
	    ;;
    esac

    return $r
}

fuzz_lang ()
{
    local lang="$1"
    local dir="$2"
    shift 2
    local f
    local r=0

    [ "${QUIET}" = 'yes' ] || printf '%-60s\n' "Semi-fuzzing (${lang})"
    for f in $(find "${dir}" -type f -name 'input.*'); do
	if ! fuzz_lang_file "${lang}" "${f}"; then
	    r=1
	    break
	fi
    done
    [ "${QUIET}" = 'yes' ] || echo
    return $r
}

action_fuzz ()
{
    action_fuzz_common fuzz_lang "$@"
}

action_fuzz_common ()
{
    local fn="$1"
    local action="$2"
    shift 2

    local units_dir
    local cmdline
    local lang
    local r

    while [ $# -gt 0 ]; do
	case $1 in
	    --ctags)
		shift
		CTAGS="$1"
		shift
		;;
	    --ctags=*)
		CTAGS="${1#--ctags=}"
		shift
		;;
	    --languages)
		shift
		LANGUAGES=$(echo "${1}" | tr ',' ' ')
		shift
		;;
	    --languages=*)
		LANGUAGES=$(echo "${1#--languages=}" | tr ',' ' ')
		shift
		;;
	    --quiet)
		QUIET=yes
		shift
		;;
	    --with-timeout)
		shift
		WITH_TIMEOUT="$1"
		shift
		;;
	    --with-timeout=*)
		WITH_TIMEOUT="${1#--with-timeout=}"
		shift
		;;
	    --with-valgrind)
		shift
		WITH_VALGRIND=yes
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT="$1"
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT="${1#--colorized-output=}"
		shift
		;;
	    --run-shrink)
		RUN_SHRINK=yes
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		units_dir="$1"
		shift
		break;
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$units_dir" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$units_dir" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    fi

    if ! [ -f "${CTAGS}" ]; then
	ERROR 1 "no such file: ${CTAGS}"
    elif ! [ -e "${CTAGS}" ]; then
	ERROR 1 "${CTAGS} is not an executable file"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    : ${WITH_TIMEOUT:=2}
    [ "$WITH_TIMEOUT" = 0 ] || check_availability timeout
    [ "$WITH_VALGRIND" = 'yes' ] && check_availability valgrind
    check_availability find

    cmdline="${CTAGS} --options=NONE --\*-kinds=\* --fields=\*"
    _CMDLINE="${cmdline} -G -o - "
    _CMDLINE_FOR_SHRINKING="${_CMDLINE}"
    if [ "$WITH_VALGRIND" = yes ]; then
	_CMDLINE="valgrind --leak-check=full --error-exitcode=${_VALGRIND_EXIT} --log-file=%s ${_CMDLINE}"
	WITH_TIMEOUT=$(( WITH_TIMEOUT * ${_VG_TIMEOUT_FACTOR} ))
    fi

    if ! [ "$WITH_TIMEOUT" = 0 ]; then
	_CMDLINE="timeout --foreground $WITH_TIMEOUT ${_CMDLINE}"
	_CMDLINE_FOR_SHRINKING="timeout --foreground 1 ${_CMDLINE_FOR_SHRINKING}"
    fi

    for lang in $( ${cmdline} --list-languages 2>/dev/null | "${_LINE_SPLITTER}" |sed -e 's/ //' ) ; do
	if [ -n "${LANGUAGES}" ] && ! member_p "${lang}" ${LANGUAGES}; then
	    continue
	fi
	"${fn}" "${lang}" "${units_dir}"
	r=$?
    done

    return $r
}

help_fuzz ()
{
cat <<EOF
$0 fuzz [OPTIONS] UNITS-DIR

	   Run all tests case under UNITS-DIR.

	   OPTIONS:
		--ctags CTAGS: ctags executable file for testing
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
		--quiet: don't print dots as passed test cases.
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout.
					 default is 1.
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
EOF
}

noise_reduce ()
{
    local input="$1"
    local len="$2"
    local pos="$3"
    shift 3

    dd bs=1 count=$pos skip=0 if="$input"
    dd bs=1 count=$(( len - pos - 1 )) skip=$(( pos + 1 )) if="$input"
}

noise_inject ()
{
    local input="$1"
    local len="$2"
    local pos="$3"
    local c="$4"
    shift 4

    dd bs=1 count=$pos skip=0 if="$input"
    printf "%c" "$c"
    dd bs=1 count=$(( len - pos )) skip=$pos if="$input"
}

noise_report_line ()
{
    local pos="$1"
    local len="$2"
    local status_="$3"
    local how="$4"

    local progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
    local nspace


    if [ $((pos + 1)) -eq "${len}" ] || [ $status_ -gt 0 ]; then
	nspace=0
	while [ $nspace -lt $(( _NOISE_REPORT_MAX_COLUMN - progress_offset - 1)) ]; do
	    printf ' '
	    nspace=$((nspace + 1))
	done
	printf " %s %d/%d" "${how}" "$pos" "${len}"
    fi
}

noise_lang_file_noisespec ()
{
    local input="$1"
    local len="$2"
    local pos="$3"
    local c="$4"
    local genfn="$5"
    local lang="$6"
    local how="$7"
    shift 7

    local msg
    if [ "${how}" = + ]; then
	msg=INJECTED
	how="${how}${c}"
    else
	msg=REDUCED
	how="${how} "
    fi

    local dir="${input%/*}"
    local onoised=$(printf "%s/NOISE-INPUT-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
    local ocmdline=$(printf "%s/NOISE-CMDLINE-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
    local ovalgrind=$(printf "%s/NOISE-VALGRIND-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)
    local oshrink=$(printf "%s/NOISE-SHRINK-%s-%s-%d.tmp" "${dir}" "${msg}" "$pos" \'$c)

    local cmdline
    local cmdline_for_shirking
    local progress_offset
    local r

    rm -f "${ocmdline}" "${ovalgrind}" "${onoised}" "${oshrink}"
    if [ "${WITH_VALGRIND}" = 'yes' ]; then
	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${onoised}" "${ovalgrind}" )
	else
	cmdline="${_CMDLINE} --language-force=${lang} ${onoised}"
    fi
    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"

    "${genfn}" "${input}" "${len}" "$pos" "$c" > "$onoised"  2> /dev/null

    progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
    if [ "${progress_offset}" -eq 0 ]; then
	[ $pos -gt 0 ] && printf " %s %d/%d" "${how}" "$pos" "${len}"
	echo
    fi

    echo "${cmdline}" > "${ocmdline}"
    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
    r=$?
    case $r in
	    0)
	    printf 'o'
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    rm "${onoised}"
	    rm -f "${ovalgrind}" "${ocmdline}"
	    ;;
	${_TIMEOUT_EXIT})
	    printf "T"
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    printf '\n%-20s\n' "[timeout $lang]" "$onoised"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${onoised}" "${oshrink}" "${lang}"
	    ;;
	${_VALGRIND_EXIT})
	    printf "V"
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    printf '\n%-20s %s\n' "[valgrind-error $lang]" "$onoised"
	    ;;
	*)
	    printf "!"
	    noise_report_line "${pos}" "${len}" "${r}" "${how}"
	    printf '\n%-20s %s\n' "[unexpected-status($r) $lang]" "$onoised"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${onoised}" "${oshrink}" "${lang}"
	    ;;
    esac
    return $r
}

noise_lang_file ()
{
    local lang="$1"
    local input="$2"
    shift 2


    local cmdline
    local cmdline_for_shirking
    local len=$(stat -c %s "${input}")
    local r
    local i
    local c
    local guessed_lang

    guessed_lang=$( ${_CMDLINE_FOR_SHRINKING} --print-language "${input}" 2>/dev/null | sed -n 's/^.*: //p')
    if [ "${lang}" !=  "${guessed_lang}" ]; then
	return 0
    fi

    i=0
    c='!'
    echo "Testing cases derived from: ${input}"
    line '.' --no-newline
    while [ "$i" -lt "$len" ]; do
	if noise_lang_file_noisespec "${input}" "${len}" "$i" "$c" noise_reduce "${lang}" -; then
	    i=$(( i + 1 ))
	else
	    echo
	    return 1
	fi
    done

    for c in 'a' '0'						\
	'!' '@' '#' '$' '%' '^' '&' '*' '(' ')' '-' '=' '_'	\
	'+' '|'  '[' ']' '{' '}' '\' ';' "'" ':' '"' ',' '.'    \
	'/' '<' '>' '?' '`' '~'; do
	i=0
	while [ "$i" -lt "$len" ]; do
	    if noise_lang_file_noisespec "${input}" "${len}" "$i" "$c" noise_inject "${lang}" +; then
		i=$(( i + 1 ))
	    else
		echo
		return 1
	    fi
	done
    done
    echo
    return 0
}

noise_lang ()
{
    local lang="$1"
    local dir="$2"
    shift 2
    local f
    local r
    printf '%-60s\n' "Noised-fuzzing (${lang})"
    line '-'

    r=0
    for f in $(find "${dir}" -type f -name 'input.*'); do
	if ! noise_lang_file "${lang}" "${f}"; then
	    r=1
	    break
	fi
    done
    echo
    return $r
}

action_noise ()
{
    action_fuzz_common noise_lang "$@"
}

help_noise ()
{
cat <<EOF
$0 noise [OPTIONS] UNITS-DIR

	   Run all tests case for LANGUAGE with "noise" for
	   finding unexpected behavior like entering an
	   infinite loop.
	   Here "noise" means removing one byte from
	   somewhere file position of the original test case;
	   or adding something one byte to
	   somewhere file position of the original test case.

	   OPTIONS:
		--ctags CTAGS: ctags executable file for testing
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
		--quiet: don't print dots as passed test cases.
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout.
					 default is 1.
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
EOF
}


tmain_compare_result()
{
    local build_topdir=$1
    local f

    for f in ${build_topdir}/*/*-diff.txt; do
	if [ -f "$f" ]; then
	    echo "$f"
	    echo
	    cat "$f" | sed -e 's|.*|	&|'
	    echo
	fi
    done
    if [ -f ${build_topdir}/*/gdb-backtrace.txt ]; then
	cat ${build_topdir}/*/gdb-backtrace.txt
    fi
}

tmain_compare()
{
    local subdir=$1
    local build_subdir=$2
    local aspect=$3
    local generated

    printf '%-60s' "${aspect}"
    generated=${build_subdir}/${aspect}-diff.txt
    if diff -U 0 --strip-trailing-cr \
	    ${build_subdir}/${aspect}-actual.txt \
	    ${subdir}/${aspect}-expected.txt \
	    > ${generated} 2>&1; then
	run_result ok '/dev/null'
	rm ${generated}
	return 0
    else
	run_result error '/dev/null' "diff: ${generated}"
	return 1
    fi
}

failed_git_marker ()
{
    local f=$1
    local l

    if type "git" > /dev/null 2>&1; then
	l=$(git ls-files -- "$f")
	if [ -z "$l" ]; then
	    echo '<G>'
	fi
    fi
}

is_crashed ()
{
    local f=$1

    grep -q -i "core dump" "$f"
}

print_backtraces()
{
    local ctags_exe=$1
    shift 1

    local coref
    for coref in "$@"; do
	if [ -f "${coref}" ]; then
	    gdb "${ctags_exe}" -c "${coref}" -ex where -batch
	else
	    echo "no such file: ${coref}"
	fi
    done
}

CODE_FOR_IGNORING_THIS_TMAIN_TEST=77
tmain_run ()
{
    local topdir=$1
    local build_topdir=$2
    shift 2
    local units="$@"

    local subdir
    local basedir

    local test_name
    local failed
    local f

    local aspect
    local engine

    local r
    local a
    local status_=0

    local need_rearrange

    if ! [ $(basename "${CTAGS}") = 'ctags' ]; then
	need_rearrange=yes
    fi

    basedir=$(pwd)
    for subdir in ${topdir}/*.d; do
	if [ "${subdir}" = ${topdir}/'*.d' ]; then
	    return 1
	fi

	test_name=$(basename ${subdir} .d)

	if [ -n "${units}" ] && ! member_p "${test_name}" ${units}; then
	    continue
	fi

	build_subdir=${build_topdir}/$(basename ${subdir})
	if ! mkdir -p ${build_subdir}; then
	    return 1
	fi

	rm -f ${build_subdir}/*-actual.txt

	echo "Testing ${test_name}"
	line '-'
	(
	    cd ${subdir}
	    ${SHELL} run.sh \
		     ${basedir}/${CTAGS} \
		     ${build_subdir} \
		     ${basedir}/${READTAGS}
	) > ${build_subdir}/stdout-actual.txt 2> ${build_subdir}/stderr-actual.txt
	r=$?
	echo $r > ${build_subdir}/exit-actual.txt

	if [ -n "${need_rearrange}" ]; then
	    sed -i -e 's|^'$(basename "${CTAGS}")':|ctags:|' ${build_subdir}/stderr-actual.txt
	fi

	if [ $r = $CODE_FOR_IGNORING_THIS_TMAIN_TEST ]; then
	    run_result skip '/dev/null' "$(cat ${build_subdir}/stdout-actual.txt)"
	    for a in ${build_subdir}/*-actual.txt; do
		if [ -f "$a" ]; then
		    rm $a
		fi
	    done
	    echo
	    continue
	fi

	if [ -f ${build_subdir}/tags ]; then
	    mv ${build_subdir}/tags ${build_subdir}/tags-actual.txt
	fi
	for aspect in stdout stderr exit tags; do
	    if [ -f ${subdir}/${aspect}-expected.txt ]; then
		engine=compare
		if tmain_${engine} ${subdir} ${build_subdir} ${aspect}; then
		    rm ${build_subdir}/${aspect}-actual.txt
		else
		    failed="${failed} ${test_name}/${aspect}-${engine}"
		    failed="${failed}$(failed_git_marker ${subdir}/${aspect}-expected.txt)"
		    status_=1
		    if [ ${aspect} = stderr ] &&
			   is_crashed ${build_subdir}/${aspect}-actual.txt &&
			   type "gdb" > /dev/null 2>&1; then
			print_backtraces "${basedir}/${CTAGS}" \
					 ${build_subdir}/core* \
					 > ${build_subdir}/gdb-backtrace.txt
		    fi
		fi
	    elif [ -f ${build_subdir}/${aspect}-actual.txt ]; then
		     rm ${build_subdir}/${aspect}-actual.txt
	    fi
	done

	echo
    done

    if [ -n "${failed}" ]; then
	echo
	echo Failed tests
	line '='
	for f in ${failed}; do
	    echo $f | sed -e 's|<G>| (not committed/cached yet)|'
	done
	echo

	if [ "${SHOW_DIFF_OUTPUT}" = yes ]; then
	    engine=compare
	    echo Detail "[$engine]"
	    line '-'
	    tmain_${engine}_result ${build_topdir}
	fi
    fi

    return $status_
}

action_tmain ()
{
    local action="$1"
    shift
    local tmain_dir
    local build_dir

    while [ $# -gt 0 ]; do
	case $1 in
	    --ctags)
		shift
		CTAGS="$1"
		shift
		;;
	    --ctags=*)
		CTAGS="${1#--ctags=}"
		shift
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT="$1"
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT="${1#--colorized-output=}"
		shift
		;;
	    --with-valgrind)
		shift
		WITH_VALGRIND=yes
		;;
	    --show-diff-output)
		SHOW_DIFF_OUTPUT=yes
		shift
		;;
	    --readtags=*)
		READTAGS="${1#--readtags=}"
		shift
		;;
	    --units)
		shift
		UNITS=$(echo "$1" | tr ',' ' ')
		shift
		;;
	    --units=*)
		UNITS=$(echo "${1#--units=}" | tr ',' ' ')
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		tmain_dir="$1"
		shift
		build_dir=${1:-${tmain_dir}}
		if [ -n "$1" ]; then
		    shift
		fi
		break
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$tmain_dir" ]; then
	ERROR 1 "TMAIN_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$tmain_dir" ]; then
	ERROR 1 "No such directory(tmain_dir): ${tmain_dir}"
    fi

    case "${build_dir}" in
	/*) ;;
	*) build_dir=$(pwd)/${build_dir} ;;
    esac
    if ! [ -d "$build_dir" ]; then
	ERROR 1 "No such directory(build_dir): ${build_dir}"
    fi

    if ! [ -f "${CTAGS}" ]; then
	ERROR 1 "no such file: ${CTAGS}"
    elif ! [ -e "${CTAGS}" ]; then
	ERROR 1 "${CTAGS} is not an executable file"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    tmain_run ${tmain_dir} ${build_dir} ${UNITS}
    return $?
}

help_tmain ()
{
    cat <<EOF
$0 tmain [OPTIONS] TMAIN-DIR [BUILD-DIR]

	   Run tests for main part of ctags.
	   If BUILD-DIR is not given, TMAIN-DIR is reused as BUILD-DIR.

	   OPTIONS:

		--ctags CTAGS: ctags executable file for testing
		--colorized-output yes|no: print the result in color.
		--with-valgrind: (not implemented) run a test case under valgrind
		--show-diff-output: (not implemented)show diff output for failed test cases in the summary.
		--units UNITS1[,UNITS2,...]: run only Tmain/UNIT*.d (.d is not needed)
EOF
}

action_clean_tmain()
{
    local action="$1"
    shift

    local tmain_dir=$1
    shift

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "$tmain_dir" ]; then
	ERROR 1 "TMAIN_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "$tmain_dir" ]; then
	ERROR 1 "No such directory: ${tmain_dir}"
    fi

    check_availability find
    check_availability rm

    local object
    local type
    for object in stdout stderr exit tags; do
	for type in actual diff; do
	    rm -f $(find "$tmain_dir" -name ${object}-${type}.txt)
	    rm -f $(find "$tmain_dir" -name gdb-backtrace.txt)
	done
    done
    return 0
}

help_clean_tmain ()
{
    cat <<EOF
$0 clean_tmain TMAIN-DIR

	   Clean all files created during tmain testing
EOF

}

help_chop ()
{
    cat <<EOF
$0 chop|slap [OPTIONS] UNITS-DIR

	   OPTIONS:

		--ctags CTAGS: ctags executable file for testing
		--languages PARSER1[,PARSER2,...]: run only PARSER* related cases
		--quiet: don't print dots as passed test cases.
		--with-timeout DURATION: run a test case under timeout
					 command with SECOND.
					 0 means no timeout.
					 default is 1.
		--with-valgrind: run a test case under valgrind
			       If this option given, DURATION is changed to
			       DURATION := DURATION * ${_VG_TIMEOUT_FACTOR}
EOF
}

action_chop ()
{
    if [ "$1" = "chop" ]; then
	action_fuzz_common chop_lang "$@"
    else
	action_fuzz_common slap_lang "$@"
    fi
}

chop_lang()
{
    chop_lang_common "tail" "$@"
}

slap_lang()
{
    chop_lang_common "head" "$@"
}

chop_lang_common ()
{
    local endpoint=$1
    shift 1

    local lang="$1"
    local dir="$2"
    shift 2
    local f
    local r

    printf '%-60s\n' "Fuzzing by truncating input from ${endpoint} (${lang})"
    line '-'

    r=0
    for f in $(find "${dir}" -type f -name 'input.*'); do
	if ! chop_lang_file "$1" "${lang}" "${f}"; then
	    r=1
	    break
	fi
    done
    echo
    return $r
}

chop_lang_file ()
{
    local endpoint=$1
    shift 1

    local lang="$1"
    local input="$2"
    shift 2

    local r
    local cmdline
    local cmdline_for_shirking
    local len=$(stat -c %s "${input}")

    local guessed_lang
    guessed_lang=$( ${_CMDLINE_FOR_SHRINKING} --print-language "${input}" 2>/dev/null | sed -n 's/^.*: //p')
    if [ "${lang}" !=  "${guessed_lang}" ]; then
	return 0
    fi

    i=0
    echo "Testing cases derived from: ${input}"
    line '.' --no-newline

    r=0
    while [ "$i" -lt "$len" ]; do
	if chop_lang_file_chopspec "${endpoint}" "${input}" "${len}" "$i" "${lang}"; then
	    i=$(( i + 1 ))
	else
	    r=1
	    break
	fi
    done
    echo
    return $r
}

chop()
{
    local endpoint=$1
    local input=$2
    local pos=$3
    local len=$4

    if [ "${endpoint}" = "tail" ]; then
	dd if=$input bs=1 count=$pos
    else
	dd if=$input bs=1 count=$((len - pos)) skip=$pos
    fi
}

chop_lang_file_chopspec()
{
    local endpoint=$1
    shift 1

    local input="$1"
    local len="$2"
    local pos="$3"
    local lang="$4"
    shift 4

    local dir="${input%/*}"
    local ochopped=$(printf "%s/CHOP-INPUT-%s.tmp" "${dir}" "$pos")
    local ocmdline=$(printf "%s/CHOP-CMDLINE-%s.tmp" "${dir}" "$pos")
    local ovalgrind=$(printf "%s/CHOP-VALGRIND-%s.tmp" "${dir}" "$pos")
    local oshrink=$(printf "%s/CHOP-SHRINK-%s.tmp" "${dir}" "$pos")

    local cmdline
    local cmdline_for_shirking
    local progress_offset
    local r

    rm -f "${ocmdline}" "${ovalgrind}" "${ochopped}" "${oshrink}"

    if [ "${WITH_VALGRIND}" = 'yes' ]; then
	cmdline=$( printf "${_CMDLINE} --language-force=${lang} ${ochopped}" "${ovalgrind}" )
    else
	cmdline="${_CMDLINE} --language-force=${lang} ${ochopped}"
    fi
    cmdline_for_shirking="${_CMDLINE_FOR_SHRINKING} --language-force=${lang} %s"

    chop "${endpoint}" "${input}" "${pos}" "${len}" > "$ochopped"  2> /dev/null

    progress_offset=$(( pos % _NOISE_REPORT_MAX_COLUMN ))
    if [ "${progress_offset}" -eq 0 ]; then
	[ $pos -gt 0 ] && printf " %d/%d" "$pos" "${len}"
	echo
    fi

    echo "${cmdline}" > "${ocmdline}"
    ( exec 2>&-; ${cmdline} 2> /dev/null > /dev/null )
    r=$?
    case $r in
	0)
	    printf 'o'
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    rm "${ochopped}"
	    rm -f "${ovalgrind}" "${ocmdline}"
	    ;;
	${_TIMEOUT_EXIT})
	    printf "T"
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    printf '\n%-20s\n' "[timeout $lang]" "$ochopped"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${ochopped}" "${oshrink}" "${lang}"
	    ;;
	${_VALGRIND_EXIT})
	    printf "V"
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    printf '\n%-20s %s\n' "[valgrind-error $lang]" "$ochopped"
	    ;;
	*)
	    printf "!"
	    noise_report_line "${pos}" "${len}" "${r}" ""
	    printf '\n%-20s %s\n' "[unexpected-status($r) $lang]" "$ochopped"
	    [ "${RUN_SHRINK}" = 'yes' ] && fuzz_shrink "${cmdline_for_shirking}" "${ochopped}" "${oshrink}" "${lang}"
	    ;;
    esac
    return $r
}

help_validate_input ()
{
    cat <<EOF
$0 validate-input [OPTIONS] UNITS-DIR VALIDATORS-DIR

	Validate the input files (only for the test cases specifying validators.)

	OPTIONS:
		--validators=validator[,...]: Validate test cases specifying
					    given validators.
		--colorized-output: yes|no: print the result in color.
EOF
}

has_validator_acceptable_name ()
{
    local validator=$1
    if [ -z "${validator}" ]; then
	return 1
    fi

    echo "${validator}" | grep -q "^[-a-zA-Z+#0-9]\+$"
}

is_validator_runnable ()
{
    local v=$1
    local d=$2
    shift 2

    "$d/"validator-"$v" is_runnable
}

update_validator_list ()
{
    local type=$1
    local v=$2

    case $type in
	runnable)
	    if ! member_p "$v" ${_RUNNABLE_VALIDATORS}; then
		_RUNNABLE_VALIDATORS="${_RUNNABLE_VALIDATORS} $v"
	    fi
	    ;;
	unavailable)
	    if ! member_p "$v" ${_UNAVAILABLE_VALIDATORS}; then
		_UNAVAILABLE_VALIDATORS="${_UNAVAILABLE_VALIDATORS} $v"
	    fi
	    ;;
	*)
	    ERROR 1 "INTERNAL ERROR: wrong validator list type: ${type}"
	    ;;
    esac
}

#
# Whether a validator can be run or not.
#
# This runs "is_runnable" subcommand of the validator.
#
validate_validator ()
{
    local v=$1
    local validators_dir=$2
    local make_error=$3

    if member_p "$v" ${_RUNNABLE_VALIDATORS}; then
	return 0
    elif member_p "$v" ${_UNAVAILABLE_VALIDATORS}; then
	return 1
    fi

    if ! has_validator_acceptable_name "$v"; then
	if [ "${make_error}" = "error" ]; then
	    ERROR 1 "Unacceptable validator name: $v"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    elif ! [ -f "${validators_dir}/validator-$v" ]; then
	if [ "${make_error}" = "error" ]; then
	    ERROR 1 "No such validator: $v (${validators_dir}/validator-$v)"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    elif ! [ -x "${validators_dir}/validator-$v" ]; then
	if [ "$make_error" = "error" ]; then
	    ERROR 1 "Not executable: $v (${validators_dir}/validator-$v)"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    elif ! is_validator_runnable "$v" "${validators_dir}"; then
	if [ "${make_error}" = "error" ]; then
	    ERROR 1 "$v (${validators_dir}/validator-$v) is not ready to run"
	else
	    update_validator_list unavailable "$v"
	    return 1
	fi
    fi

    update_validator_list runnable "$v"
    return 0
}

#
# Choose a validator suitable for the current context.
#
# Return value
# 0: The caller should run echo'ed validator.
#
# 1: The caller should skip the input. It means
#    - the suitable validator is not listed in VALIDATORS, or
#    - no expected.tags and no validator file exist.
#    The caller doesn't have to update any validation counters.
# 2: The caller should skip the input and update the unavailable
#    counter.
#
resolve_validator ()
{
    local validator_file=$1
    local default_validator=$2
    local has_expected_tags=$3
    local validators_dir=$4

    shift 4

    local candidate_validator
    local local_validator

    if [ -r "$validator_file" ]; then
	local_validator=$(cat "${validator_file}" | grep -v '#')
	if [ -z "${local_validator}" ]; then
	    ERROR 1 "Empty validator specfile: ${local_validator}"
	else
	    candidate_validator=${local_validator}
	fi
    elif [ "$has_expected_tags" = no ]; then
	return 1
    else
	candidate_validator=${default_validator}
    fi

    if [ -z "${candidate_validator}" ]; then
	return 2
    elif [ -z "${VALIDATORS}" ] || member_p "${candidate_validator}" ${VALIDATORS}; then
	echo "${candidate_validator}"
	if validate_validator "${candidate_validator}" "${validators_dir}" noerror; then
	    return 0
	else
	    return 2
	fi
    else
	return 1
    fi
}

#
# Report the result of validation for INPUT with decoration
# The validation result counters are updated here.
#
validate_report_one ()
{
    local input=$1
    local validator=$2
    local status=$3
    shift 3

    if [ "${validator}" = "${_NOOP_VALIDATOR}" ]; then
	return
    fi

    local i=$(basename $input)
    local d=$(basename $(dirname $input))

    printf '%-65s' "$d/$i with ${validator}"
    case "${status}" in
	valid)
	    if [ "${validator}" = "${_KNOWN_INVALIDATION_VALIDATOR}" ]; then
		printf '%b\n' $(decorate yellow "known-invalidation")
		V_SKIP_KNOWN_INVALIDATION=$(( V_SKIP_KNOWN_INVALIDATION + 1 ))
	    else
		printf '%b\n' $(decorate green "valid")
		V_VALID=$(( V_VALID + 1))
	    fi
	    ;;
	invalid)
	    printf '%b\n' $(decorate red "invalid")
	    V_INVALID=$(( V_INVALID + 1))
	    ;;
	unavailable)
	    printf '%b\n' $(decorate yellow "unavailable")
	    V_SKIP_VALIDATOR_UNAVAILABLE=$(( V_SKIP_VALIDATOR_UNAVAILABLE + 1))
	    ;;
	*)
	    ERROR 1 "INTERNAL ERROR: wrong validation status: ${status}"
	    ;;
    esac
}

#
# Run a validator for the given input
#
# This runs "validate" subcommand of the validator.
#
validate_file ()
{
    local input=$1
    local validator=$2
    local validators_dir=$3
    shift 3

    ${validators_dir}/validator-${validator} validate $input
    return $?
}

#
# Validate input files under *.[dbtiv].
#
validate_dir ()
{
    local base_dir=$1
    local default_validator=$2
    local validators_dir=$3
    shift 3

    local f
    local t

    local v0 s0 inputs0
    local v s inputs

    #
    # No expected.tags* implies the input is invalid.
    # We don't have to run any validator as far as no
    # ./validator explicitly is given.
    #
    local has_expected_tags=no
    for f in "${base_dir}"/expected.tags*; do
	[ -r "$f" ] || continue
	unwanted_file "$f" && continue
	has_expected_tags=yes
	break
    done

    # A validator specified in ./validator is used for validating ./input.foo.
    # It will be used for validating ./input[-_]*.foo, too if ./validator[-_]*.
    # doesn't exit.
    inputs0=$(for f in "${base_dir}"/input.*; do
		  [ -r "$f" ] || continue
		  unwanted_file "$f" && continue
		  echo $f
	      done | sort)
    v0=$(resolve_validator "${base_dir}"/validator \
			  "${default_validator}" \
			  "${has_expected_tags}" \
			  "${validators_dir}")
    s0=$?

    case "$s0" in
	0)
	    update_validator_list runnable "$v0"
	    for f in $inputs0; do
		if validate_file "$f" "$v0" "${validators_dir}"; then
		    validate_report_one "$f" "$v0" valid
		else
		    validate_report_one "$f" "$v0" invalid
		fi
	    done
	    default_validator=$v0
	    ;;
	1)
	    # no action needed
	    ;;
	2)
	    update_validator_list unavailable "$v0"
	    for f in $inputs0; do
		validate_report_one "$f" "$v0" unavailable
	    done
	    default_validator=
	    ;;
    esac

    inputs=$(for f in "${base_dir}"/input[-_][0-9].* \
		      "${base_dir}"/input[-_][0-9][-_]*.*; do
		 [ -r "$f" ] || continue
		 unwanted_file "$f" && continue
		 echo $f
	     done | sort)

    for f in $inputs; do
	t=${f#input}; t=${t%.*}
	v=$(resolve_validator "${base_dir}"/validator"$t" \
			     "$default_validator" \
			     "$has_expected_tags" \
			     "$validators_dir")
	s=$?
	case "$s" in
	    0)
		update_validator_list runnable "$v0"
		if validate_file "$f" "$v" "${validators_dir}"; then
		    validate_report_one "$f" "$v" valid
		else
		    validate_report_one "$f" "$v" invalid
		fi
		;;
	    1)
		# no action needed
		;;
	    2)
		update_validator_list unavailable "$v0"
		validate_report_one "$f" "$v" unavailable
		;;
	esac
    done
}

#
# Validate input files under *.r.
#
validate_category ()
{
    local category=$1
    local base_dir=$2
    local validators_dir=$3
    shift 3

    local d
    local default_validator=${_NOOP_VALIDATOR}

    if [ -r "${base_dir}/validator" ]; then
	default_validator=$(cat "${base_dir}/validator" | grep -v '#')
	if [ -z "${default_validator}" ]; then
	    ERROR 1 "Empty validator specfile (in ${base_dir}/validator)"
	fi

	validate_validator "${default_validator}" "${validators_dir}" noerror
    fi

    for d in "${base_dir}"/*.[dbtiv]; do
	[ -d "$d" ] || continue
	validate_dir "$d" "${default_validator}" "${validators_dir}"
    done
}

#
# Report the summary of validations
#
validate_summary ()
{
    echo
    echo "Summary"
    line

    printf '  %-40s' "#valid:"
    printf '%b\n' $(decorate green "${V_VALID}")

    printf '  %-40s' "#invalid:"
    if [ "${V_INVALID}" = 0 ]; then
	echo "${V_INVALID}"
    else
	printf '%b\n' $(decorate red "${V_INVALID}")
    fi

    printf '  %-40s' "#skipped (known invalidation)"
    if [ "${V_SKIP_KNOWN_INVALIDATION}" = 0 ]; then
	echo 0
    else
	printf '%b\n' $(decorate yellow "${V_SKIP_KNOWN_INVALIDATION}")
    fi

    printf '  %-40s' "#skipped (validator unavailable)"
    if [ "${V_SKIP_VALIDATOR_UNAVAILABLE}" = 0 ]; then
	echo 0
    else
	local u
	printf '%b\n' $(decorate yellow "${V_SKIP_VALIDATOR_UNAVAILABLE}")

	echo
	echo "Unavailable validators"
	line
	for u in ${_UNAVAILABLE_VALIDATORS}; do
	    echo "	$u"
	done
    fi

    if [ "${V_INVALID}" = 0 ]; then
	return 0
    else
	return "${_VALIDATION_EXIT_INVALID}"
    fi
}

action_validate_input ()
{
    local action=$1
    shift

    local units_dir
    local validators_dir
    local validators

    local v
    local d

    while [ $# -gt 0 ]; do
	case $1 in
	    --validators)
		shift
		validators=$1
		shift
		;;
	    --validators=*)
		validators=${1#--validators=}
		shift
		;;
	    --colorized-output)
		shift
		COLORIZED_OUTPUT=$1
		shift
		;;
	    --colorized-output=*)
		COLORIZED_OUTPUT=${1#--colorized-output=}
		shift
		;;
	    -*)
		ERROR 1 "unknown option \"${1}\" for ${action} action"
		;;
	    *)
		units_dir=$1
		shift
		validators_dir=$1
		shift
		break;
		;;
	esac
    done

    if [ $# -gt 0 ]; then
	ERROR 1 "too many arguments for ${action} action: $*"
    elif [ -z "${units_dir}" ]; then
	ERROR 1 "UNITS_DIR parameter is not given in ${action} action"
    elif [ -z "${validators_dir}" ]; then
	ERROR 1 "VALIDATORS_DIR parameter is not given in ${action} action"
    fi

    if ! [ -d "${units_dir}" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    elif ! [ -d "${validators_dir}" ]; then
	ERROR 1 "No such directory: ${units_dir}"
    fi

    if ! ( [ "${COLORIZED_OUTPUT}" = 'yes' ] || [ "${COLORIZED_OUTPUT}" = 'no' ] ); then
	ERROR 1 "unexpected option argument for --colorized-output: ${COLORIZED_OUTPUT}"
    fi

    if [ -n "${validators}" ]; then
	VALIDATORS=$(echo "${validators}" | tr ',' ' ')
	for v in ${VALIDATORS}; do
	    validate_validator "$v" "${validators_dir}" error
	done
    fi

    for d in ${units_dir}/*.r; do
	[ -d "$d" ] || continue
	category=${d##*/}
	echo
	echo "Category: ${category}"
	line
	validate_category "${category}" "$d" "${validators_dir}"
    done

    echo
    echo "Category: ${_DEFAULT_CATEGORY}"
    line
    for d in ${units_dir}/*.[dbtiv]; do
	[ -d "$d" ] || continue
	validate_dir "$d" "${_NOOP_VALIDATOR}" "$validators_dir"
    done

    validate_summary
    return $?
}

# * Avoid issues between sed and the locale settings by overriding it using
#   LC_ALL, which takes precedence over all other locale configurations:
#   https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
#
# * Avoid unexpected pathname conversion on MSYS2.
#   https://github.com/msys2/msys2/wiki/Porting#filesystem-namespaces
prepare_environment ()
{
    _PREPERE_ENV=$(cat <<'EOF'
LC_ALL="C"; export LC_ALL
MSYS2_ARG_CONV_EXCL='--regex-;--_scopesep' export MSYS2_ARG_CONV_EXCL
EOF
)
    eval ${_PREPERE_ENV}
}

main ()
{
    if [ $# = 0 ]; then
	action_help 1>&2
	exit 1
    fi

    case $1 in
	help|-h|--help)
	    action_help
	    return 0
	    ;;
	run)
	    action_run "$@"
	    return $?
	    ;;
	clean)
	    action_clean "$@"
	    return $?
	    ;;
	fuzz)
	    action_fuzz "$@"
	    return $?
	    ;;
	noise)
	    action_noise "$@"
	    return $?
	    ;;
	tmain)
	    action_tmain "$@"
	    return $?
	    ;;
	clean-tmain)
	    action_clean_tmain "$@"
	    return $?
	    ;;
	shrink)
	    action_shrink "$@"
	    return $?
	    ;;
	chop)
	    action_chop "$@"
	    return $?
	    ;;
	slap)
	    action_chop "$@"
	    return $?
	    ;;
	validate-input)
	    action_validate_input "$@"
	    return $?
	    ;;
	*)
	    ERROR 1 "unknown action: $1"
	    ;;
    esac
}

prepare_environment
main "$@"
exit $?
