v5.0.0
=====================================================================
Excited to bring V5 to life. This includes some BREAKING CHANGES to
several aspects of ZSH-type scwrypts. Please refer to the readme
for upgrade details (specifically docs/upgrade/v4-to-v5.md)
--- New Features -------------------------
- ZSH testing library with basic mock capabilities
- new scwrypts environment file format includes metadata and more
advanced features like optional parent env overrides, selection
inheritence, and improved structurual flexibility
- speedup cache for non-CI runs of ZSH-type scwrypts
- ${scwryptsmodule} syntax now allows a consistent unique-naming
scheme for functions in ZSH-type scwrypts while providing better
insight into origin of API calls in other modules
- reusable, case-statement-driven argument parsers in ZSH-type scwrypts
--- Changes ------------------------------
- several utility function renames in ZSH-type scwrypts to improve
consistency
- documentation comments included in ZSH libraries
- ZSH-type scwrypts now allow library modules to live alongside
executables
(zsh/lib still supported; autodetection determines default)
--- Bug Fixes ----------------------------
- hardened environment checking for REQUIRED_ENV variables; this removes
the ability to overwrite variables in local function contexts
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
#####################################################################
|
||||
|
||||
use unittest/operations
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}() {
|
||||
local ERRORS=0 TEST_ERRORS=0
|
||||
[ "$SCWRYPTS_LOG_LEVEL" ] || SCWRYPTS_LOG_LEVEL=4
|
||||
|
||||
local UNITTESTS=($(echo "${(k)functions}" | sed 's/ /\n/g' | grep '^test\.' | sort))
|
||||
[[ ${#UNITTESTS[@]} -gt 0 ]] \
|
||||
|| echo.error "must define at least one unittest" \
|
||||
|| return 1
|
||||
|
||||
echo.status "${SCWRYPTS_TEST_MODULE_STRING}starting test suite"
|
||||
|
||||
local UNITTEST_RESULTS_DIR="${SCWRYPTS_TEMP_PATH}/test"
|
||||
mkdir -p "${UNITTEST_RESULTS_DIR}"
|
||||
|
||||
local TEST_COUNT=0
|
||||
|
||||
command -v beforeall &>/dev/null && beforeall
|
||||
|
||||
local UNITTEST
|
||||
for UNITTEST in ${UNITTESTS[@]}
|
||||
do
|
||||
((TEST_COUNT+=1))
|
||||
ERRORS=0
|
||||
command -v beforeeach &>/dev/null && beforeeach
|
||||
|
||||
${UNITTEST} &> "${UNITTEST_RESULTS_DIR}/${UNITTEST}.txt" \
|
||||
&& echo.success "${SCWRYPTS_TEST_MODULE_STRING}${UNITTEST}" \
|
||||
|| { echo.error "${SCWRYPTS_TEST_MODULE_STRING}${UNITTEST}"; ((TEST_ERRORS+=1)); echo "--- begin test output ---">&2; cat "${UNITTEST_RESULTS_DIR}/${UNITTEST}.txt"; echo "--- end test output ---\n">&2; }
|
||||
|
||||
command -v aftereach &>/dev/null && aftereach
|
||||
|
||||
unittest.operations restore
|
||||
unittest.mock.env.restore
|
||||
done
|
||||
|
||||
command -v afterall &>/dev/null && afterall
|
||||
|
||||
local EXIT_CODE=$TEST_ERRORS
|
||||
[[ $TEST_ERRORS -eq 0 ]] \
|
||||
&& echo.success "${SCWRYPTS_TEST_MODULE_STRING}passed ${TEST_COUNT} / ${TEST_COUNT} test(s)" \
|
||||
|| echo.error "${SCWRYPTS_TEST_MODULE_STRING}failed ${EXIT_CODE} / ${TEST_COUNT} test(s)" \
|
||||
;
|
||||
|
||||
return $EXIT_CODE
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
#####################################################################
|
||||
|
||||
DEPENDENCIES+=(sed)
|
||||
|
||||
#####################################################################
|
||||
|
||||
#
|
||||
# "mocking" is critical to unittesting workflows, and although this
|
||||
# provides a preliminary implementation idea for "mocking" in ZSH,
|
||||
# this is very new and subject to change
|
||||
#
|
||||
# for critical workflows, though, some limited tests are way better
|
||||
# than no tests at all!
|
||||
#
|
||||
# known issues:
|
||||
# - callstack assertions are never set when the function call is
|
||||
# a non-terminal member of a pipestream; for example:
|
||||
#
|
||||
# unittest.mock func-a --stdout asdf
|
||||
#
|
||||
# func-a | grep -q "asdf"
|
||||
# ^ grep will succeed, indicating the mock worked
|
||||
#
|
||||
# func-a.assert.called
|
||||
# ^ will fail; I guess pipe streams are executed in a subshell
|
||||
# where export doesn't work as expected?
|
||||
#
|
||||
|
||||
MOCKS=()
|
||||
${scwryptsmodule}() {
|
||||
local DESCRIPTION="
|
||||
(beta) generates a function mock for basic ZSH unit testing
|
||||
"
|
||||
eval "$(utils.parse.autosetup)"
|
||||
|
||||
##########################################
|
||||
|
||||
export MOCK__ORIGINAL_IMPLEMENTATION__${FUNCTION_VARIABLE}="$(which ${FUNCTION})"
|
||||
|
||||
[ "${STDOUT}" ] && export MOCK__STDOUT__${FUNCTION_VARIABLE}="${STDOUT}"
|
||||
[ "${STDERR}" ] && export MOCK__STDERR__${FUNCTION_VARIABLE}="${STDERR}"
|
||||
|
||||
[ ${EXIT_CODE} ] || EXIT_CODE=0
|
||||
export MOCK__EXIT_CODE__${FUNCTION_VARIABLE}=${EXIT_CODE}
|
||||
|
||||
##########################################
|
||||
|
||||
# tricky! in order to set the ${FUNCTION} as literal within zsh functions, we need
|
||||
# to run all the test definitions as an eval line :S
|
||||
|
||||
eval "
|
||||
|
||||
export MOCK__CALLSTACK__${FUNCTION_VARIABLE}=()
|
||||
export MOCK__CALLCOUNT__${FUNCTION_VARIABLE}=0
|
||||
|
||||
${FUNCTION}() {
|
||||
MOCK__CALLSTACK__${FUNCTION_VARIABLE}+=(\"\$@\")
|
||||
((MOCK__CALLCOUNT__${FUNCTION_VARIABLE}+=1))
|
||||
|
||||
printf \"\$MOCK__STDOUT__${FUNCTION_VARIABLE}\"
|
||||
printf \"\$MOCK__STDERR__${FUNCTION_VARIABLE}\" >&2
|
||||
|
||||
return \$(eval echo '\$MOCK__EXIT_CODE__'${FUNCTION_VARIABLE})
|
||||
}
|
||||
|
||||
${FUNCTION}.assert.called() {
|
||||
local ERRORS=0
|
||||
[[ MOCK__CALLCOUNT__${FUNCTION_VARIABLE} -gt 0 ]] \
|
||||
|| echo.error \"${FUNCTION} was not called\"
|
||||
}
|
||||
|
||||
${FUNCTION}.assert.not.called() {
|
||||
local ERRORS=0
|
||||
${FUNCTION}.assert.called &>/dev/null
|
||||
[[ \$? -ne 0 ]] \
|
||||
|| echo.error \"${FUNCTION} was called\"
|
||||
}
|
||||
|
||||
${FUNCTION}.assert.callstack() {
|
||||
local ERRORS=0
|
||||
[[ \"\$@\" =~ ^\${MOCK__CALLSTACK__${FUNCTION_VARIABLE}}$ ]] \
|
||||
|| echo.error \"${FUNCTION} callstack does not match\nexpected : \$@\nreceived : \${MOCK__CALLSTACK__${FUNCTION_VARIABLE}}\"
|
||||
}
|
||||
|
||||
${FUNCTION}.assert.callstackincludes() {
|
||||
local ERRORS=0
|
||||
[[ \${MOCK__CALLSTACK__${FUNCTION_VARIABLE}} =~ \$@ ]] \
|
||||
|| echo.error \"${FUNCTION} callstack does not include\nexpected : \$@\ncallstack : \${MOCK__CALLSTACK__${FUNCTION_VARIABLE}}\"
|
||||
}
|
||||
|
||||
${FUNCTION}.reset() {
|
||||
unset \
|
||||
MOCK__CALLSTACK__${FUNCTION_VARIABLE} \
|
||||
MOCK__CALLCOUNT__${FUNCTION_VARIABLE} \
|
||||
;
|
||||
}
|
||||
|
||||
${FUNCTION}.restore() {
|
||||
MOCKS=(\$(echo \"\$MOCKS\" | sed 's/\s\+/\n/g' | grep -v \"^${FUNCTION}$\"))
|
||||
|
||||
unset \
|
||||
MOCK__CALLSTACK__${FUNCTION_VARIABLE} \
|
||||
MOCK__CALLCOUNT__${FUNCTION_VARIABLE} \
|
||||
;
|
||||
|
||||
unset -f \
|
||||
${FUNCTION} \
|
||||
${FUNCTION}.assert.called \
|
||||
${FUNCTION}.assert.not.called \
|
||||
${FUNCTION}.assert.callstack \
|
||||
${FUNCTION}.assert.callstackincludes \
|
||||
${FUNCTION}.reset \
|
||||
${FUNCTION}.restore \
|
||||
;
|
||||
|
||||
local ORIGINAL_IMPLEMENTATION=\"\$(eval echo '\$MOCK__ORIGINAL_IMPLEMENTATION__'${FUNCTION_VARIABLE})\"
|
||||
[[ \$(echo \"\$ORIGINAL_IMPLEMENTATION\" | wc -l) -gt 1 ]] \
|
||||
&& eval \"\$ORIGINAL_IMPLEMENTATION\"
|
||||
|
||||
unset ORIGINAL__IMPLEMENTATION__${FUNCTION_VARIABLE}
|
||||
}
|
||||
"
|
||||
|
||||
MOCKS+=(${FUNCTION})
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}.parse.locals() {
|
||||
local FUNCTION
|
||||
local FUNCTION_VARIABLE
|
||||
local STDOUT
|
||||
local STDERR
|
||||
local EXIT_CODE
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse() {
|
||||
# local FUNCTION STDOUT STDERR EXIT_CODE
|
||||
local PARSED=0
|
||||
|
||||
case $1 in
|
||||
( --stdout )
|
||||
PARSED=2
|
||||
STDOUT="$2"
|
||||
;;
|
||||
|
||||
( --stderr )
|
||||
PARSED=2
|
||||
STDERR="$2"
|
||||
;;
|
||||
|
||||
( --exit-code )
|
||||
PARSED=2
|
||||
EXIT_CODE="$2"
|
||||
;;
|
||||
|
||||
( * ) [[ ${POSITIONAL_ARGS} -gt 0 ]] && return 0
|
||||
((POSITIONAL_ARGS+=1))
|
||||
PARSED=1
|
||||
case ${POSITIONAL_ARGS} in
|
||||
( 1 ) FUNCTION="$1" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
return ${PARSED}
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse.usage() {
|
||||
USAGE__usage+=' function [...options...]'
|
||||
|
||||
USAGE__args+='
|
||||
function the function to be mocked
|
||||
'
|
||||
|
||||
USAGE__options+='
|
||||
--stdout <string> mock the stdout output for the call
|
||||
--stderr <string> mock the stdout output for the call
|
||||
--exit-code <number> mock the exit code for the call
|
||||
'
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse.validate() {
|
||||
FUNCTION_VARIABLE="$(echo "${FUNCTION}" | sed 's/\./___/g; s/-/_____/g')"
|
||||
[ "${FUNCTION_VARIABLE}" ] || echo.error "failed to determine safe variable name for '${FUNCTION}'"
|
||||
|
||||
[ ${FUNCTION} ] && command -v ${FUNCTION} &>/dev/null || {
|
||||
[[ $(eval echo "\$MOCK_UNCALLABLE_WARNING_ISSUED__${FUNCTION_VARIABLE}") =~ true ]] || {
|
||||
echo.warning "mocking uncallable '${FUNCTION}'"
|
||||
export MOCK_UNCALLABLE_WARNING_ISSUED__${FUNCTION_VARIABLE}=true
|
||||
}
|
||||
}
|
||||
|
||||
echo "${MOCKS}" | sed 's/\s\+/\n/g' | grep -q "^${FUNCTION}$" \
|
||||
&& echo.error "cannot mock '${FUNCTION}' (it is already mocked)"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#####################################################################
|
||||
|
||||
DEPENDENCIES+=(sed)
|
||||
|
||||
#####################################################################
|
||||
|
||||
MOCKED_ENV=()
|
||||
${scwryptsmodule}() {
|
||||
local DESCRIPTION="
|
||||
(beta) mocks an environment variable for testing
|
||||
"
|
||||
eval "$(utils.parse.autosetup)"
|
||||
|
||||
##########################################
|
||||
|
||||
MOCKED_ENV+=(${ENVIRONMENT_VARIABLE_NAME})
|
||||
|
||||
export ${ENVIRONMENT_VARIABLE_NAME}__original_value="${(P)ENVIRONMENT_VARIABLE_NAME}"
|
||||
export ${ENVIRONMENT_VARIABLE_NAME}=${ENVIRONMENT_VARIABLE_VALUE}
|
||||
}
|
||||
|
||||
${scwryptsmodule}.restore() {
|
||||
local ENVIRONMENT_VARIABLE_NAME ORIGINAL_VALUE
|
||||
for ENVIRONMENT_VARIABLE_NAME in ${MOCKED_ENV[@]}
|
||||
do
|
||||
ORIGINAL_VALUE="$(eval echo '$'$ENVIRONMENT_VARIABLE_NAME'__original_value')"
|
||||
[ "$ORIGINAL_VALUE" ] \
|
||||
&& export ${ENVIRONMENT_VARIABLE_NAME}="$ORIGINAL_VALUE" \
|
||||
|| unset ${ENVIRONMENT_VARIABLE_NAME} \
|
||||
;
|
||||
|
||||
unset ${ENVIRONMENT_VARIABLE_NAME}__checked 2>/dev/null
|
||||
done
|
||||
MOCKED_ENV=()
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}.parse.locals() {
|
||||
local ENVIRONMENT_VARIABLE_NAME
|
||||
local ENVIRONMENT_VARIABLE_VALUE
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse() {
|
||||
local PARSED=0
|
||||
|
||||
case $1 in
|
||||
( --value )
|
||||
PARSED=2
|
||||
ENVIRONMENT_VARIABLE_VALUE="$2"
|
||||
;;
|
||||
|
||||
( * )
|
||||
[[ $POSITIONAL_ARGS -gt 0 ]] && return 0
|
||||
((POSITIONAL_ARGS+=1))
|
||||
PARSED=1
|
||||
case $POSITIONAL_ARGS in
|
||||
( 1 ) ENVIRONMENT_VARIABLE_NAME="$1"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
return $PARSED
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse.usage() {
|
||||
USAGE__usage+=' function [...options...]'
|
||||
|
||||
USAGE__args+='
|
||||
name the name of the environment variable
|
||||
'
|
||||
|
||||
USAGE__options+='
|
||||
--value the value of the environment variable
|
||||
'
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse.validate() {
|
||||
[ "$ENVIRONMENT_VARIABLE_NAME" ] \
|
||||
|| echo.error "no environment variable specified"
|
||||
|
||||
echo "$MOCKED_ENV" | sed 's/\s\+/\n/g' | grep -q "^$ENVIRONMENT_VARIABLE_NAME$" \
|
||||
&& echo.error "environment variable '$ENVIRONMENT_VARIABLE_NAME' has already been mocked"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#
|
||||
# function mocking utilities for ZSH unit testing
|
||||
#
|
||||
|
||||
# primary mock generator
|
||||
use unittest/mock/create
|
||||
eval "${scwryptsmodule}() { ${scwryptsmodule}.create \$@; }"
|
||||
|
||||
# mock environment variables
|
||||
use unittest/mock/env
|
||||
@@ -0,0 +1,38 @@
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}() {
|
||||
local DESCRIPTION="
|
||||
allows batch operations against existing mocks for lib/test/unittest
|
||||
"
|
||||
eval "$(utils.parse.autosetup)"
|
||||
|
||||
##########################################
|
||||
|
||||
local MOCK
|
||||
for MOCK in ${MOCKS[@]}
|
||||
do
|
||||
${MOCK}.${OPERATION}
|
||||
done
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}.locals() {
|
||||
local OPERATION
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse() {
|
||||
local PARSED=0
|
||||
|
||||
case $1 in
|
||||
( restore | reset ) PARSED=1; OPERATION="$1" ;;
|
||||
esac
|
||||
|
||||
return ${PARSED}
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse.usage() {
|
||||
USAGE__args+='
|
||||
$1 one of (restore reset) to perform on all active mocks
|
||||
'
|
||||
}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env zsh
|
||||
#####################################################################
|
||||
|
||||
use scwrypts/meta
|
||||
|
||||
#####################################################################
|
||||
|
||||
USAGE__description="
|
||||
runs tests across scwrypts zsh modules (beta)
|
||||
"
|
||||
|
||||
USAGE__args='
|
||||
$@ paths or lookup patterns to test
|
||||
'
|
||||
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
local \
|
||||
ARGS=() \
|
||||
PARSERS=()
|
||||
|
||||
eval "$ZSHPARSEARGS"
|
||||
|
||||
##########################################
|
||||
|
||||
local MODE
|
||||
[[ ${#ARGS[@]} -eq 0 ]] \
|
||||
&& MODE=all \
|
||||
|| MODE=filter \
|
||||
;
|
||||
|
||||
local TEST_FILES="" GROUP
|
||||
for GROUP in ${SCWRYPTS_GROUPS[@]}
|
||||
do
|
||||
TEST_FILES+="$(find "$(scwrypts.config.group ${GROUP} root)" -type f -name \*.test.zsh)"
|
||||
done
|
||||
|
||||
for FILTER in ${ARGS[@]}
|
||||
do
|
||||
TEST_FILES="$(echo "${TEST_FILES}" | grep "${FILTER}")"
|
||||
done
|
||||
|
||||
local SCWRYPTS_CONFIG="$(scwrypts.meta.run --config)"
|
||||
local TEST_FILE
|
||||
local TEST_SUITE_COUNT=0 TEST_SUITE_FAILED_COUNT=0
|
||||
for TEST_FILE in $(echo "${TEST_FILES}")
|
||||
do
|
||||
((TEST_SUITE_COUNT+=1))
|
||||
local TEST_SUITE_NAME="$(basename -- "$(dirname -- ${TEST_FILE})")/$(basename -- "${TEST_FILE}")"
|
||||
zsh <<< "
|
||||
() {
|
||||
local ERRORS=0
|
||||
local CI=true
|
||||
$SCWRYPTS_CONFIG
|
||||
source '$TEST_FILE'
|
||||
use unittest/execute-test-file
|
||||
utils.check-environment
|
||||
SUPPRESS_USAGE_OUTPUT=true SCWRYPTS_TEST_MODULE_STRING=\"${TEST_SUITE_NAME} : \" unittest.execute-test-file
|
||||
}
|
||||
" || ((TEST_SUITE_FAILED_COUNT+=1))
|
||||
done
|
||||
|
||||
[[ ${TEST_SUITE_FAILED_COUNT} -eq 0 ]] \
|
||||
&& echo.success "\nsuccessfully passed ${TEST_SUITE_COUNT} / ${TEST_SUITE_COUNT} test suite(s)\n " \
|
||||
|| echo.error "\nfailed ${TEST_SUITE_FAILED_COUNT} / ${TEST_SUITE_COUNT} test suite(s)\n " \
|
||||
;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
${scwryptsmodule}() {
|
||||
local ERRORS=0
|
||||
[ $1 ] \
|
||||
|| echo.error "must specify a function name to provide" \
|
||||
|| return 2
|
||||
|
||||
command -v $1 || echo.error "missing '$1'"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#
|
||||
# common logic used in testing
|
||||
#
|
||||
|
||||
# use uuidgen for random string generation
|
||||
DEPENDENCIES+=(uuidgen)
|
||||
|
||||
# ensure a module provides a function by name
|
||||
use unittest/test/provides
|
||||
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# (beta) module for performing unit tests on scwrypts zsh modules
|
||||
#
|
||||
|
||||
# provides function and environment variable mocking (+ some assertions)
|
||||
use unittest/mock
|
||||
|
||||
# provides common logic used in testing
|
||||
use unittest/test
|
||||
|
||||
#
|
||||
# create a test file to match your module name
|
||||
# ├── my-thing.module.zsh
|
||||
# └── my-thing.test.zsh
|
||||
#
|
||||
# add the import line 'use unittest'
|
||||
#
|
||||
# define tests by creating functions called 'test.your-test-name()'
|
||||
# - the test "passes" on successful return code (e.g. 'return 0')
|
||||
# - the test "fails" on any other return code
|
||||
#
|
||||
# some other testing features implemented:
|
||||
# - defining 'beforeall()' < executed before the test suite runs
|
||||
# - defining 'beforeeach()' < executed before each test function
|
||||
# - defining 'aftereach()' < executed after each test function
|
||||
# - defining 'afterall()' < executed after the test suite completes
|
||||
#
|
||||
# using 'scwrypts unittest run' will run each test suite in an isolated
|
||||
# subshell, so configurations are not persisted between test files
|
||||
#
|
||||
Reference in New Issue
Block a user