diff --git a/cts/cts-schemas.in b/cts/cts-schemas.in index a6b4a49807..629fefcc90 100755 --- a/cts/cts-schemas.in +++ b/cts/cts-schemas.in @@ -1,543 +1,543 @@ #!@BASH_PATH@ # # Copyright 2018-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # Note on portable usage of sed: GNU/POSIX/*BSD sed have a limited subset of # compatible functionality. Do not use the -i option, alternation (\|), # \0, or character sequences such as \n or \s. # Exit immediately if a command fails, with some exceptions (for example, when # part of an if or while condition). Treat unset variables as errors during # expansion. See bash(1) man page for details. set -eu # If readlink supports -e, use it readlink -e / >/dev/null 2>/dev/null if [ $? -eq 0 ]; then test_home=$(dirname "$(readlink -e "$0")") else test_home=$(dirname "$0") fi suites_dir="$test_home/schemas" src_dir=$(dirname "$test_home") if [ -d "$src_dir/xml" ]; then export PCMK_schema_directory="$src_dir/xml" echo "Using local schemas from: $PCMK_schema_directory" else export PCMK_schema_directory=@CRM_SCHEMA_DIRECTORY@ fi DIFF="diff -u" DIFF_PAGER="less -LRX" RNG_VALIDATOR="xmllint --noout --relaxng" XSLT_PROCESSOR="xsltproc --nonet" # Available test suites tests="test2to3" # # commons # emit_result() { local howmany=${1:?} # how many errors (0/anything else incl. strings) local subject=${2:?} local prefix=${3-} if [ -n "$prefix" ]; then prefix="$prefix: " fi if [ "$howmany" = "0" ]; then printf "%s%s finished OK\n" "$prefix" "$subject" else printf "%s%s encountered $howmany errors\n" "$prefix" "$subject" fi } emit_error() { local msg=${1:?} printf "%s\n" "$msg" >&2 } # @TODO We can probably drop the log functions. It's unclear why they're needed. # returns 1 + floor of base 2 logarithm if the argument is between 1 and 255, or # 0 the argument is 0 log2_or_0_return() { local i=${1:?} return $(((!(i >> 1) && i) * 1 \ + (!(i >> 2) && i & (1 << 1)) * 2 \ + (!(i >> 3) && i & (1 << 2)) * 3 \ + (!(i >> 4) && i & (1 << 3)) * 4 \ + (!(i >> 5) && i & (1 << 4)) * 5 \ + (!(i >> 6) && i & (1 << 5)) * 6 \ + (!(i >> 7) && i & (1 << 6)) * 7 \ + !!(i >> 7) * 7 )) } # rough addition of two base 2 logarithms log2_or_0_add() { local op1=${1:?} local op2=${2:?} if [ "$op1" -gt "$op2" ]; then return $op1 elif [ "$op2" -gt "$op1" ]; then return $op2 elif [ "$op1" -gt 0 ]; then return $((op1 + 1)) else return $op1 fi } # # test phases # # stdin: input file per line test_cleaner() { local source="" local source_basename="" local source_dir="" local ref_dir="" local ref_err_dir="" while read source; do source_basename=$(basename "$source") source_dir=$(dirname "$source") ref_dir="${source_dir/%xml/ref}" ref_err_dir="${source_dir/%xml/ref.err}" - rm -f "$ref_dir/${source_basename%.*}-*.up" \ - "$ref_err_dir/${source_basename%.*}-*.up.err" + rm -f "$ref_dir/${source_basename%.*}".up* \ + "$ref_err_dir/${source_basename%.*}".up.err* done } test_explanation() { local template="" while [ $# -gt 0 ]; do case "$1" in -o=*) template="$PCMK_schema_directory/upgrade-${1#-o=}.xsl";; esac shift done $XSLT_PROCESSOR "$PCMK_schema_directory/upgrade-detail.xsl" "$template" } cleanup_module_error() { # Work around a libxml2 bug. At least as of libxslt-1.1.41 and # libxml2-2.10.4, if the stylesheet contains a user-defined top-level # element (that is, one with a namespace other than the XSL namespace), # libxslt tries to load the namespace URI as an XML module. If this fails, # libxml2 logs a "module error: failed to open ..." message. # # This appears to be fixed in libxml2 v2.13 with commit ecb4c9fb. sed "/module error/d" "$1" > "$1.new" mv -- "$1.new" "$1" } test_runner_upgrade_one() { local source=${1:?} local input=${2:?} local transform=${3:?} local mode=${4:?} # extra modes wrt. "referential" outcome, see below local transform_num="${transform##*-}" transform_num="${transform_num%.xsl}" local source_dir=$(dirname "$source") local ref_dir="${source_dir/%xml/ref}" local ref_err_dir="${source_dir/%xml/ref.err}" local source_basename=$(basename "$source") local ref_basename="${source_basename%.*}.ref-$transform_num" local ref_err_basename="${source_basename%.*}.ref.err-$transform_num" local ref="$ref_dir/$ref_basename" local ref_err="$ref_err_dir/$ref_err_basename" local target="${ref/.ref/.up}" local target_err="${ref_err/.ref.err/.up.err}" local proc_rc=0 local diff_rc=0 local answer="" if ! [ "$((mode & (1 << 0)))" -ne 0 ] && ! [ -f "$ref_err" ]; then ref_err="/dev/null" fi $XSLT_PROCESSOR "$transform" "$input" > "$target" 2> "$target_err" \ || proc_rc=$? cleanup_module_error "$target_err" if [ "$proc_rc" -ne 0 ]; then echo "$target_err" return "$proc_rc" fi if [ "$mode" -ne 0 ]; then if [ "$((mode & (1 << 0)))" -ne 0 ]; then cp -a "$target" "$ref" cp -a "$target_err" "$ref_err" fi if [ "$((mode & (1 << 1)))" -ne 0 ]; then { $DIFF "$input" "$ref" && printf '\n(files match)\n'; } \ | $DIFF_PAGER >&2 if [ $? -ne 0 ]; then printf "\npager failure\n" >&2 return 1 fi printf '\nIs comparison OK? ' >&2 if read answer &2; return 1;; esac else return 1 fi fi elif [ -f "$ref" ] && [ -e "$ref_err" ]; then _output=$(cat "$ref") echo "$_output" | $DIFF - "$target" >&2 || diff_rc=$? if [ "$diff_rc" -eq 0 ]; then $DIFF "$ref_err" "$target_err" >&2 || diff_rc=$? fi if [ "$diff_rc" -ne 0 ]; then emit_error "Outputs differ from referential ones" echo "/dev/null" return 1 fi else emit_error "Referential file(s) missing: $ref" echo "/dev/null" return 1 fi echo "$target" } # stdout: filename of the transformed file test_runner_upgrade() { local template=${1:?} local source=${2:?} # filename local mode=${3:?} # extra modes wrt. "referential" outcome, see below local target="" local transform="" local rc=0 local transforms=$(ls "$PCMK_schema_directory"/upgrade-$template-*.xsl \ | sort -n) local input=$(mktemp) cp "$source" "$input" for transform in $transforms; do target=$(test_runner_upgrade_one "$source" "$input" "$transform" \ "$mode") rc=$? if [ "$rc" -ne 0 ]; then break; fi cp "$target" "$input" done rm -f "$input" echo "$target" return "$rc" } test_runner_validate() { local schema=${1:?} local target=${2:?} # filename if ! $RNG_VALIDATOR "$schema" "$target" 2>/dev/null; then $RNG_VALIDATOR "$schema" "$target" fi } # -o= ... which conventional version to deem as the transform origin # -t= ... which conventional version to deem as the transform target # -D # -G ... see usage # stdin: input file per line test_runner() { local template="" local schema_o="" local schema_t="" local mode=0 local ret=0 local origin="" local target="" while [ $# -gt 0 ]; do case "$1" in -o=*) template="${1#-o=}" schema_o="$PCMK_schema_directory/pacemaker-${1#-o=}.rng";; -t=*) schema_t="$PCMK_schema_directory/pacemaker-${1#-t=}.rng";; -G) mode=$((mode | (1 << 0)));; -D) mode=$((mode | (1 << 1)));; esac shift done if [ ! -f "${schema_o:?}" ] || [ ! -f "${schema_t:?}" ]; then emit_error "Origin and/or target schema missing, rerun make" return 1 fi while read origin; do printf '%-60s' "$origin... " # pre-validate if ! test_runner_validate "$schema_o" "$origin"; then ret=$((ret + 1)); echo "E:pre-validate"; continue fi # upgrade if ! target=$(test_runner_upgrade "$template" "$origin" "$mode"); then ret=$((ret + 1)); if [ -z "$target" ]; then break fi echo "E:upgrade" if [ -s "$target" ]; then echo --- cat "$target" || : echo --- fi continue fi # post-validate if ! test_runner_validate "$schema_t" "$target"; then ret=$((ret + 1)); echo "E:post-validate"; continue fi echo "OK" echo "$origin" | test_cleaner done log2_or_0_return "$ret" } # # particular test variations # -C # -X # stdin: granular test specification(s) if any # test2to3() { local spec="" local pattern="" while read spec; do spec=${spec%.xml} spec=${spec%\*} pattern="$pattern -name ${spec}*.xml -o" done if [ -n "$pattern" ]; then pattern="( ${pattern%-o} )" fi find "$suites_dir/test-2/xml" -name xml -o -type d -prune \ -o -name '*.xml' $pattern -print \ | env LC_ALL=C sort \ | { case " $* " in *\ -C\ *) test_cleaner;; *\ -X\ *) test_explanation -o=2.10;; *) test_runner -o=2.10 -t=3.0 "$@" || return $?;; esac; } } # # "framework" # # option-likes ... options to be passed down # argument-likes ... drives a test selection test_suite() { local pass="" local select="" local select_full="" local spec="" local _test="" local global_ret=0 local ret=0 local test_spec="" local test_specs="" local test_full="" while [ $# -gt 0 ]; do case "$1" in -) printf '%s\n' 'waiting for tests specified at stdin...'; while read spec; do select="${spec}@$1" done;; -*) pass="$pass $1";; *) select_full="${select_full}@$1" select="${select}@${1%%/*}";; esac shift done # select contains a '@'-delimited list of test suite names from CLI select="${select}@" # select_full contains a '@'-delimited list of test names select_full="${select_full}@" for _test in ${tests}; do while true; do case "$select" in *@${_test}@*) # A known test suite _test was found in the list of # requested test suites select. Strip it out of select. # # The purpose of this seems to be to prevent the later # select_full loop from selecting specific tests from this # suite, if the user also requested the entire suite. test_specs="${select%%@${_test}@*}@${select#*@${_test}@}" if [ "$test_specs" = "@" ]; then select= # nothing left else select="$test_specs" fi continue ;; @) case "$_test" in test*) break;; esac # filter ;; esac if [ -n "$test_specs" ]; then break fi continue 2 # move on to matching with next local test done test_specs= while true; do case "$select_full" in *@${_test}/*) # A test was requested from a known test suite. This does # not mean the requested test actually exists, but rather # that it was requested as the form "/...". # Strip extraneous data from test path test_full="${_test}/${select_full#*@${_test}/}" test_full="${test_full%%@*}" # Strip the requested test out of select_full select_full="${select_full%%@${test_full}@*}"\ "@${select_full#*@${test_full}@}" # Strip the test suite name and slash from the test spec test_specs="$test_specs ${test_full#*/}" ;; *) break ;; esac done # Feed the test specs (if any) as stdin to the respective test suite # function _test() for test_spec in $test_specs; do printf '%s\n' "$test_spec" done | "$_test" $pass || ret=$? if [ "$ret" = 0 ]; then emit_result "$ret" "$_test" else emit_result "at least 2^$((ret - 1))" "$_test" fi log2_or_0_add "$global_ret" "$ret" global_ret=$? done if [ -n "${select#@}" ]; then emit_error "Non-existing test(s):$(echo "${select}" | tr '@' ' ')" log2_or_0_add "$global_ret" 1 || global_ret=$? fi return "$global_ret" } # NOTE: big letters are dedicated for per-test-set behaviour, # small ones for generic/global behaviour usage() { printf \ '%s\n%s\n %s\n %s\n %s\n %s\n %s\n %s\n %s\n %s\n' \ "usage: $0 [-{C,D,G,X}]* \\" \ " [-|{${tests## }}*]" \ "- when no suites (arguments) provided, \"test*\" ones get used" \ "- with '-' suite specification the actual ones grabbed on stdin" \ "- use '-C' to only cleanup ephemeral byproducts" \ "- use '-D' to review originals vs. \"referential\" outcomes" \ "- use '-G' to generate \"referential\" outcomes" \ "- use '-X' to show explanatory details about the upgrade" \ "- test specification can be granular, e.g. 'test2to3/022'" } main() { local pass="" local bailout=0 local ret=0 while [ $# -gt 0 ]; do case "$1" in -h) usage; exit;; -C|-G|-X) bailout=1;; esac pass="$pass $1" shift done test_suite $pass || ret=$? if [ "$bailout" -eq 0 ]; then test_suite -C $pass >/dev/null || true fi if [ "$ret" = 0 ]; then emit_result "$ret" "Overall suite" else emit_result "at least 2^$((ret - 1))" "Overall suite" fi return "$ret" } main "$@"