1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
|
# mockup: a function to mockup other commands/functions
#
# this can be used to simulate a program that records calling arguments,
# provided input, produces specified output/errors, and exits with a specified
# return code. very useful for unit testing!
#
# simple usage:
# $ echo file1 file2 file3 > ls.output.txt
# $ mockup -o ls.output.txt ls
# $ ls
# file1 file2 file3
#
# more advanced features are possible, such as multiple different invocations
# and support for running in parallel under pipes. see the nonexistant
# documentation for examples
set -u
MOCKUP_WORKDIR=${MOCKUP_WORKDIR:-${TMPDIR}/mockup-$$}
# record internal mockup program failure
mockup_fail(){
echo "MOCKUP ERROR: $@" >> "${errorfile:-/dev/stderr}"
return 127
}
# helpful usage stmts
mockup_help(){
cat << EOF
mockup: create "mock" programs/functions for unit testing shell scripts.
usage:
mockup -h
mockup [-i] [-o file] [-e file] [-r code] command
command: command-line program or shell function to override.
options:
-h: print this useful help message.
-i: record the input provided on stdin (to mockup.<n>.input.<cmd>)
-o FILE: print the contents of FILE on standard output.
-e FILE: print the contents of FILE on standard error.
-r CODE: return with status CODE.
EOF
}
# main function, called from test suite
mockup(){
local args efile input ofile rcode
local n cmd envfile
args=`getopt -o e:hio:r: -n mockup -- "$@"`
test $? = 0 || mockup_fail invalid options passed to mockup
eval set -- "$args"
# do something that will always be true without using true
# thus allowing even the "true" command to be mocked up :)
while [ "true" ]; do
case "$1" in
-e)
efile="$2"
shift
;;
-h)
mockup_help
return 0
;;
-i)
input=yes
;;
-o)
ofile="$2"
shift
;;
-r)
rcode="$2"
shift
;;
--)
shift
break
;;
*)
mockup_help >&2;
mockup_fail invalid arguments passed to mockup
return 127
;;
esac
shift
done
if [ ! -e "$MOCKUP_WORKDIR" ]; then
mockup_fail "${MOCKUP_WORKDIR} not found, create or set MOCKUP_WORKDIR"
return 127
elif [ $# -ne 1 ]; then
mockup_help 2>&1
mockup_fail "$# $* need command name to mockup"
return 127
fi
cmd=$1
# to support multiple invokations, organize filenames in a queue
n=`mockup_next_available "$MOCKUP_WORKDIR" "$cmd"`
mockup_print "$cmd" "$n"
envfile=$MOCKUP_WORKDIR/$n.env.$cmd
if [ -f "$envfile" ]; then
. "$envfile"
else
mockup_fail "env file for $cmd (iteration $n) not found"
return 127
fi
return 0
}
# utility function for the case where a program might be called
# multiple times in the same test suite but it's desired to
# have different behaviours on each invokation
mockup_next_available(){
local wd="$1"
local cmd="$2"
local n=0;
while [ -f "$wd/$n.script.$cmd" ] || [ -f "$wd/done.$n.script.$cmd" ]; do
n=`expr $n + 1`
done
echo $n
}
# likewise but opposite: find the next to be evaluated
mockup_next_evaluated(){
local wd="$1"
local cmd="$2"
local n=0;
local next oldnext
n=`ls "$wd" | grep -E "^[0-9]*.script.$cmd" 2>/dev/null | head -1`
n=`basename "$n" ".script.$cmd"`
echo ${n:-0}
}
# utility function to quote cmdline arguments distinctly wrt whitespace
# it can also replace certain environment variables with a template value,
# i.e. s/$TMPDIR/<TMPDIR>/ for easy comparison
mockup_escape(){
local esc_tmpdir
esc_tmpdir=`echo $TMPDIR | sed -e 's,\\\\,\\\\\\\\,g'`
sed -e "s,$esc_tmpdir,<TMPDIR>,g" 2>"$errorfile" -e "s/'/'\\\\''/g" << EOF
$1
EOF
}
# create the mock wrappers etc for the selected program/function
mockup_print(){
local n scriptfile cmdlinefile errorfile mockup_control envfile
local cmd="$1"
local next oldnext
n="$2"
# the actual hook
scriptfile="$MOCKUP_WORKDIR/$n.script.$cmd"
donescriptfile="$MOCKUP_WORKDIR/done.$n.script.$cmd"
# where any received input is put
inputfile="$MOCKUP_WORKDIR/$n.input.$cmd"
doneinputfile="$MOCKUP_WORKDIR/done.$n.input.$cmd"
# where the cmdline with which the hook was called is stored
cmdlinefile="$MOCKUP_WORKDIR/$n.cmdline.$cmd"
donecmdlinefile="$MOCKUP_WORKDIR/done.$n.cmdline.$cmd"
# where the mockup-internal errors are stored
errorfile="$MOCKUP_WORKDIR/$n.errors.$cmd"
doneerrorfile="$MOCKUP_WORKDIR/done.$n.errors.$cmd"
# env file (see below)
envfile="$MOCKUP_WORKDIR/$n.env.$cmd"
doneenvfile="$MOCKUP_WORKDIR/done.$n.env.$cmd"
# master hook function control file for mockup scripts
mockup_control="$MOCKUP_WORKDIR/mockup.control"
# create an environment file to store all of this, but in the
# form of after it's run (so it can be sourced for comparisons later)
cat << EOF >"$envfile"
mockup_cmdline="$donecmdlinefile"
mockup_scriptfile="$donescriptfile"
mockup_errorfile="$doneerrorfile"
mockup_inputfile="$doneinputfile"
EOF
# see if the mockup script already has a mockup hook registered
# and if not, add it, and re-source it. this hook doesn't actually
# do the real work, as there's an extra level of indirection (argh,
# meta-meta-shell programming makes teh hedz hurt) to support
# multiple sequential invokations
if ! grep -q "^$cmd(){" "$mockup_control" 2>/dev/null; then
cat << EOF >> "${mockup_control}"
$cmd(){
local next n oldnext ret
local errorfile
errorfile="$errorfile"
unset -f mock_$cmd >/dev/null 2>&1
n="\`mockup_next_evaluated \"\$MOCKUP_WORKDIR\" \"$cmd\"\`"
if [ \$? -ne 0 ] || [ -z "\$n" ]; then
mockup_fail "no mockup script to execute for $cmd"
return 127
fi
next="\$MOCKUP_WORKDIR/\$n.script.$cmd"
if [ -f "\$next" ]; then
. "\${next}"
else
mockup_fail "sourcing \${next}"
return 127
fi
mock_$cmd "\$@"
ret=\$?
(
cd \$MOCKUP_WORKDIR
for f in \$n.*.$cmd; do
mv "\$f" "done.\$f"
if [ \$? -ne 0 ]; then
mockup_fail "rotating out \${next}"
return 127
fi
done
)
return \$ret
}
EOF
fi
. "$mockup_control"
# okay, now this is the real meat and potatoes of the mockup work
# this gets put into the sequential script file, and is called by
# the main hook above.
cat << EOF > "$scriptfile"
mock_$cmd(){
local escaped cmdline
local errorfile="$errorfile"
# record the cmdline
set $cmd "\$@"
while [ \$# -gt 0 ]; do
escaped="\`mockup_escape \"\$1\"\`"
if [ -n "\${cmdline:-}" ]; then
cmdline="\$cmdline '\$escaped'"
else
cmdline="'\$escaped'"
fi
shift
done
set "\$cmdline"
echo "\$@" > "$cmdlinefile"
if [ \$? -ne 0 ]; then
mockup_fail failed recording cmdlinefile
return 127
fi
# record input
if [ -n "${input:-}" ]; then
cat > "${inputfile:-}" 2>>"$errorfile"
if [ \$? -ne 0 ]; then
mockup_fail failed recording input
return 127
fi
fi
# produce output
if [ -n "${ofile:-}" ]; then
cat < "${ofile:-}" 2>>"$errorfile"
if [ \$? -ne 0 ]; then
mockup_fail failed producing output
return 127
fi
fi
# produce errors
if [ -n "${efile:-}" ]; then
if [ ! -f "${efile:-}" ]; then
mockup_fail failed to find error output file
return 127
else
cat < "${efile:-}" >&2
fi
if [ \$? -ne 0 ]; then
mockup_fail failed producing error output
return 127
fi
fi
# and return requested value
return ${rcode:-0}
}
EOF
}
|