# # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (the "License"). # You may not use this file except in compliance with the License. # # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE # or http://www.opensolaris.org/os/licensing. # See the License for the specific language governing permissions # and limitations under the License. # # When distributing Covered Code, include this CDDL HEADER in each # file and include the License file at usr/src/OPENSOLARIS.LICENSE. # If applicable, add the following below this CDDL HEADER, with the # fields enclosed by brackets "[]" replaced with your own identifying # information: Portions Copyright [yyyy] [name of copyright owner] # # CDDL HEADER END # # # Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. # # # Written by Roland Mainz # # HACK BEGIN # this is a HACK to disable the VM* debugging, otherwise the test runs for # two hour on our SPARC if [[ ! -v NOVMDEBUG ]] ; then unset VMALLOC_OPTIONS VMDEBUG NOVMDEBUG=1 exec ${SHELL} "$0" "$@" fi # HACK END # test setup function err_exit { print -u2 -n "\t" print -u2 -r ${Command}[$1]: "${@:2}" (( Errors < 127 && Errors++ )) } alias err_exit='err_exit $LINENO' set -o nounset Command=${0##*/} integer Errors=0 typeset ocwd typeset tmpdir typeset out # create temporary test directory ocwd="$PWD" tmpdir="$(mktemp -t -d "test_sun_solaris_command_substitution.XXXXXXXX")" || err_exit "Cannot create temporary directory" cd "${tmpdir}" || { err_exit "cd ${tmpdir} failed." ; exit $((Errors)) ; } function isvalidpid { kill -0 ${1} 2>/dev/null && return 0 return 1 } function pretests { cat <'myseq' #!${SHELL} # script version of "seq" function main { integer i integer arg1=\$1 integer arg2=\$2 integer arg3=\$3 case \$# in 1) for (( i=1. ; i <= arg1 ; i=i+1. )) ; do printf '%d\n' i done ;; 2) for (( i=arg1 ; i <= arg2 ; i=i+1. )) ; do printf '%d\n' i done ;; 3) for (( i=arg1 ; i <= arg3 ; i+=arg2 )) ; do printf '%d\n' i done ;; *) print -u2 -f '%s: Illegal number of arguments %d\n' "\$0" \$# return 1 ;; esac return 0 } set -o noglob IFS='' main "\$@" exit \$? EOF chmod a+rx 'myseq' return 0 } function posttests { rm 'myseq' } # test1: run loop and check various temp filesizes # (Please keep this test syncted with sun_solaris_cr_6800929_large_command_substitution_hang.sh) function test1 { integer childpid integer maxwait integer i integer testfilesize typeset tmpfile='test1tmpfile' integer testid compound -r test1=( compound -r -a testcases=( # test 1a: Run test child for $(...) # (note the pipe chain has to end in a builtin command, an external command may not trigger the bug) ( name="test1a" cmd="builtin cat ; print -- \"\$(cat \"${tmpfile}\" | cat)\" ; true" ) # test 1b: Same as test1a but uses ${... ; } instead if $(...) ( name="test1b" cmd="builtin cat ; print -- \"\${ cat \"${tmpfile}\" | cat ; }\" ; true" ) # test 1c: Same as test1a but does not use a pipe ( name="test1c" cmd="builtin cat ; print -- \"\$(cat \"${tmpfile}\" ; true)\" ; true" ) # test 1d: Same as test1a but does not use a pipe ( name="test1d" cmd="builtin cat ; print -- \"\${ cat \"${tmpfile}\" ; true ; }\" ; true" ) # test 1e: Same as test1a but uses an external "cat" command ( name="test1e" cmd="builtin -d cat /bin/cat ; print -- \"\$(cat \"${tmpfile}\" | cat)\" ; true" ) # test 1f: Same as test1a but uses an external "cat" command ( name="test1f" cmd="builtin -d cat /bin/cat ; print -- \"\${ cat \"${tmpfile}\" | cat ; }\" ; true" ) # test 1g: Same as test1a but uses an external "cat" command ( name="test1g" cmd="builtin -d cat /bin/cat ; print -- \"\$(cat \"${tmpfile}\" ; true)\" ; true" ) # test 1h: Same as test1a but uses an external "cat" command ( name="test1h" cmd="builtin -d cat /bin/cat ; print -- \"\${ cat \"${tmpfile}\" ; true ; }\" ; true" ) ) ) for (( testfilesize=1*1024 ; testfilesize <= 1024*1024 ; testfilesize*=2 )) ; do # Create temp file { for (( i=0 ; i < testfilesize ; i+=64 )) ; do print '0123456789abcdef01234567890ABCDEF0123456789abcdef01234567890ABCDE' done } >"${tmpfile}" # wait up to log2(i) seconds for the child to terminate # (this is 10 seconds for 1KB and 19 seconds for 512KB) (( maxwait=log2(testfilesize) )) for testid in "${!test1.testcases[@]}" ; do nameref currtst=test1.testcases[testid] ${SHELL} -o errexit -c "${currtst.cmd}" >"${tmpfile}.out" & (( childpid=$! )) for (( i=0 ; i < maxwait ; i++ )) ; do isvalidpid ${childpid} || break sleep 0.25 done if isvalidpid ${childpid} ; then err_exit "${currtst.name}: child (pid=${childpid}) still busy, filesize=${testfilesize}." kill -KILL ${childpid} 2>/dev/null fi wait || err_exit "${currtst.name}: Child returned non-zero exit code." # wait for child (and/or avoid zombies/slime) # compare input/output cmp -s "${tmpfile}" "${tmpfile}.out" || err_exit "${currtst.name}: ${tmpfile} and ${tmpfile}.out differ, filesize=${testfilesize}." rm "${tmpfile}.out" done # Cleanup rm "${tmpfile}" done return 0 } # test2: If a command substitution calls a function and that function contains # a command substitution which contains a piped command, the original # command substitution calling the function will return 127 instead of 0. # This is causing problems in several VSC tests. # If we remove the piped command from the simple case in the attached script, # it returns 0. function test2 { typeset str typeset testbody typeset testout testbody=$( # means command substitution start, means command substitution end cat < printf "hi" | tr "h" "H" echo \$pipedcmd return 0 } foo=myfunc retval=\$? if [ "\$foo"X != "HiX" ]; then echo "myfunc returned '\${foo}'; expected 'Hi'" fi if [ \$retval -ne 0 ]; then echo "command substitution calling myfunc returned \"\${retval}\"; expected 0" else echo "command substitution calling myfunc successfully returned 0" fi EOF ) # Test 002/a: Plain test testout=${ printf "%B\n" testbody | sed 's//$(/g;s//)/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == "command substitution calling myfunc successfully returned 0" ]] || err_exit "Expected 'command substitution calling myfunc successfully returned 0', got ${testout}" # Test 002/b: Same as test002/a but replaces "$(" with "${" testout=${ printf "%B\n" testbody | sed 's//${ /g;s// ; }/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == "command substitution calling myfunc successfully returned 0" ]] || err_exit "Expected 'command substitution calling myfunc successfully returned 0', got ${testout}" # Test 002/c: Same as test002/a but forces |fork()| for a subshell via "ulimit -c 0" testout=${ printf "%B\n" testbody | sed 's//$( ulimit -c 0 ; /g;s//)/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == "command substitution calling myfunc successfully returned 0" ]] || err_exit "Expected 'command substitution calling myfunc successfully returned 0', got ${testout}" # Test 002/d: Same as test002/a but uses extra subshell testout=${ printf "%B\n" testbody | sed 's//$( ( /g;s//) )/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == "command substitution calling myfunc successfully returned 0" ]] || err_exit "Expected 'command substitution calling myfunc successfully returned 0', got ${testout}" # Test 002/e: Same as test002/b but uses extra subshell after "${ " testout=${ printf "%B\n" testbody | sed 's//${ ( /g;s//) ; }/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == "command substitution calling myfunc successfully returned 0" ]] || err_exit "Expected 'command substitution calling myfunc successfully returned 0', got ${testout}" return 0 } # test3: An expression within backticks which should return false, instead # returns true (0). function test3 { typeset str typeset testbody typeset testout testbody=$( # means command substitution start, means command substitution end cat <expr "NOMATCH" : ".*Z" > /dev/null ; then echo "xerror" else echo "xok" fi EOF ) # Test 003/a: Plain test testout=${ printf "%B\n" testbody | sed 's//$(/g;s//)/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == 'xok' ]] || err_exit "Expected 'xok', got ${testout}" # Test 003/b: Same as test003/a but replaces "$(" with "${" testout=${ printf "%B\n" testbody | sed 's//${ /g;s// ; }/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == 'xok' ]] || err_exit "Expected 'xok', got ${testout}" # Test 003/c: Same as test003/a but forces |fork()| for a subshell via "ulimit -c 0" testout=${ printf "%B\n" testbody | sed 's//$( ulimit -c 0 ; /g;s//)/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == 'xok' ]] || err_exit "Expected 'xok', got ${testout}" # Test 003/d: Same as test003/a but uses extra subshell testout=${ printf "%B\n" testbody | sed 's//$( ( /g;s//) )/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == 'xok' ]] || err_exit "Expected 'xok', got ${testout}" # Test 003/e: Same as test003/b but uses extra subshell after "${ " testout=${ printf "%B\n" testbody | sed 's//${ ( /g;s//) ; }/g' | ${SHELL} 2>&1 || err_exit "command returned exit code $?" } [[ "${testout}" == 'xok' ]] || err_exit "Expected 'xok', got ${testout}" return 0 } ######################################################################## #### test set 004: # test pipe within ${... ; } command subtitution ending in a # non-builtin command (therefore we use "/bin/cat" instead of "cat" below # to force the use of the external "cat" command). ast-ksh.2009-01-20 # had a bug which caused this test to fail. function test4 { typeset testout testout=$( ${SHELL} -c 'pipedcmd=${ printf "hi" | /bin/cat ; } ; print $pipedcmd' ) [[ "${testout}" == 'hi' ]] || err_exit "test004: Expected 'hi', got '${testout}'" return 0 } ######################################################################## #### test set 005: # Test whether the shell may hang in a # 'exec 5>/dev/null; print $(eval ls -d . 2>&1 1>&5)' # Originally discovered with ast-ksh.2009-05-05 which hung in # the "configure" script of postgresql-8.3.7.tar.gz (e.g. # configure --enable-thread-safety --without-readline) function test5 { integer testid integer maxwait integer i integer childpid typeset tmpfile='test5_tmpfile' compound -r -a testcases=( # gsf's reduced testcase ( name='test5_a' cmd='exec 5>/dev/null; print $(eval ls -d . 2>&1 1>&5)done' ) # gisburn's reduced testcase ( name='test5_b' cmd='exec 5>/dev/null; print $(eval "/bin/printf hello\n" 2>&1 1>&5)done' ) ## The following tests do not trigger the problem but are included here for completeness ## and to make sure we don't get other incarnations of the same problem later... # same as test5_a but uses ${ ... ; } instead of $(...) ( name='test5_c' cmd='exec 5>/dev/null; print "${ eval ls -d . 2>&1 1>&5 ;}done"' ) # same as test5_b but uses ${ ... ; } instead of $(...) ( name='test5_d' cmd='exec 5>/dev/null; print "${ eval "/bin/printf hello\n" 2>&1 1>&5 ;}done"' ) # same as test5_a but uses "ulimit -c 0" to force the shell to use a seperare process for $(...) ( name='test5_e' cmd='exec 5>/dev/null; print $(ulimit -c 0 ; eval ls -d . 2>&1 1>&5)done' ) # same as test5_b but uses "ulimit -c 0" to force the shell to use a seperare process for $(...) ( name='test5_f' cmd='exec 5>/dev/null; print $(ulimit -c 0 ; eval "/bin/printf hello\n" 2>&1 1>&5)done' ) ) maxwait=5 for testid in "${!testcases[@]}" ; do nameref currtst=testcases[testid] ${SHELL} -o errexit -c "${currtst.cmd}" >"${tmpfile}.out" & (( childpid=$! )) for (( i=0 ; i < maxwait ; i++ )) ; do isvalidpid ${childpid} || break sleep 0.25 done if isvalidpid ${childpid} ; then err_exit "${currtst.name}: child (pid=${childpid}) still busy." kill -KILL ${childpid} 2>/dev/null fi wait || err_exit "${currtst.name}: Child returned non-zero exit code." # wait for child (and/or avoid zombies/slime) testout="$( < "${tmpfile}.out" )" rm "${tmpfile}.out" || err_exit "File '${tmpfile}.out' could not be removed." [[ "${testout}" == 'done' ]] || err_exit "test '${currtst.name}' failed, expected 'done', got '${testout}'" done return 0 } # left/right type typeset -T lr_t=( typeset left typeset right ) # test6: Programatically iterate over all combinations of tested # command substitutions with large amounts of data function test6 { integer childpid integer i typeset cmd typeset child_killed # bool typeset -a test_list integer tli # test_list index # basic test command (this should be "myseq 30000" but the "cat" below is faster) typeset -r basecmd='/bin/cat "tmpfile.expected"' integer cl typeset -r -a cmdlist=( # plain " ${basecmd} " " exec ${basecmd} " " ${basecmd} ; true " # plain pipe " ${basecmd} | cat " " ${basecmd} | cat ; true " " ${basecmd} | /bin/cat " " ${basecmd} | /bin/cat ; true " # pipe in subshell # ("ulimit -c 0" is used to force the a subshell to call |fork()|) " ${basecmd} | ( cat ) " " ${basecmd} | ( cat ; true ) " " ${basecmd} | ( /bin/cat ) " " ${basecmd} | ( exec /bin/cat ) " " ${basecmd} | ( ulimit -c 0 ; /bin/true ; exec /bin/cat ) " " ${basecmd} | ( /bin/cat ; true ) " # extra (operators not part of original problem but we cover then here to be sure) ' /bin/cat <( /bin/cat "tmpfile.expected" ) ' ' /bin/cat <( /bin/cat <( /bin/cat "tmpfile.expected" ) ) ' ' cat <( cat "tmpfile.expected" ) ' ' cat <( cat <( /bin/cat "tmpfile.expected" ) ) ' ' print -- "$( < "tmpfile.expected" )" ' ) integer l1 lr_t -r -a level1=( ( left='print -- "$( ' right=' )"' ) ( left='print -- "$( ulimit -c 0 ; ' right=' )"' ) ( left='print -- "${ ' right=' ; }"' ) ( left='print -- "${ ulimit -c 0 ; ' right=' ; }"' ) ( left='s="${ ' right=' ; }" ; /bin/cat <<<"${s}"' ) ) integer l2 lr_t -r -a level2=( ( left=' ' right=' ' ) ( left='( ' right=' )' ) ( left='print -- "$( ' right=' )"' ) ( left='print -- "$( ulimit -c 0 ; ' right=' )"' ) ( left='print -- "${ ' right=' ; }"' ) ( left='print -- "${ ulimit -c 0 ; ' right=' ; }"' ) ( left='s="${ ' right=' ; }" ; /bin/cat <<<"${s}"' ) ) integer l3 lr_t -r -a level3=( ( left=' ' right=' ' ) ( left='( ' right=' )' ) ( left='print -- "$( ' right=' )"' ) ( left='print -- "$( ulimit -c 0 ; ' right=' )"' ) ( left='print -- "${ ' right=' ; }"' ) ( left='print -- "${ ulimit -c 0 ; ' right=' ; }"' ) ( left='s="${ ' right=' ; }" ; /bin/cat <<<"${s}"' ) ) myseq 30000 >'tmpfile.expected' # build a list of tests... for (( l1=0 ; l1 < ${#level1[@]} ; l1++ )) ; do for (( l2=0 ; l2 < ${#level2[@]} ; l2++ )) ; do for (( l3=0 ; l3 < ${#level3[@]} ; l3++ )) ; do for (( cl=0 ; cl < ${#cmdlist[@]} ; cl++ )) ; do cmd='' cmd+="${level1[l1].left}" cmd+="${level2[l2].left}" cmd+="${level3[l3].left}" cmd+="${cmdlist[cl]}" cmd+="${level3[l3].right}" cmd+="${level2[l2].right}" cmd+="${level1[l1].right}" # make sure "cat" is a builtin not bound to a PATH element if we use it [[ "${cmd}" == ~(E)[^/]cat ]] && cmd="builtin cat ; ${cmd}" # append test to list test_list+=( "${cmd}" ) done done done done # print -u2 -f "# got %d tests\n" "${#test_list[@]}" # run tests... for (( tli=0 ; tli < ${#test_list[@]} ; tli++ )) ; do # (( tli % 100 == 0 )) && print -u2 -f "# mark %d, %f\n" tli SECONDS cmd="${test_list[tli]}" testname="${0}/'${cmd}'" #print -u2 -f "## Running %T: %s\n" "now" "${testname}" # debug child_killed='false' ${SHELL} -o errexit -c "${cmd}" >'tmpfile.out' 2>'tmpfile.err' & (( childpid=$! )) # wait 20 seconds for (( i=0 ; i < 80 ; i++ )) ; do isvalidpid ${childpid} || break sleep 0.25 done # kill child if neccesary... if isvalidpid ${childpid} ; then err_exit "${testname}: child (pid=${childpid}) still busy, sending SIGTERM" kill -TERM ${childpid} 2>/dev/null child_killed='true' sleep 2 if isvalidpid ${childpid} ; then err_exit "${testname}: child (pid=${childpid}) still busy, sending SIGKILL" kill -KILL ${childpid} 2>/dev/null fi fi wait || err_exit "${testname}: Child returned non-zero exit code $?." # wait for child (and/or avoid zombies/slime) # process output... [[ "$( < 'tmpfile.err' )" == '' ]] || err_exit "${testname}: Expected empty stderr, got $(printf "%q\n" "$( < "tmpfile.err" )")" if ! ${child_killed} ; then cmp -s 'tmpfile.expected' 'tmpfile.out' || err_exit "${testname}: tmpfile.expected and tmpfile.out differ." fi rm 'tmpfile.out' 'tmpfile.err' done rm 'tmpfile.expected' return 0 } # test7: Test if command substitution of external and internal # utilities results in the same data function test7 { compound out=( typeset stdout stderr ; integer res ) integer i compound -r -a tests=( # original testcase ( cmd='builtin cat ; myseq 11 >"tmp11" ; LC_ALL="C" diff -u <(print -- "$(/bin/cat <( /bin/cat "tmp11" ) )") <(print -- "$(cat <( cat "tmp11" ) )")' expected_output='No differences encountered' ) # same testcase as above but uses "head" instead of "cat" ( cmd='builtin head ; myseq 11 >"tmp11" ; LC_ALL="C" diff -u <(print -- "$(/usr/bin/head <( /usr/bin/head "tmp11" ) )") <(print -- "$(head <( cat "tmp11" ) )")' expected_output='No differences encountered' ) # Glenn Fowlers testcase ( cmd='print 1 | : "$(/bin/cat <(/bin/cat))"' expected_output='' ) ) for (( i=0 ; i < ${#tests[@]} ; i++ )) ; do nameref ts=tests[i] out.stderr="${ { out.stdout="${ ${SHELL} -o nounset -c "${ts.cmd}" ; (( out.res=$? )) ; }" ; } 2>&1 ; }" [[ "${out.stdout}" == "${ts.expected_output}" ]] || err_exit "${0}/${i}: Expected stdout to match $(printf '%q\n' "${ts.expected_output}"), got ${ printf '%q\n' "${out.stdout}" ; }" [[ "${out.stderr}" == '' ]] || err_exit "${0}/${i}: Expected empty stderr, got ${ printf '%q\n' "${out.stderr}" ; }" done rm 'tmp11' return 0 } # test8: One-time issue which came up during development for # CR #6946957 ("Nested sub shell in command substitution with lots of data hangs") function test8 { typeset testname="${0}/hang" integer childpid # original test, do _NOT_ edit!! typeset -r cmd=$' for (( i=0 ; i < 10 ; i++ )) ; do set -x ( typeset base=`line` print "generating ${base}.c" ( true ) >/dev/null ) <<<"a" done ' ${SHELL} -o errexit -c "${cmd}" >'tmpfile.out' 2>'tmpfile.err' & (( childpid=$! )) # wait 10 seconds for (( i=0 ; i < 40 ; i++ )) ; do isvalidpid ${childpid} || break sleep 0.25 done # kill child if neccesary... if isvalidpid ${childpid} ; then err_exit "${testname}: child (pid=${childpid}) still busy, sending SIGTERM" kill -TERM ${childpid} 2>/dev/null sleep 2 if isvalidpid ${childpid} ; then err_exit "${testname}: child (pid=${childpid}) still busy, sending SIGKILL" kill -KILL ${childpid} 2>/dev/null fi fi wait || err_exit "${testname}: Child returned non-zero exit code $?." # wait for child (and/or avoid zombies/slime) rm 'tmpfile.out' 'tmpfile.err' return 0 } # prepare for tests export PATH="${PWD}:${PATH}" builtin cmp builtin rm unset VMALLOC_OPTIONS VMDEBUG # no debug for helper scripts pretests # run tests test1 test2 test3 test4 test5 test6 test7 test8 # cleanup posttests cd "${ocwd}" rmdir "${tmpdir}" || err_exit "Cannot remove temporary directory ${tmpdir}." # tests done exit $((Errors))