>From f72b299cdda03fafb6dfad1fcf289ff50cb5ddf5 Mon Sep 17 00:00:00 2001 From: Bernhard Voelker Date: Wed, 9 Jan 2019 00:24:34 +0100 Subject: [PATCH 01/12] tests: add shell-style test framework Borrow the 'tests' framework from GNU coreutils. This allows better shell-style tests with more control over stdin, stdout, stderr, signals, preparatory steps, cleanup, return code verification, root-only tests, etc. * .gitignore: Add entries for per-test *.log and *.trs files, and the 'test-suite.log'. * .x-update-copyright: Exempt 'tests/init.sh' as this comes from gnulib. * Makefile.am: Include 'tests/local.mk'. (EXTRA_DIST): Add 'tests/GNUmakefile'. (SUBDIRS): Move 'gnulib-tests' to the end, i.e., run our own tests first. (ALL_RECURSIVE_TARGETS): Initialize. (update-gnulib-to-latest): Copy 'tests/init.sh' from gnulib. * cfg.mk: Add some syntax-check rules. * init.cfg: Add file. * tests/GNUmakefile: Likewise. * tests/envvar-check: Likewise. * tests/init.sh: Likewise. * tests/lang-default: Likewise. * tests/local.mk: Likewise. * tests/misc/help-version.sh: Likewise. * tests/other-fs-tmpdir: Likewise. * tests/sample-test: Likewise. * tests/.gitignore: Likewise. * NEWS (Changes to the build process): Mention the new test framework. --- .x-update-copyright | 1 + Makefile.am | 8 +- NEWS | 6 + cfg.mk | 129 ++++++- init.cfg | 702 +++++++++++++++++++++++++++++++++++++ tests/.gitignore | 3 + tests/GNUmakefile | 20 ++ tests/envvar-check | 58 +++ tests/init.sh | 618 ++++++++++++++++++++++++++++++++ tests/lang-default | 10 + tests/local.mk | 109 ++++++ tests/misc/help-version.sh | 64 ++++ tests/other-fs-tmpdir | 55 +++ tests/sample-test | 36 ++ 14 files changed, 1816 insertions(+), 3 deletions(-) create mode 100644 init.cfg create mode 100644 tests/.gitignore create mode 100644 tests/GNUmakefile create mode 100644 tests/envvar-check create mode 100755 tests/init.sh create mode 100644 tests/lang-default create mode 100644 tests/local.mk create mode 100755 tests/misc/help-version.sh create mode 100644 tests/other-fs-tmpdir create mode 100644 tests/sample-test diff --git a/.x-update-copyright b/.x-update-copyright index 7306adcc..eabb8227 100644 --- a/.x-update-copyright +++ b/.x-update-copyright @@ -1,3 +1,4 @@ ^ChangeLog-2013$ ^COPYING$ ^bootstrap$ +^tests/init\.sh$ diff --git a/Makefile.am b/Makefile.am index 1acee1bd..1ea7387e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -28,13 +28,16 @@ EXTRA_DIST = \ build-aux/git-version-gen \ config.h.in \ stamp-h.in \ + tests/GNUmakefile \ tool-versions.txt DISTCLEANFILES = tool-versions.txt # "gnulib-tests" is the gnulib unit test dir. -SUBDIRS = gl gnulib-tests build-aux lib find xargs locate doc po m4 +SUBDIRS = gl build-aux lib find xargs locate doc po m4 gnulib-tests + +ALL_RECURSIVE_TARGETS = ACLOCAL_AMFLAGS = -I gl/m4 -I m4 @@ -107,6 +110,7 @@ gnulib-sync update-gnulib-to-latest: && git submodule foreach git pull origin master \ && cp -v gnulib/doc/COPYINGv3 COPYING \ && cp -v gnulib/build-aux/bootstrap bootstrap \ + && cp -v gnulib/tests/init.sh tests/init.sh \ && git status --short -- gnulib COPYING bootstrap \ ) @@ -123,3 +127,5 @@ coverage-clean: done clean-local: coverage-clean + +include $(top_srcdir)/tests/local.mk diff --git a/NEWS b/NEWS index 0ed37879..47338232 100644 --- a/NEWS +++ b/NEWS @@ -145,6 +145,12 @@ The translation files in the PO directory are no longer version controlled; instead bootstrap auto-updates them from "translationproject.org" during a maintainer build. +A shell-style test framework borrowed from GNU coreutils has been added. +This allows better tests with more control over stdin, stdout, stderr, +signals, preparatory steps, cleanup, return code verification, root-only +tests, etc. + + * Major changes in release 4.6.0, 2015-12-28 ** Stable Release diff --git a/cfg.mk b/cfg.mk index 4bc51872..16d605ea 100644 --- a/cfg.mk +++ b/cfg.mk @@ -92,16 +92,62 @@ exclude_file_name_regexp--sc_bindtextdomain = \ # cases where neither argument is a string literal. local-checks-to-skip += sc_prohibit_strcmp +# Ensure that each root-requiring test is run via the "check-root" rule. +sc_root_tests: + @t1=sc-root.expected; t2=sc-root.actual; \ + grep -nl '^ *require_root_$$' `$(VC_LIST) tests` | \ + sed 's|.*/tests/|tests/|' | sort > $$t1; \ + for t in $(all_root_tests); do echo $$t; done | sort > $$t2; \ + st=0; diff -u $$t1 $$t2 || st=1; \ + rm -f $$t1 $$t2; \ + exit $$st + +# Ensure that all version-controlled test cases are listed in $(all_tests). +sc_tests_list_consistency: + @bs="\\"; \ + test_extensions_rx=`echo $(TEST_EXTENSIONS) \ + | sed -e "s/ /|/g" -e "s/$$bs./$$bs$$bs./g"`; \ + { \ + for t in $(all_tests); do echo $$t; done; \ + cd $(top_srcdir); \ + $(SHELL) build-aux/vc-list-files tests \ + | grep -Ev '^tests/init\.sh$$' \ + | $(EGREP) "$$test_extensions_rx\$$"; \ + } | sort | uniq -u | grep . && exit 1; : + +# Ensure that all version-controlled test scripts are executable. +sc_tests_executable: + @set -o noglob 2>/dev/null || set -f; \ + find_ext="-name '' "`printf -- "-o -name *%s " $(TEST_EXTENSIONS)`;\ + find $(srcdir)/tests \( $$find_ext \) \! -perm -u+x -print \ + | { sed "s|^$(srcdir)/||"; git ls-files $(srcdir)/tests/; } \ + | sort | uniq -d \ + | sed -e "s/^/$(ME): Please make test executable: /" | grep . \ + && exit 1; : + +# Avoid :>file which doesn't propagate errors +sc_prohibit_colon_redirection: + @cd $(srcdir)/tests && GIT_PAGER= git grep -n ': *>.*||' \ + && { echo '$(ME): '"The leading colon in :> will hide errors" 1>&2; \ + exit 1; } \ + || : + # Usage of error() with an exit constant, should instead use die(), # as that avoids warnings and may generate better code, due to being apparent # to the compiler that it doesn't return. sc_die_EXIT_FAILURE: - @GIT_PAGER= git grep -E 'error \(.*_(FAILURE|INVALID)' \ - -- find lib locate xargs \ + @cd $(srcdir) \ + && GIT_PAGER= git grep -E 'error \(.*_(FAILURE|INVALID)' \ + -- find lib locate xargs \ && { echo '$(ME): '"Use die() instead of error" 1>&2; \ exit 1; } \ || : +sc_prohibit-skip: + @prohibit='\|\| skip ' \ + halt='Use skip_ not skip' \ + $(_sc_search_regexp) + # Disallow the C99 printf size specifiers %z and %j as they're not portable. # The gnulib printf replacement does support them, however the printf # replacement is not currently explicitly depended on by the gnulib error() @@ -114,6 +160,79 @@ sc_prohibit-c99-printf-format: && { echo '$(ME): Use PRI*MAX instead of %j or %z' 1>&2; exit 1; } \ || : +# Ensure that tests don't use `cmd ... && fail=1` as that hides crashes. +# The "exclude" expression allows common idioms like `test ... && fail=1` +# and the 2>... portion allows commands that redirect stderr and so probably +# independently check its contents and thus detect any crash messages. +sc_prohibit_and_fail_1: + @prohibit='&& fail=1' \ + exclude='(returns_|stat|kill|test |EGREP|grep|compare|2> *[^/])' \ + halt='&& fail=1 detected. Please use: returns_ 1 ... || fail=1' \ + in_vc_files='^tests/' \ + $(_sc_search_regexp) + +# Ensure that env vars are not passed through returns_ as +# that was seen to fail on FreeBSD /bin/sh at least +sc_prohibit_env_returns: + @prohibit='=[^ ]* returns_ ' \ + exclude='_ returns_ ' \ + halt='Passing env vars to returns_ is non portable' \ + in_vc_files='^tests/' \ + $(_sc_search_regexp) + +# Use framework_failure_, not the old name without the trailing underscore. +sc_prohibit_framework_failure: + @prohibit='\' \ + halt='use framework_failure_ instead' \ + $(_sc_search_regexp) + +# Prohibit the use of `...` in tests/. Use $(...) instead. +sc_prohibit_test_backticks: + @prohibit='`' in_vc_files='^tests/' \ + halt='use $$(...), not `...` in tests/' \ + $(_sc_search_regexp) + +# Ensure that compare is used to check empty files +# so that the unexpected contents are displayed +sc_prohibit_test_empty: + @prohibit='test -s.*&&' in_vc_files='^tests/' \ + halt='use `compare /dev/null ...`, not `test -s ...` in tests/' \ + $(_sc_search_regexp) + +# Ensure that tests call the get_min_ulimit_v_ function if using ulimit -v +sc_prohibit_test_ulimit_without_require_: + @cd $(srcdir) \ + && (GIT_PAGER= git grep -l get_min_ulimit_v_ -- tests; \ + GIT_PAGER= git grep -l 'ulimit -v' -- tests) \ + | sort | uniq -u | grep . && { echo "$(ME): the above test(s)"\ + " should match get_min_ulimit_v_ with ulimit -v" 1>&2; exit 1; } || : + +# Ensure that tests call the cleanup_ function if using background processes +sc_prohibit_test_background_without_cleanup_: + @cd $(srcdir) \ + && (GIT_PAGER= git grep -El '( &$$|&[^&]*=\$$!)' -- tests; \ + GIT_PAGER= git grep -l 'cleanup_()' -- tests | sed p) \ + | sort | uniq -u | grep . && { echo "$(ME): the above test(s)"\ + " should use cleanup_ for background processes" 1>&2; exit 1; } || : + +# Ensure that tests call the print_ver_ function for programs which are +# actually used in that test. +sc_prohibit_test_calls_print_ver_with_irrelevant_argument: + @cd $(srcdir) \ + && GIT_PAGER= git grep -w print_ver_ -- tests \ + | sed 's#:print_ver_##' \ + | { fail=0; \ + while read file name; do \ + for i in $$name; do \ + grep -w "$$i" $$file|grep -vw print_ver_|grep -q . \ + || { fail=1; \ + echo "*** Test: $$file, offending: $$i." 1>&2; };\ + done; \ + done; \ + test $$fail = 0 || exit 1; \ + } || { echo "$(ME): the above test(s) call print_ver_ for" \ + "program(s) they don't use" 1>&2; exit 1; } + # Exempt the contents of any usage function from the following. _continued_string_col_1 = \ s/^usage .*?\n}//ms;/\\\n\w/ and print ("$$ARGV\n"),$$e=1;END{$$e||=0;exit $$e} @@ -143,6 +262,12 @@ sc_preprocessor_indentation: echo '$(ME): skipping test $@: cppi not installed' 1>&2; \ fi +exclude_file_name_regexp--sc_prohibit_test_backticks = \ + ^tests/(local\.mk|init\.sh)$$ + +# Now that we have better tests, make this the default. +export VERBOSE = yes + # During 'make update-copyright', convert a sequence with gaps to the minimal # containing range. update-copyright-env = \ diff --git a/init.cfg b/init.cfg new file mode 100644 index 00000000..d4563d5a --- /dev/null +++ b/init.cfg @@ -0,0 +1,702 @@ +# This file is sourced by init.sh, *before* its initialization. + +# Copyright (C) 2010-2019 Free Software Foundation, Inc. + +# 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 3 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 . + +# This goes hand in hand with the "exec 9>&2;" in tests/Makefile.am's +# TESTS_ENVIRONMENT definition. +stderr_fileno_=9 + +# Having an unsearchable directory in PATH causes execve to fail with EACCES +# when applied to an unresolvable program name, contrary to the desired ENOENT. +# Avoid the problem by rewriting PATH to exclude unsearchable directories. +# Also, if PATH lacks /sbin and/or /usr/sbin, append it/them. +sanitize_path_() +{ + # FIXME: remove double quotes around $IFS when all tests use init.sh. + # They constitute a work-around for a bug in FreeBSD 8.1's /bin/sh. + local saved_IFS="$IFS" + IFS=: + set -- $PATH + IFS=$saved_IFS + + local d d1 + local colon= + local new_path= + for d in "$@"; do + test -z "$d" && d1=. || d1=$d + if ls -d "$d1/." > /dev/null 2>&1; then + new_path="$new_path$colon$d" + colon=':' + fi + done + + for d in /sbin /usr/sbin ; do + case ":$new_path:" in + *:$d:*) ;; + *) new_path="$new_path:$d" ;; + esac + done + + PATH=$new_path + export PATH +} + +getlimits_() +{ + eval $(getlimits) + test "$INT_MAX" || fatal_ "running getlimits" +} + +require_no_default_acl_() +{ + if getfacl --version < /dev/null > /dev/null 2>&1; then + getfacl "$1" | grep '^default:' && skip_ 'Default ACL detected' + else + ls -ld "$1" | grep '.........+' && skip_ 'ACL detected' + fi +} + +require_acl_() +{ + getfacl --version < /dev/null > /dev/null 2>&1 \ + && setfacl --version < /dev/null > /dev/null 2>&1 \ + || skip_ "This test requires getfacl and setfacl." + + id -u bin > /dev/null 2>&1 \ + || skip_ "This test requires a local user named bin." +} + +is_local_dir_() +{ + test $# = 1 || framework_failure_ + df --local "$1" >/dev/null 2>&1 +} + +require_mount_list_() +{ + local mount_list_fail='cannot read table of mounted file systems' + df --local 2>&1 | grep -F "$mount_list_fail" >/dev/null && + skip_ "$mount_list_fail" +} + +dump_mount_list_() +{ + cat /proc/self/mountinfo || + cat /proc/self/mounts || + cat /proc/mounts || + cat /etc/mtab +} + +require_local_dir_() +{ + require_mount_list_ + is_local_dir_ . || + skip_ "This test must be run on a local file system." +} + +require_selinux_() +{ + # When in a chroot of an SELinux-enabled system, but with a mock-simulated + # SELinux-*disabled* system, recognize that SELinux is disabled system wide: + grep 'selinuxfs$' /proc/filesystems > /dev/null \ + || skip_ "this system lacks SELinux support" + + # Independent of whether SELinux is enabled system-wide, + # the current file system may lack SELinux support. + # Also the current build may have SELinux support disabled. + case $(ls -Zd .) in + '? .'|'unlabeled .') + test -z "$CONFIG_HEADER" \ + && framework_failure_ 'CONFIG_HEADER not defined' + grep '^#define HAVE_SELINUX_SELINUX_H 1' "$CONFIG_HEADER" > /dev/null \ + && selinux_missing_="(file) system" || selinux_missing_="build" + skip_ "this $selinux_missing_ lacks SELinux support" + ;; + esac +} + +# Return the SELinux type component if available +get_selinux_type() { ls -Zd "$1" | sed -n 's/.*:\(.*_t\)[: ].*/\1/p'; } + +# Whether SELinux Multi Level Security is enabled +mls_enabled_() { + sestatus 2>&1 | + grep 'Policy MLS status:.*enabled' > /dev/null +} + +# Skip this test if we're not in SELinux "enforcing" mode. +require_selinux_enforcing_() +{ + require_selinux_ + test "$(getenforce)" = Enforcing \ + || skip_ "This test is useful only with SELinux in Enforcing mode." +} + +require_smack_() +{ + grep 'smackfs$' /proc/filesystems > /dev/null \ + || skip_ "this system lacks SMACK support" + + test "$(ls -Zd .)" != '? .' \ + || skip_ "this file system lacks SMACK support" +} + +require_openat_support_() +{ + # Skip this test if your system has neither the openat-style functions + # nor /proc/self/fd support with which to emulate them. + + test -z "$CONFIG_HEADER" \ + && framework_failure_ 'CONFIG_HEADER not defined' + + _skip=yes + grep '^#define HAVE_OPENAT' "$CONFIG_HEADER" > /dev/null && _skip=no + test -d /proc/self/fd && _skip=no + if test $_skip = yes; then + skip_ 'this system lacks openat support' + fi +} + +# Return true if command runs with the +# ulimit specified in the first argument +ulimit_supported_() +{ + local v + v="$1" + shift + + ( + # Try to disable core dumps which may + # occur with memory constraints + trap '' SEGV; ulimit -c 0; + + ulimit -v $v && "$@" + ) >/dev/null 2>&1 +} + +# Determine the minimum required VM limit to run the given command. +# Output that value to stdout ... to be used by the caller. +# Return 0 in case of success, and a non-Zero value otherwise. +get_min_ulimit_v_() +{ + local v + local page_size + + # Increase result by this amount to avoid alignment issues + page_size=$(getconf PAGESIZE || echo 4096) + page_size=$(($page_size / 1024)) + + for v in $( seq 5000 5000 50000 ); do + if ulimit_supported_ $v "$@"; then + local prev_v + prev_v=$v + for v in $( seq $(($prev_v-1000)) -1000 1000 ); do + ulimit_supported_ $v "$@" || + { + ret_v=$((prev_v + $page_size)) + echo $ret_v + return 0 + } + prev_v=$v + done + fi + done + # The above did not find a working limit. Echo a very small number - just + # in case the caller does not handle the non-Zero return value. + echo 1; return 1 +} + +require_readable_root_() +{ + test -r / || skip_ "/ is not readable" +} + +# Skip the current test if strace is not available or doesn't work +# with the named syscall. Usage: require_strace_ unlink +require_strace_() +{ + test $# = 1 || framework_failure_ + + strace -V < /dev/null > /dev/null 2>&1 || + skip_ 'no strace program' + + strace -qe "$1" echo > /dev/null 2>&1 || + skip_ 'strace -qe "'"$1"'" does not work' + + # On some linux/sparc64 systems, strace works fine on 32-bit executables, + # but prints only one line of output for every 64-bit executable. + strace -o log-help ls --help >/dev/null || framework_failure_ + n_lines_help=$(wc -l < log-help) + rm -f log-help + if test $n_lines_help = 0 || test $n_lines_help = 1; then + skip_ 'strace produces no more than one line of output' + fi +} + +# Skip the current test if valgrind doesn't work, +# which could happen if not installed, +# or hasn't support for the built architecture, +# or hasn't appropriate error suppressions installed etc. +require_valgrind_() +{ + valgrind --error-exitcode=1 true 2>/dev/null || + skip_ "requires a working valgrind" +} + +# Skip the current test if setfacl doesn't work on the current file system, +# which could happen if not installed, or if ACLs are not supported by the +# kernel or the file system, or are turned off via mount options. +# +# Work around the following two issues: +# +# 1) setfacl maps ACLs into file permission bits if on "noacl" file systems. +# +# On file systems which do not support ACLs (e.g. ext4 mounted with -o noacl), +# setfacl operates on the regular file permission bits, and only fails if the +# given ACL spec does not fit into there. Thus, to test if ACLs really work +# on the current file system, pass an ACL spec which can't be mapped that way. +# "Default" ACLs (-d) seem to fulfill this requirement. +# +# 2) setfacl only invokes the underlying system call if the ACL would change. +# +# If the given ACL spec would not change the ACLs on the file, then setfacl +# does not invoke the underlying system call - setxattr(). Therefore, to test +# if setting ACLs really works on the current file system, call setfacl twice +# with conflictive ACL specs. +require_setfacl_() +{ + local d='acltestdir_' + mkdir $d || framework_failure_ + local f=0 + + setfacl -d -m user::r-x $d \ + && setfacl -d -m user::rwx $d \ + || f=1 + rm -rf $d || framework_failure_ + test $f = 0 \ + || skip_ "setfacl does not work on the current file system" +} + +# Require a controlling input 'terminal'. +require_controlling_input_terminal_() +{ + have_input_tty=yes + tty -s || have_input_tty=no + test -t 0 || have_input_tty=no + if test "$have_input_tty" = no; then + skip_ 'requires controlling input terminal +This test must have a controlling input "terminal", so it may not be +run via "batch", "at", or "ssh". On some systems, it may not even be +run in the background.' + fi +} + +require_built_() +{ + skip_=no + for i in "$@"; do + case " $built_programs " in + *" $i "*) ;; + *) echo "$i: not built" 1>&2; skip_=yes ;; + esac + done + + test $skip_ = yes && skip_ "required program(s) not built" +} + +require_file_system_bytes_free_() +{ + local req=$1 + local expr=$(stat -f --printf "$req / %S <= %a" .) + $AWK "BEGIN{ exit !($expr) }" \ + || skip_ "this test needs at least $req bytes of free space" +} + +uid_is_privileged_() +{ + # Make sure id -u succeeds. + my_uid=$(id -u) \ + || { echo "$0: cannot run 'id -u'" 1>&2; return 1; } + + # Make sure it gives valid output. + case $my_uid in + 0) ;; + *[!0-9]*) + echo "$0: invalid output ('$my_uid') from 'id -u'" 1>&2 + return 1 ;; + *) return 1 ;; + esac +} + +get_process_status_() +{ + sed -n '/^State:[ ]*\([[:alpha:]]\).*/s//\1/p' /proc/$1/status +} + +# Convert an ls-style permission string, like drwxr----x and -rw-r-x-wx +# to the equivalent chmod --mode (-m) argument, (=,u=rwx,g=r,o=x and +# =,u=rw,g=rx,o=wx). Ignore ACLs. +rwx_to_mode_() +{ + case $# in + 1) rwx=$1;; + *) echo "$0: wrong number of arguments" 1>&2 + echo "Usage: $0 ls-style-mode-string" 1>&2 + return;; + esac + + case $rwx in + [ld-][rwx-][rwx-][rwxsS-][rwx-][rwx-][rwxsS-][rwx-][rwx-][rwxtT-]) ;; + [ld-][rwx-][rwx-][rwxsS-][rwx-][rwx-][rwxsS-][rwx-][rwx-][rwxtT-][+.]) ;; + *) echo "$0: invalid mode string: $rwx" 1>&2; return;; + esac + + # Perform these conversions: + # S s + # s xs + # T t + # t xt + # The 'T' and 't' ones are only valid for 'other'. + s='s/S/@/;s/s/x@/;s/@/s/' + t='s/T/@/;s/t/x@/;s/@/t/' + + u=$(echo $rwx|sed 's/^.\(...\).*/,u=\1/;s/-//g;s/^,u=$//;'$s) + g=$(echo $rwx|sed 's/^....\(...\).*/,g=\1/;s/-//g;s/^,g=$//;'$s) + o=$(echo $rwx|sed 's/^.......\(...\).*/,o=\1/;s/-//g;s/^,o=$//;'$s';'$t) + echo "=$u$g$o" +} + +skip_if_() +{ + case $1 in + root) skip_ must be run as root ;; + non-root) skip_ must be run as non-root ;; + *) ;; # FIXME? + esac +} + +very_expensive_() +{ + if test "$RUN_VERY_EXPENSIVE_TESTS" != yes; then + skip_ 'very expensive: disabled by default +This test is very expensive, so it is disabled by default. +To run it anyway, rerun make check with the RUN_VERY_EXPENSIVE_TESTS +environment variable set to yes. E.g., + + env RUN_VERY_EXPENSIVE_TESTS=yes make check + +or use the shortcut target of the toplevel Makefile, + + make check-very-expensive +' + fi +} + +expensive_() +{ + if test "$RUN_EXPENSIVE_TESTS" != yes; then + skip_ 'expensive: disabled by default +This test is relatively expensive, so it is disabled by default. +To run it anyway, rerun make check with the RUN_EXPENSIVE_TESTS +environment variable set to yes. E.g., + + env RUN_EXPENSIVE_TESTS=yes make check + +or use the shortcut target of the toplevel Makefile, + + make check-expensive +' + fi +} + +# Test whether we can run our just-built root owned find, +# i.e., that $NON_ROOT_USERNAME has access to the build directory. +nonroot_has_perm_() +{ + require_built_ chroot + + local find_version=$( + chroot --skip-chdir --user=$NON_ROOT_USERNAME / env PATH="$PATH" \ + find --version | + sed -n '1s/.* //p' + ) + case ":$find_version:" in + :$PACKAGE_VERSION:) ;; + *) return 1;; + esac +} + +require_root_() +{ + uid_is_privileged_ || skip_ "must be run as root" + NON_ROOT_USERNAME=${NON_ROOT_USERNAME=nobody} + NON_ROOT_GID=${NON_ROOT_GID=$(id -g $NON_ROOT_USERNAME)} + + # When the current test invokes chroot, call nonroot_has_perm_ + # to check for a common problem. + grep '^[ ]*chroot' "../$0" \ + && { nonroot_has_perm_ \ + || skip_ "user $NON_ROOT_USERNAME lacks execute permissions"; } +} + +skip_if_root_() { uid_is_privileged_ && skip_ "must be run as non-root"; } + +# Set 'groups' to a space-separated list of at least two groups +# of which the user is a member. +require_membership_in_two_groups_() +{ + test $# = 0 || framework_failure_ + + groups=${FINDUTILS_GROUPS-$( (id -G || /usr/xpg4/bin/id -G) 2>/dev/null)} + case "$groups" in + *' '*) ;; + *) skip_ 'requires membership in two groups +this test requires that you be a member of more than one group, +but running '\''id -G'\'' either failed or found just one. If you really +are a member of at least two groups, then rerun this test with +FINDUTILS_GROUPS set in your environment to the space-separated list +of group names or numbers. E.g., + + env FINDUTILS_GROUPS='\''users cdrom'\'' make check + +' + ;; + esac +} + +# Is /proc/$PID/status supported? +require_proc_pid_status_() +{ + sleep 2 & + local pid=$! + sleep .5 + grep '^State:[ ]*[S]' /proc/$pid/status > /dev/null 2>&1 || + skip_ "/proc/$pid/status: missing or 'different'" + kill $pid +} + +# Does trap support signal names? +# Old versions of ash did not. +require_trap_signame_() +{ + (trap '' CHLD) || skip_ 'requires trap with signal name support' +} + +# Does kill support sending signal to whole group? +# dash 0.5.8 at least does not. +require_kill_group_() +{ + kill -0 -- -1 || skip_ 'requires kill with group signalling support' +} + +# Return nonzero if the specified path is on a file system for +# which FIEMAP support exists. Note some file systems (like ext3 and btrfs) +# only support FIEMAP for files, not directories. +fiemap_capable_() +{ + if ! python < /dev/null; then + warn_ 'fiemap_capable_: python missing: assuming not fiemap capable' + return 1 + fi + python "$abs_srcdir"/tests/fiemap-capable "$@" +} + +# Skip the current test if "." lacks d_type support. +require_dirent_d_type_() +{ + python < /dev/null \ + || skip_ python missing: assuming no d_type support + + python "$abs_srcdir"/tests/d_type-check \ + || skip_ requires d_type support +} + +# Skip the current test if we lack Perl. +require_perl_() +{ + : ${PERL=perl} + $PERL -e 'use warnings' > /dev/null 2>&1 \ + || skip_ 'configure did not find a usable version of Perl' +} + +# Does the current (working-dir) file system support sparse files? +require_sparse_support_() +{ + test $# = 0 || framework_failure_ + # Test whether we can create a sparse file. + # For example, on Darwin6.5 with a file system of type hfs, it's not possible. + # NTFS requires 128K before a hole appears in a sparse file. + t=sparse.$$ + dd bs=1 seek=128K of=$t < /dev/null 2> /dev/null + set x $(du -sk $t) + kb_size=$2 + rm -f $t + if test $kb_size -ge 128; then + skip_ 'this file system does not support sparse files' + fi +} + +# Compile a shared lib using the GCC options for doing so. +# Pass input and output file as parameters respectively. +# Any other optional parmeters are passed to $CC. +gcc_shared_() +{ + local in=$1 + local out=$2 + shift 2 || return 1 + + $CC -Wall -shared --std=gnu99 -fPIC -O2 $* "$in" -o "$out" -ldl +} + +# There are a myriad of ways to build shared libs, +# so we only consider running tests requiring shared libs, +# on platforms that support building them as follows. +require_gcc_shared_() +{ + gcc_shared_ '-' 'd.so' -xc < /dev/null 2>&1 \ + || skip_ '$CC -shared ... failed to build a shared lib' + rm -f d.so +} + +mkfifo_or_skip_() +{ + test $# = 1 || framework_failure_ + if ! mkfifo "$1"; then + # Make an exception of this case -- usually we interpret framework-creation + # failure as a test failure. However, in this case, when running on a SunOS + # system using a disk NFS mounted from OpenBSD, the above fails like this: + # mkfifo: cannot make fifo 'fifo-10558': Not owner + skip_ 'unable to create a fifo' + fi +} + +# Disable the current test if the working directory seems to have +# the setgid bit set. +skip_if_setgid_() +{ + setgid_tmpdir=setgid-$$ + (umask 77; mkdir $setgid_tmpdir) + perms=$(stat --printf %A $setgid_tmpdir) + rmdir $setgid_tmpdir + case $perms in + drwx------);; + drwxr-xr-x);; # Windows98 + DJGPP 2.03 + *) skip_ 'this directory has the setgid bit set';; + esac +} + +# Skip if files are created with a different group to the current user +# This can happen due to a setgid dir, or by some other mechanism on OS X: +# https://unix.stackexchange.com/q/63865 +# https://bugs.gnu.org/14024#41 +skip_if_nondefault_group_() +{ + touch grp.$$ + gen_ug=$(stat -c '%u:%g' grp.$$) + rm grp.$$ + test "$gen_ug" = "$(id -ru):$(id -rg)" || + skip_ 'Files are created with a different gid' +} + +skip_if_mcstransd_is_running_() +{ + test $# = 0 || framework_failure_ + + # When mcstransd is running, you'll see only the 3-component + # version of file-system context strings. Detect that, + # and if it's running, skip this test. + __ctx=$(stat --printf='%C\n' .) || framework_failure_ + case $__ctx in + *:*:*:*) __ctx_ok=1 ;; # four components is ok + *:*:*) # three components is ok too if there is no MLS + mls_enabled_ || __ctx_ok=1 ;; + esac + + test "$__ctx_ok" || + skip_ "unexpected context '$__ctx'; turn off mcstransd" +} + +# Skip the current test if umask doesn't work as usual. +# This test should be run in the temporary directory that ends +# up being removed via the trap commands. +working_umask_or_skip_() +{ + umask 022 + touch file1 file2 + chmod 644 file2 + perms=$(ls -l file1 file2 | sed 's/ .*//' | uniq) + rm -f file1 file2 + + case $perms in + *' + '*) skip_ 'your build directory has unusual umask semantics' + esac +} + +# Retry a function requiring a sufficient delay to _pass_ +# using a truncated exponential backoff method. +# Example: retry_delay_ dd_reblock_1 .1 6 +# This example will call the dd_reblock_1 function with +# an initial delay of .1 second and call it at most 6 times +# with a max delay of 3.2s (doubled each time), or a total of 6.3s +# Note ensure you do _not_ quote the parameter to GNU sleep in +# your function, as it may contain separate values that sleep +# needs to accumulate. +# Further function arguments will be forwarded to the test function. +retry_delay_() +{ + local test_func=$1 + local init_delay=$2 + local max_n_tries=$3 + shift 3 || return 1 + + local attempt=1 + local num_sleeps=$attempt + local time_fail + while test $attempt -le $max_n_tries; do + local delay=$($AWK -v n=$num_sleeps -v s="$init_delay" \ + 'BEGIN { print s * n }') + "$test_func" "$delay" "$@" && { time_fail=0; break; } || time_fail=1 + attempt=$(expr $attempt + 1) + num_sleeps=$(expr $num_sleeps '*' 2) + done + test "$time_fail" = 0 +} + +# Call this with a list of programs under test immediately after +# sourcing init.sh. +print_ver_() +{ + require_built_ "$@" + if test "$VERBOSE" = yes; then + local i + for i in $*; do + env $i --version + done + fi +} + +# Are we running on GNU/Hurd? +require_gnu_() +{ + test "$(uname)" = GNU \ + || skip_ 'not running on GNU/Hurd' +} + +# Prepend all our source directories to PATH. +path_prepend_ "${srcdir=.}/find" "${srcdir=.}/locate" "${srcdir=.}/xargs" + +sanitize_path_ diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..702a9418 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,3 @@ +/*/*.log +/*/*.trs +/test-suite.log diff --git a/tests/GNUmakefile b/tests/GNUmakefile new file mode 100644 index 00000000..3c178a2c --- /dev/null +++ b/tests/GNUmakefile @@ -0,0 +1,20 @@ +# Provide a compatibility layer so that the commands used before the +# conversion of tests/ to non-recursive make still work. To do that, we +# must rerun the "make check" from the parent, and with tests/ prefixed +# onto any TESTS values. The SUBDIRS=. is to prevent the top-level check +# rules from descending into e.g., gnulib-test/. + +.PHONY: all +all: + @echo 'tests/GNUmakefile: did you mean to make "check"?' 1>&2 + @exit 1 + +ifeq ($(TESTS),) +tests = +else +tests = TESTS=$(addprefix tests/,$(TESTS)) +endif + +.PHONY: check +check: + cd .. && $(MAKE) $@ $(tests) SUBDIRS=. diff --git a/tests/envvar-check b/tests/envvar-check new file mode 100644 index 00000000..c2c7a754 --- /dev/null +++ b/tests/envvar-check @@ -0,0 +1,58 @@ +# -*- sh -*- +# Check environment variables for sane values while testing. + +# Copyright (C) 2000-2019 Free Software Foundation, Inc. + +# 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 3 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 . + +if (FOO=FOO; unset FOO) >/dev/null 2>&1; then + as_unset=unset +else + as_unset=false +fi + +envvar_check_fail=0 +vars=' + _POSIX2_VERSION + _STDBUF_E + _STDBUF_I + _STDBUF_O + BASH_ENV + BLOCKSIZE + BLOCK_SIZE + CDPATH + COLUMNS + ENV + LANGUAGE + POSIXLY_CORRECT + QUOTING_STYLE + SIMPLE_BACKUP_SUFFIX + TABSIZE + TERM + COLORTERM + TIME_STYLE + TMPDIR + VERSION_CONTROL +' +for var in $vars +do + $as_unset $var + if eval test \"\${$var+set}\" = set; then + echo "$0: the $var environment variable is set --" \ + ' unset it and rerun this test' >&2 + envvar_check_fail=1 + fi +done + +test "$envvar_check_fail" = 1 && exit 1 diff --git a/tests/init.sh b/tests/init.sh new file mode 100755 index 00000000..0d6ddee3 --- /dev/null +++ b/tests/init.sh @@ -0,0 +1,618 @@ +# source this file; set up for tests + +# Copyright (C) 2009-2019 Free Software Foundation, Inc. + +# 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 3 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 . + +# Using this file in a test +# ========================= +# +# The typical skeleton of a test looks like this: +# +# #!/bin/sh +# . "${srcdir=.}/init.sh"; path_prepend_ . +# Execute some commands. +# Note that these commands are executed in a subdirectory, therefore you +# need to prepend "../" to relative filenames in the build directory. +# Note that the "path_prepend_ ." is useful only if the body of your +# test invokes programs residing in the initial directory. +# For example, if the programs you want to test are in src/, and this test +# script is named tests/test-1, then you would use "path_prepend_ ../src", +# or perhaps export PATH='$(abs_top_builddir)/src$(PATH_SEPARATOR)'"$$PATH" +# to all tests via automake's TESTS_ENVIRONMENT. +# Set the exit code 0 for success, 77 for skipped, or 1 or other for failure. +# Use the skip_ and fail_ functions to print a diagnostic and then exit +# with the corresponding exit code. +# Exit $? + +# Executing a test that uses this file +# ==================================== +# +# Running a single test: +# $ make check TESTS=test-foo.sh +# +# Running a single test, with verbose output: +# $ make check TESTS=test-foo.sh VERBOSE=yes +# +# Running a single test, keeping the temporary directory: +# $ make check TESTS=test-foo.sh KEEP=yes +# +# Running a single test, with single-stepping: +# 1. Go into a sub-shell: +# $ bash +# 2. Set relevant environment variables from TESTS_ENVIRONMENT in the +# Makefile: +# $ export srcdir=../../tests # this is an example +# 3. Execute the commands from the test, copy&pasting them one by one: +# $ . "$srcdir/init.sh"; path_prepend_ . +# ... +# 4. Finally +# $ exit + +ME_=`expr "./$0" : '.*/\(.*\)$'` + +# Prepare PATH_SEPARATOR. +# The user is always right. +if test "${PATH_SEPARATOR+set}" != set; then + # Determine PATH_SEPARATOR by trying to find /bin/sh in a PATH which + # contains only /bin. Note that ksh looks also at the FPATH variable, + # so we have to set that as well for the test. + PATH_SEPARATOR=: + (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \ + && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \ + || PATH_SEPARATOR=';' + } +fi + +# We use a trap below for cleanup. This requires us to go through +# hoops to get the right exit status transported through the handler. +# So use 'Exit STATUS' instead of 'exit STATUS' inside of the tests. +# Turn off errexit here so that we don't trip the bug with OSF1/Tru64 +# sh inside this function. +Exit () { set +e; (exit $1); exit $1; } + +# Print warnings (e.g., about skipped and failed tests) to this file number. +# Override by defining to say, 9, in init.cfg, and putting say, +# export ...ENVVAR_SETTINGS...; $(SHELL) 9>&2 +# in the definition of TESTS_ENVIRONMENT in your tests/Makefile.am file. +# This is useful when using automake's parallel tests mode, to print +# the reason for skip/failure to console, rather than to the .log files. +: ${stderr_fileno_=2} + +# Note that correct expansion of "$*" depends on IFS starting with ' '. +# Always write the full diagnostic to stderr. +# When stderr_fileno_ is not 2, also emit the first line of the +# diagnostic to that file descriptor. +warn_ () +{ + # If IFS does not start with ' ', set it and emit the warning in a subshell. + case $IFS in + ' '*) printf '%s\n' "$*" >&2 + test $stderr_fileno_ = 2 \ + || { printf '%s\n' "$*" | sed 1q >&$stderr_fileno_ ; } ;; + *) (IFS=' '; warn_ "$@");; + esac +} +fail_ () { warn_ "$ME_: failed test: $@"; Exit 1; } +skip_ () { warn_ "$ME_: skipped test: $@"; Exit 77; } +fatal_ () { warn_ "$ME_: hard error: $@"; Exit 99; } +framework_failure_ () { warn_ "$ME_: set-up failure: $@"; Exit 99; } + +# This is used to simplify checking of the return value +# which is useful when ensuring a command fails as desired. +# I.e., just doing `command ... &&fail=1` will not catch +# a segfault in command for example. With this helper you +# instead check an explicit exit code like +# returns_ 1 command ... || fail +returns_ () { + # Disable tracing so it doesn't interfere with stderr of the wrapped command + { set +x; } 2>/dev/null + + local exp_exit="$1" + shift + "$@" + test $? -eq $exp_exit && ret_=0 || ret_=1 + + if test "$VERBOSE" = yes && test "$gl_set_x_corrupts_stderr_" = false; then + set -x + fi + { return $ret_; } 2>/dev/null +} + +# Sanitize this shell to POSIX mode, if possible. +DUALCASE=1; export DUALCASE +if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then + emulate sh + NULLCMD=: + alias -g '${1+"$@"}'='"$@"' + setopt NO_GLOB_SUBST +else + case `(set -o) 2>/dev/null` in + *posix*) set -o posix ;; + esac +fi + +# We require $(...) support unconditionally. +# We require non-surprising "local" semantics (this eliminates dash). +# This takes the admittedly draconian step of eliminating dash, because the +# assignment tab=$(printf '\t') works fine, yet preceding it with "local " +# transforms it into an assignment that sets the variable to the empty string. +# That is too counter-intuitive, and can lead to subtle run-time malfunction. +# The example below is less subtle in that with dash, it evokes the run-time +# exception "dash: 1: local: 1: bad variable name". +# We require a few additional shell features only when $EXEEXT is nonempty, +# in order to support automatic $EXEEXT emulation: +# - hyphen-containing alias names +# - we prefer to use ${var#...} substitution, rather than having +# to work around lack of support for that feature. +# The following code attempts to find a shell with support for these features. +# If the current shell passes the test, we're done. Otherwise, test other +# shells until we find one that passes. If one is found, re-exec it. +# If no acceptable shell is found, skip the current test. +# +# The "...set -x; P=1 true 2>err..." test is to disqualify any shell that +# emits "P=1" into err, as /bin/sh from SunOS 5.11 and OpenBSD 4.7 do. +# +# Use "9" to indicate success (rather than 0), in case some shell acts +# like Solaris 10's /bin/sh but exits successfully instead of with status 2. + +# Eval this code in a subshell to determine a shell's suitability. +# 10 - passes all tests; ok to use +# 9 - ok, but enabling "set -x" corrupts app stderr; prefer higher score +# ? - not ok +gl_shell_test_script_=' +test $(echo y) = y || exit 1 +f_local_() { local v=1; }; f_local_ || exit 1 +f_dash_local_fail_() { local t=$(printf " 1"); }; f_dash_local_fail_ +score_=10 +if test "$VERBOSE" = yes; then + test -n "$( (exec 3>&1; set -x; P=1 true 2>&3) 2> /dev/null)" && score_=9 +fi +test -z "$EXEEXT" && exit $score_ +shopt -s expand_aliases +alias a-b="echo zoo" +v=abx + test ${v%x} = ab \ + && test ${v#a} = bx \ + && test $(a-b) = zoo \ + && exit $score_ +' + +if test "x$1" = "x--no-reexec"; then + shift +else + # Assume a working shell. Export to subshells (setup_ needs this). + gl_set_x_corrupts_stderr_=false + export gl_set_x_corrupts_stderr_ + + # Record the first marginally acceptable shell. + marginal_= + + # Search for a shell that meets our requirements. + for re_shell_ in __current__ "${CONFIG_SHELL:-no_shell}" \ + /bin/sh bash dash zsh pdksh fail + do + test "$re_shell_" = no_shell && continue + + # If we've made it all the way to the sentinel, "fail" without + # finding even a marginal shell, skip this test. + if test "$re_shell_" = fail; then + test -z "$marginal_" && skip_ failed to find an adequate shell + re_shell_=$marginal_ + break + fi + + # When testing the current shell, simply "eval" the test code. + # Otherwise, run it via $re_shell_ -c ... + if test "$re_shell_" = __current__; then + # 'eval'ing this code makes Solaris 10's /bin/sh exit with + # $? set to 2. It does not evaluate any of the code after the + # "unexpected" first '('. Thus, we must run it in a subshell. + ( eval "$gl_shell_test_script_" ) > /dev/null 2>&1 + else + "$re_shell_" -c "$gl_shell_test_script_" 2>/dev/null + fi + + st_=$? + + # $re_shell_ works just fine. Use it. + if test $st_ = 10; then + gl_set_x_corrupts_stderr_=false + break + fi + + # If this is our first marginally acceptable shell, remember it. + if test "$st_:$marginal_" = 9: ; then + marginal_="$re_shell_" + gl_set_x_corrupts_stderr_=true + fi + done + + if test "$re_shell_" != __current__; then + # Found a usable shell. Preserve -v and -x. + case $- in + *v*x* | *x*v*) opts_=-vx ;; + *v*) opts_=-v ;; + *x*) opts_=-x ;; + *) opts_= ;; + esac + re_shell=$re_shell_ + export re_shell + exec "$re_shell_" $opts_ "$0" --no-reexec "$@" + echo "$ME_: exec failed" 1>&2 + exit 127 + fi +fi + +# If this is bash, turn off all aliases. +test -n "$BASH_VERSION" && unalias -a + +# Note that when supporting $EXEEXT (transparently mapping from PROG_NAME to +# PROG_NAME.exe), we want to support hyphen-containing names like test-acos. +# That is part of the shell-selection test above. Why use aliases rather +# than functions? Because support for hyphen-containing aliases is more +# widespread than that for hyphen-containing function names. +test -n "$EXEEXT" && test -n "$BASH_VERSION" && shopt -s expand_aliases + +# Enable glibc's malloc-perturbing option. +# This is useful for exposing code that depends on the fact that +# malloc-related functions often return memory that is mostly zeroed. +# If you have the time and cycles, use valgrind to do an even better job. +: ${MALLOC_PERTURB_=87} +export MALLOC_PERTURB_ + +# This is a stub function that is run upon trap (upon regular exit and +# interrupt). Override it with a per-test function, e.g., to unmount +# a partition, or to undo any other global state changes. +cleanup_ () { :; } + +# Emit a header similar to that from diff -u; Print the simulated "diff" +# command so that the order of arguments is clear. Don't bother with @@ lines. +emit_diff_u_header_ () +{ + printf '%s\n' "diff -u $*" \ + "--- $1 1970-01-01" \ + "+++ $2 1970-01-01" +} + +# Arrange not to let diff or cmp operate on /dev/null, +# since on some systems (at least OSF/1 5.1), that doesn't work. +# When there are not two arguments, or no argument is /dev/null, return 2. +# When one argument is /dev/null and the other is not empty, +# cat the nonempty file to stderr and return 1. +# Otherwise, return 0. +compare_dev_null_ () +{ + test $# = 2 || return 2 + + if test "x$1" = x/dev/null; then + test -s "$2" || return 0 + emit_diff_u_header_ "$@"; sed 's/^/+/' "$2" + return 1 + fi + + if test "x$2" = x/dev/null; then + test -s "$1" || return 0 + emit_diff_u_header_ "$@"; sed 's/^/-/' "$1" + return 1 + fi + + return 2 +} + +for diff_opt_ in -u -U3 -c '' no; do + test "$diff_opt_" != no && + diff_out_=`exec 2>/dev/null; diff $diff_opt_ "$0" "$0" < /dev/null` && + break +done +if test "$diff_opt_" != no; then + if test -z "$diff_out_"; then + compare_ () { diff $diff_opt_ "$@"; } + else + compare_ () + { + # If no differences were found, AIX and HP-UX 'diff' produce output + # like "No differences encountered". Hide this output. + diff $diff_opt_ "$@" > diff.out + diff_status_=$? + test $diff_status_ -eq 0 || cat diff.out || diff_status_=2 + rm -f diff.out || diff_status_=2 + return $diff_status_ + } + fi +elif cmp -s /dev/null /dev/null 2>/dev/null; then + compare_ () { cmp -s "$@"; } +else + compare_ () { cmp "$@"; } +fi + +# Usage: compare EXPECTED ACTUAL +# +# Given compare_dev_null_'s preprocessing, defer to compare_ if 2 or more. +# Otherwise, propagate $? to caller: any diffs have already been printed. +compare () +{ + # This looks like it can be factored to use a simple "case $?" + # after unchecked compare_dev_null_ invocation, but that would + # fail in a "set -e" environment. + if compare_dev_null_ "$@"; then + return 0 + else + case $? in + 1) return 1;; + *) compare_ "$@";; + esac + fi +} + +# An arbitrary prefix to help distinguish test directories. +testdir_prefix_ () { printf gt; } + +# Run the user-overridable cleanup_ function, remove the temporary +# directory and exit with the incoming value of $?. +remove_tmp_ () +{ + __st=$? + cleanup_ + if test "$KEEP" = yes; then + echo "Not removing temporary directory $test_dir_" + else + # cd out of the directory we're about to remove + cd "$initial_cwd_" || cd / || cd /tmp + chmod -R u+rwx "$test_dir_" + # If removal fails and exit status was to be 0, then change it to 1. + rm -rf "$test_dir_" || { test $__st = 0 && __st=1; } + fi + exit $__st +} + +# Given a directory name, DIR, if every entry in it that matches *.exe +# contains only the specified bytes (see the case stmt below), then print +# a space-separated list of those names and return 0. Otherwise, don't +# print anything and return 1. Naming constraints apply also to DIR. +find_exe_basenames_ () +{ + feb_dir_=$1 + feb_fail_=0 + feb_result_= + feb_sp_= + for feb_file_ in $feb_dir_/*.exe; do + # If there was no *.exe file, or there existed a file named "*.exe" that + # was deleted between the above glob expansion and the existence test + # below, just skip it. + test "x$feb_file_" = "x$feb_dir_/*.exe" && test ! -f "$feb_file_" \ + && continue + # Exempt [.exe, since we can't create a function by that name, yet + # we can't invoke [ by PATH search anyways due to shell builtins. + test "x$feb_file_" = "x$feb_dir_/[.exe" && continue + case $feb_file_ in + *[!-a-zA-Z/0-9_.+]*) feb_fail_=1; break;; + *) # Remove leading file name components as well as the .exe suffix. + feb_file_=${feb_file_##*/} + feb_file_=${feb_file_%.exe} + feb_result_="$feb_result_$feb_sp_$feb_file_";; + esac + feb_sp_=' ' + done + test $feb_fail_ = 0 && printf %s "$feb_result_" + return $feb_fail_ +} + +# Consider the files in directory, $1. +# For each file name of the form PROG.exe, create an alias named +# PROG that simply invokes PROG.exe, then return 0. If any selected +# file name or the directory name, $1, contains an unexpected character, +# define no alias and return 1. +create_exe_shims_ () +{ + case $EXEEXT in + '') return 0 ;; + .exe) ;; + *) echo "$0: unexpected \$EXEEXT value: $EXEEXT" 1>&2; return 1 ;; + esac + + base_names_=`find_exe_basenames_ $1` \ + || { echo "$0 (exe_shim): skipping directory: $1" 1>&2; return 0; } + + if test -n "$base_names_"; then + for base_ in $base_names_; do + alias "$base_"="$base_$EXEEXT" + done + fi + + return 0 +} + +# Use this function to prepend to PATH an absolute name for each +# specified, possibly-$initial_cwd_-relative, directory. +path_prepend_ () +{ + while test $# != 0; do + path_dir_=$1 + case $path_dir_ in + '') fail_ "invalid path dir: '$1'";; + /* | ?:*) abs_path_dir_=$path_dir_;; + *) abs_path_dir_=$initial_cwd_/$path_dir_;; + esac + case $abs_path_dir_ in + *$PATH_SEPARATOR*) fail_ "invalid path dir: '$abs_path_dir_'";; + esac + PATH="$abs_path_dir_$PATH_SEPARATOR$PATH" + + # Create an alias, FOO, for each FOO.exe in this directory. + create_exe_shims_ "$abs_path_dir_" \ + || fail_ "something failed (above): $abs_path_dir_" + shift + done + export PATH +} + +setup_ () +{ + if test "$VERBOSE" = yes; then + # Test whether set -x may cause the selected shell to corrupt an + # application's stderr. Many do, including zsh-4.3.10 and the /bin/sh + # from SunOS 5.11, OpenBSD 4.7 and Irix 5.x and 6.5. + # If enabling verbose output this way would cause trouble, simply + # issue a warning and refrain. + if $gl_set_x_corrupts_stderr_; then + warn_ "using SHELL=$SHELL with 'set -x' corrupts stderr" + else + set -x + fi + fi + + initial_cwd_=$PWD + + pfx_=`testdir_prefix_` + test_dir_=`mktempd_ "$initial_cwd_" "$pfx_-$ME_.XXXX"` \ + || fail_ "failed to create temporary directory in $initial_cwd_" + cd "$test_dir_" || fail_ "failed to cd to temporary directory" + + # As autoconf-generated configure scripts do, ensure that IFS + # is defined initially, so that saving and restoring $IFS works. + gl_init_sh_nl_=' +' + IFS=" "" $gl_init_sh_nl_" + + # This trap statement, along with a trap on 0 below, ensure that the + # temporary directory, $test_dir_, is removed upon exit as well as + # upon receipt of any of the listed signals. + for sig_ in 1 2 3 13 15; do + eval "trap 'Exit $(expr $sig_ + 128)' $sig_" + done +} + +# Create a temporary directory, much like mktemp -d does. +# Written by Jim Meyering. +# +# Usage: mktempd_ /tmp phoey.XXXXXXXXXX +# +# First, try to use the mktemp program. +# Failing that, we'll roll our own mktemp-like function: +# - try to get random bytes from /dev/urandom +# - failing that, generate output from a combination of quickly-varying +# sources and gzip. Ignore non-varying gzip header, and extract +# "random" bits from there. +# - given those bits, map to file-name bytes using tr, and try to create +# the desired directory. +# - make only $MAX_TRIES_ attempts + +# Helper function. Print $N pseudo-random bytes from a-zA-Z0-9. +rand_bytes_ () +{ + n_=$1 + + # Maybe try openssl rand -base64 $n_prime_|tr '+/=\012' abcd first? + # But if they have openssl, they probably have mktemp, too. + + chars_=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 + dev_rand_=/dev/urandom + if test -r "$dev_rand_"; then + # Note: 256-length($chars_) == 194; 3 copies of $chars_ is 186 + 8 = 194. + dd ibs=$n_ count=1 if=$dev_rand_ 2>/dev/null \ + | LC_ALL=C tr -c $chars_ 01234567$chars_$chars_$chars_ + return + fi + + n_plus_50_=`expr $n_ + 50` + cmds_='date; date +%N; free; who -a; w; ps auxww; ps -ef' + data_=` (eval "$cmds_") 2>&1 | gzip ` + + # Ensure that $data_ has length at least 50+$n_ + while :; do + len_=`echo "$data_"|wc -c` + test $n_plus_50_ -le $len_ && break; + data_=` (echo "$data_"; eval "$cmds_") 2>&1 | gzip ` + done + + echo "$data_" \ + | dd bs=1 skip=50 count=$n_ 2>/dev/null \ + | LC_ALL=C tr -c $chars_ 01234567$chars_$chars_$chars_ +} + +mktempd_ () +{ + case $# in + 2);; + *) fail_ "Usage: mktempd_ DIR TEMPLATE";; + esac + + destdir_=$1 + template_=$2 + + MAX_TRIES_=4 + + # Disallow any trailing slash on specified destdir: + # it would subvert the post-mktemp "case"-based destdir test. + case $destdir_ in + / | //) destdir_slash_=$destdir;; + */) fail_ "invalid destination dir: remove trailing slash(es)";; + *) destdir_slash_=$destdir_/;; + esac + + case $template_ in + *XXXX) ;; + *) fail_ \ + "invalid template: $template_ (must have a suffix of at least 4 X's)";; + esac + + # First, try to use mktemp. + d=`unset TMPDIR; { mktemp -d -t -p "$destdir_" "$template_"; } 2>/dev/null` && + + # The resulting name must be in the specified directory. + case $d in "$destdir_slash_"*) :;; *) false;; esac && + + # It must have created the directory. + test -d "$d" && + + # It must have 0700 permissions. Handle sticky "S" bits. + perms=`ls -dgo "$d" 2>/dev/null` && + case $perms in drwx--[-S]---*) :;; *) false;; esac && { + echo "$d" + return + } + + # If we reach this point, we'll have to create a directory manually. + + # Get a copy of the template without its suffix of X's. + base_template_=`echo "$template_"|sed 's/XX*$//'` + + # Calculate how many X's we've just removed. + template_length_=`echo "$template_" | wc -c` + nx_=`echo "$base_template_" | wc -c` + nx_=`expr $template_length_ - $nx_` + + err_= + i_=1 + while :; do + X_=`rand_bytes_ $nx_` + candidate_dir_="$destdir_slash_$base_template_$X_" + err_=`mkdir -m 0700 "$candidate_dir_" 2>&1` \ + && { echo "$candidate_dir_"; return; } + test $MAX_TRIES_ -le $i_ && break; + i_=`expr $i_ + 1` + done + fail_ "$err_" +} + +# If you want to override the testdir_prefix_ function, +# or to add more utility functions, use this file. +test -f "$srcdir/init.cfg" \ + && . "$srcdir/init.cfg" + +setup_ "$@" +# This trap is here, rather than in the setup_ function, because some +# shells run the exit trap at shell function exit, rather than script exit. +trap remove_tmp_ 0 diff --git a/tests/lang-default b/tests/lang-default new file mode 100644 index 00000000..882672fc --- /dev/null +++ b/tests/lang-default @@ -0,0 +1,10 @@ +#!/bin/sh +# Set locale-related environment variables so we get consistent +# message translations, time formats, sort orderings, etc. + +LC_ALL=C +export LC_ALL +unset LANGUAGE NLSPATH + +# These settings shouldn't matter, but unset them anyway just in case. +unset LANG LC_COLLATE LC_CTYPE LC_MESSAGES LC_MONETARY LC_NUMERIC LC_TIME diff --git a/tests/local.mk b/tests/local.mk new file mode 100644 index 00000000..894f612d --- /dev/null +++ b/tests/local.mk @@ -0,0 +1,109 @@ +## Process this file with automake to produce Makefile.in -*-Makefile-*-. + +## Copyright (C) 2007-2019 Free Software Foundation, Inc. + +## 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 3 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 . + +built_programs = find oldfind xargs frcode locate updatedb + +# Indirections required so that we'll still be able to know the +# complete list of our tests even if the user overrides TESTS +# from the command line (as permitted by the test harness API). +TESTS = $(all_tests) +root_tests = $(all_root_tests) + +EXTRA_DIST += $(all_tests) + +TEST_EXTENSIONS = .sh + +SH_LOG_COMPILER = $(SHELL) + +# We don't want this to go in the top-level directory. +TEST_SUITE_LOG = tests/test-suite.log + +# Note that the first lines are statements. They ensure that environment +# variables that can perturb tests are unset or set to expected values. +# The rest are envvar settings that propagate build-related Makefile +# variables to test scripts. +TESTS_ENVIRONMENT = \ + . $(srcdir)/tests/lang-default; \ + tmp__=$${TMPDIR-/tmp}; \ + test -d "$$tmp__" && test -w "$$tmp__" || tmp__=.; \ + . $(srcdir)/tests/envvar-check; \ + TMPDIR=$$tmp__; export TMPDIR; \ + export \ + VERSION='$(VERSION)' \ + LOCALE_FR='$(LOCALE_FR)' \ + LOCALE_FR_UTF8='$(LOCALE_FR_UTF8)' \ + abs_top_builddir='$(abs_top_builddir)' \ + abs_top_srcdir='$(abs_top_srcdir)' \ + abs_srcdir='$(abs_srcdir)' \ + built_programs='$(built_programs) $(single_binary_progs)' \ + fail=0 \ + host_os=$(host_os) \ + host_triplet='$(host_triplet)' \ + srcdir='$(srcdir)' \ + top_srcdir='$(top_srcdir)' \ + CONFIG_HEADER='$(abs_top_builddir)/$(CONFIG_INCLUDE)' \ + CC='$(CC)' \ + AWK='$(AWK)' \ + EGREP='$(EGREP)' \ + EXEEXT='$(EXEEXT)' \ + MAKE=$(MAKE) \ + PACKAGE_VERSION=$(PACKAGE_VERSION) \ + PERL='$(PERL)' \ + SHELL='$(PREFERABLY_POSIX_SHELL)' \ + ; test -d /usr/xpg4/bin && PATH='/usr/xpg4/bin$(PATH_SEPARATOR)'"$$PATH"; \ + PATH='$(abs_top_builddir)/find$(PATH_SEPARATOR)$(abs_top_builddir)/locate$(PATH_SEPARATOR)$(abs_top_builddir)/xargs$(PATH_SEPARATOR)'"$$PATH" \ + ; 9>&2 + +# On failure, display the global testsuite log on stdout. +VERBOSE = yes + +EXTRA_DIST += \ + init.cfg \ + tests/envvar-check \ + tests/init.sh \ + tests/lang-default \ + tests/other-fs-tmpdir \ + tests/sample-test + +all_root_tests = + +ALL_RECURSIVE_TARGETS += check-root +.PHONY: check-root +check-root: + $(MAKE) check TESTS='$(root_tests)' SUBDIRS=. + +# Do not choose a name that is a shell keyword like 'if', or a +# commonly-used utility like 'cat' or 'test', as the name of a test. +# Otherwise, VPATH builds will fail on hosts like Solaris, since they +# will expand 'if test ...' to 'if .../test ...', and the '.../test' +# will execute the test script rather than the standard utility. + +# Notes on the ordering of these tests: +# Place early in the list tests of the tools that +# are most commonly used in test scripts themselves. +# E.g., nearly every test script uses rm and chmod. +# help-version comes early because it's a basic sanity test. +# Put seq early, since lots of other tests use it. +# Put tests that sleep early, but not all together, so in parallel builds +# they share time with tests that burn CPU, not with others that sleep. +# Put head-elide-tail early, because it's long-running. + +all_tests = \ + tests/misc/help-version.sh \ + $(all_root_tests) + +$(TEST_LOGS): $(PROGRAMS) diff --git a/tests/misc/help-version.sh b/tests/misc/help-version.sh new file mode 100755 index 00000000..ea622421 --- /dev/null +++ b/tests/misc/help-version.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# Make sure all of these programs work properly +# when invoked with --help or --version. + +# Copyright (C) 2019 Free Software Foundation, Inc. + +# 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 3 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 . + +. "${srcdir=.}/tests/init.sh" + +# Terminate any background processes +cleanup_() { kill $pid 2>/dev/null && wait $pid; } +echo PATH=$PATH +test "$built_programs" \ + || fail_ "built_programs not specified!?!" + +test "$VERSION" \ + || fail_ "set envvar VERSION; it is required for a PATH sanity-check" + +# Extract version from --version output of the first program +for i in $built_programs; do + v=$(set -x; env $i --version | sed -n '1s/.* //p;q') + break +done + +# Ensure that it matches $VERSION. +test "x$v" = "x$VERSION" \ + || fail_ "--version-\$VERSION mismatch" + +for i in $built_programs; do + # Make sure they exit successfully, under normal conditions. + env $i --help >/dev/null || fail=1 + env $i --version >/dev/null || fail=1 + + # Make sure they fail upon 'disk full' error. + if test -w /dev/full && test -c /dev/full; then + prog=$(set -x; echo $i|sed "s/$EXEEXT$//"); + eval "expected=\$expected_failure_status_$prog" + test x$expected = x && expected=1 + + returns_ $expected env $i --help >/dev/full 2>/dev/null && + returns_ $expected env $i --version >/dev/full 2>/dev/null || + { + fail=1 + env $i --help >/dev/full 2>/dev/null + status=$? + echo "*** $i: bad exit status '$status' (expected $expected)," 1>&2 + echo " with --help or --version output redirected to /dev/full" 1>&2 + } + fi +done + +Exit $fail diff --git a/tests/other-fs-tmpdir b/tests/other-fs-tmpdir new file mode 100644 index 00000000..8efae3d1 --- /dev/null +++ b/tests/other-fs-tmpdir @@ -0,0 +1,55 @@ +#!/bin/sh + +# Use stat to find a writable directory on a file system different from that +# of the current directory. If one is found, create a temporary directory +# inside it. + +# Copyright (C) 1998-2019 Free Software Foundation, Inc. + +# 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 3 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 . + +test "${CANDIDATE_TMP_DIRS+set}" = set \ + || CANDIDATE_TMP_DIRS="$TMPDIR /tmp /dev/shm /var/tmp /usr/tmp $HOME" + +other_partition_tmpdir= + +dot_mount_point=$(stat -c %d .) +for d in $CANDIDATE_TMP_DIRS; do + + # Skip nonexistent directories. + test -d "$d" || continue + + d_mount_point=$(stat -L -c %d "$d") + + # Same partition? Skip it. + test "x$d_mount_point" = "x$dot_mount_point" && continue + + # See if we can create a directory in it. + if mkdir "$d/tmp$$" > /dev/null 2>&1; then + other_partition_tmpdir="$d/tmp$$" + break + fi + +done + +if test -z "$other_partition_tmpdir"; then + skip_ \ +"requires a writable directory on a different disk partition, +and I couldn't find one. I tried these: + $CANDIDATE_TMP_DIRS +Set your environment variable CANDIDATE_TMP_DIRS to make +this test use a different list." +fi + +test "$VERBOSE" = yes && set -x diff --git a/tests/sample-test b/tests/sample-test new file mode 100644 index 00000000..a8ff24d6 --- /dev/null +++ b/tests/sample-test @@ -0,0 +1,36 @@ +#!/bin/sh +# FIXME + +# Copyright (C) 2019 Free Software Foundation, Inc. + +# 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 3 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 . + +. "${srcdir=.}/tests/init.sh" +print_ver_ FIXME + +# FIXME: skip_if_root_ +# FIXME: require_root_ + +# If used, these must *follow* init.sh. +# FIXME: cleanup_() { rm -rf "$other_partition_tmpdir"; } +# FIXME: . "$abs_srcdir/tests/other-fs-tmpdir" + +FIXME > out || fail=1 +cat <<\EOF > exp || framework_failure_ +FIXME +EOF + +compare exp out || fail=1 + +Exit $fail -- 2.20.1