Skip to content

Commit

Permalink
♻️ refactoring test lib and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jcaillon committed Jan 16, 2025
1 parent 4366e25 commit d29fc90
Show file tree
Hide file tree
Showing 107 changed files with 1,004 additions and 369 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Latest Release](https://img.shields.io/github/v/release/jcaillon/valet?sort=date&style=flat&logo=github&logoColor=white&label=Latest%20release&color=%2350C878)][latest-release]
[![Total downloads](https://img.shields.io/github/downloads/jcaillon/valet/total.svg?style=flat)][releases]
[![MIT license](https://img.shields.io/badge/License-MIT-74A5C2.svg?style=flat)][license]
[![bash 5+ required](https://img.shields.io/badge/Requires-bash%20v5+-865FC5.svg?logo=gnubash&logoColor=white)][bash]
[![bash 5.1+ required](https://img.shields.io/badge/Requires-bash%20v5.1+-865FC5.svg?logo=gnubash&logoColor=white)][bash]

[![icon](docs/static/logo.png)][valet-site]

Expand Down
2 changes: 1 addition & 1 deletion commands.d/self-build-utils
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ _CMD_INCLUDED=1
"
# shellcheck disable=SC2086
io::invoke declare -p ${!CMD_*}
io::captureOutput declare -p ${!CMD_*}
local line
local IFS
while IFS=$'\n' read -r -d $'\n' line; do
Expand Down
14 changes: 6 additions & 8 deletions commands.d/self-install.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
#!/usr/bin/env bash
set -Eeu -o pipefail
# Title: commands.d/*
# Author: github.com/jcaillon

# if not executing in bash, we can stop here
if [[ -z "${BASH_VERSION:-}" ]]; then
printf '%s\n' "❌ This script must be run with bash." 1>&2
exit 1
fi
if [[ ${BASH_VERSINFO[0]:-0} -lt 5 ]]; then
printf '%s\n' "❌ Bash 5 or higher is required to run valet." 1>&2
# check the bash version (and that we are running in bash), make it POSIX compliant
# shellcheck disable=SC2292
# shellcheck disable=SC2086
# shellcheck disable=SC2128
if [ ${BASH_VERSINFO:-0} -lt 5 ] || { [ ${BASH_VERSINFO} -eq 5 ] && [ ${BASH_VERSINFO[1]:-0} -lt 1 ]; }; then
printf '%s\n' "❌ Bash 5.1 or higher is required to run valet."
exit 1
fi

Expand Down
85 changes: 70 additions & 15 deletions commands.d/self-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ function selfTest_runSingleTestSuite() {
# the global execution of the tests.
if ! (
core::setShellOptions
trap 'selfTest_onExitTestInternal $? ' EXIT;
trap 'selfTest_onExitTestInternal $?' EXIT; trap 'selfTest_onErrTestInternal' ERR;
selfTest_runSingleTest "${testSuiteDirectory}" "${testScript}" || exit $?
) >"${GLOBAL_TEST_STACK_FILE}" 2>"${GLOBAL_TEST_LOG_FILE}"; then

Expand All @@ -390,26 +390,57 @@ function selfTest_runSingleTestSuite() {
eval "${RETURNED_VALUE//declare -a/}"

selfTestUtils_displayTestLogs
if log::isDebugEnabled; then
selfTestUtils_displayTestLogs
fi
selfTestUtils_displayTestSuiteOutputs

log::error "The program exited with code ⌜${exitCode}⌝."$'\n'"Test scripts must not exit or return an error (do not forget that shell options are set to exit on error)."$'\n'$'\n'"If you expect a tested function or command to exit or fail, then run it in a subshell and capture the exit code like that:"$'\n'"⌜(myFunctionThatFails) || echo 'failed as expected with code \${PIPESTATUS[0]:-}'⌝"$'\n'$'\n'"Error in ⌜${testScript/#"${GLOBAL_PROGRAM_STARTED_AT_DIRECTORY}/"/}⌝."
log::printCallStack 1 7
if (( exitCode == 142 )); then
# the function test::fail was called, there was a coding mistake in the test script that we caught
io::readFile "${GLOBAL_TEST_LOG_FILE}"
log::error "The test script ⌜${testScript##*/}⌝ failed because of a bad test implementation." \
"" \
"${RETURNED_VALUE}" \
"" \
"Error in ⌜${testScript/#"${GLOBAL_PROGRAM_STARTED_AT_DIRECTORY}/"/}⌝."

elif [[ -n ${_STACK_FUNCTION_NAMES_ERR:-} ]]; then
log::error "The test script ⌜${testScript##*/}⌝ had an error and exited with code ⌜${exitCode}⌝." \
"Test scripts will exit if a command ends with an error (shell option to exit on error)." \
""\
"If you expect a tested function or command to fail, use one of these two methods to display the failure without exiting the script:" \
""\
"- ⌜myCommandThatFails || echo 'failed as expected with code \$?'⌝" \
"- ⌜test::exec myCommandThatFails⌝" \
""\
"Error in ⌜${testScript/#"${GLOBAL_PROGRAM_STARTED_AT_DIRECTORY}/"/}⌝:"
_STACK_FUNCTION_NAMES=("${_STACK_FUNCTION_NAMES_ERR[@]}")
_STACK_SOURCE_FILES=("${_STACK_SOURCE_FILES_ERR[@]}")
_STACK_LINE_NUMBERS=("${_STACK_LINE_NUMBERS_ERR[@]}")
# print the last line of the err output, which is supposed to be the bash error message
io::tail "${GLOBAL_TEST_STANDARD_ERROR_FILE}" 1 true
if [[ -n ${RETURNED_ARRAY[0]:-} ]]; then
log::printString "${RETURNED_ARRAY[0]:-}" ""
fi
log::printCallStack 2 7
else
log::error "The test script ⌜${testScript##*/}⌝ exited with code ⌜${exitCode}⌝." \
"Test scripts must not exit or return an error (shell options are set to exit on error)." \
"" \
"If you expect a tested function or command to exit, run it in a subshell using one of these two methods:" \
"" \
"- ⌜(myCommandThatFails) || echo 'failed as expected with code \${PIPESTATUS[0]:-}'⌝" \
"- ⌜test::exit myCommandThatFails⌝" \
"" \
"Error in ⌜${testScript/#"${GLOBAL_PROGRAM_STARTED_AT_DIRECTORY}/"/}⌝:"
log::printCallStack 1 7
fi

nbOfScriptsWithErrors+=1

else
# end of test
if log::isDebugEnabled; then
selfTestUtils_displayTestLogs
fi

# check if the user forgot to call test::endTest
if [[ -s "${GLOBAL_TEST_STANDARD_OUTPUT_FILE}" || -s "${GLOBAL_TEST_STANDARD_ERROR_FILE}" ]]; then
selfTestUtils_displayTestSuiteOutputs
log::error "Test script did not call ⌜test::endTest⌝ or it had outputs after the last call or ⌜test::endTest⌝."$'\n'$'\n'"Make sure to conclude all tests by calling the ⌜test::endTest⌝ function (nothing should be printed to the standard or error output after that)."$'\n'$'\n'"Error in ⌜${testScript/#"${GLOBAL_PROGRAM_STARTED_AT_DIRECTORY}/"/}⌝."
selfTestUtils_displayTestLogs
log::error "The test script had un-flushed when it ended."$'\n'$'\n'"Make sure to conclude all test scripts by calling the ⌜test::flush⌝ or ⌜test::endTest⌝ functions (nothing should be printed to the standard or error output after that)."$'\n'$'\n'"Error in ⌜${testScript/#"${GLOBAL_PROGRAM_STARTED_AT_DIRECTORY}/"/}⌝."
nbOfScriptsWithErrors+=1
fi

Expand All @@ -423,8 +454,20 @@ function selfTest_runSingleTestSuite() {
# run a custom user script after the test suite if it exists
selfTestUtils_runHookScript "${testsDotDirectory}/after-each-test-suite"

((nbOfScriptsWithErrors > 0)) && return 1
selfTestUtils_compareWithApproved "${testSuiteDirectory}" "${GLOBAL_TEST_REPORT_FILE}" || return 1
# exit with an error if there were errors in the test scripts
if ((nbOfScriptsWithErrors > 0)); then
return 1
fi

# compare the test suite outputs with the approved files
if selfTestUtils_compareWithApproved "${testSuiteDirectory}" "${GLOBAL_TEST_REPORT_FILE}"; then
selfTestUtils_displayTestLogs
return 1
fi

if log::isDebugEnabled; then
selfTestUtils_displayTestLogs
fi
return 0
}

Expand All @@ -434,7 +477,7 @@ function selfTest_runSingleTestSuite() {
# It will output the stack trace in a file.
# We need to capture the stack trace within the subshell that runs the test script.
function selfTest_onExitTestInternal() {
# handle an error in the test script
# handle an exit in the test script
local exitCode="${1}"

if ((exitCode == 0)); then
Expand All @@ -447,6 +490,18 @@ function selfTest_onExitTestInternal() {
declare -p _STACK_FUNCTION_NAMES _STACK_SOURCE_FILES _STACK_LINE_NUMBERS 1>&3
}

# ## selfTest_onErrTestInternal
#
# Will be called if a test script has an error.
# We need to capture the stack trace within the subshell that runs the test script.
function selfTest_onErrTestInternal() {
# handle an error in the test script
_STACK_FUNCTION_NAMES_ERR=("${FUNCNAME[@]}")
_STACK_SOURCE_FILES_ERR=("${BASH_SOURCE[@]}")
_STACK_LINE_NUMBERS_ERR=("${BASH_LINENO[@]}")
declare -p _STACK_FUNCTION_NAMES_ERR _STACK_SOURCE_FILES_ERR _STACK_LINE_NUMBERS_ERR 1>&3
}

# ## selfTest_runSingleTest
#
# Run a single test script.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/002.installation/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: Learn how to install Valet on your system.

## 📦 Dependencies

- Bash version 5 or superior is required (it uses bash `EPOCHREALTIME`).
- Bash version 5.1 or superior is required (it uses bash `EPOCHREALTIME`).
- From [GNU coreutils][gnu-core-utils]: it uses `rm`, `mv`, `mkdir` for all commands. It uses `cp`, `chmod`, `touch` for the installation/updates. *You most likely already have all of these!*
- [curl][curl] and [tar][tar] are needed only if you want to use the `self update` or `self extend` command.

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/030.test-commands/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Tests are implemented in `.sh` scripts directly under a test suite directory. Th

You have 2 extra functions at your disposal in test scripts (see [libraries/test][libraries-tests] for more details):

- `test::commentTest "comment"`: Add a text paragraph in the test result file. E.g. `test::commentTest "Here we are testing that 1+1 is equal to 2. Awesome."`
- `test::comment "comment"`: Add a text paragraph in the test result file. E.g. `test::comment "Here we are testing that 1+1 is equal to 2. Awesome."`
- `test::endTest "title" $? "description"`: Call this function after each test to append the test results to the report file. This create a new `h3` header with the title, write the given exit code and include the stdout and stderr of the test inside markdown code. It optionally can add a description to the test.

You can check a script example to [test the Valet string library here][valet-string-lib-tests].
Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/800.roadmap/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ url: /docs/roadmap

This page lists the features that I would like to implement in Valet. They come in addition to new features described in the [issues][valet-issues].

- document the test; use the lib-test test as an example, and also link to the test:: lib.
- test the new bash:: lib
- refacto progress bar; use signal to tell the bg job to redraw the progress bar after displaying a log.
- after logging, if progress bar is in progress, we need to redraw it immediately.
Expand Down
9 changes: 7 additions & 2 deletions libraries.d/core
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
# Description: this script contains the core functions and variables and it
# should be sourced, not called directly
# Author: github.com/jcaillon
if [[ ${BASH_VERSINFO[0]:-0} -lt 5 ]]; then
printf '%s\n' "Bash 5 or higher is required to run valet."

# check the bash version (and that we are running in bash), make it POSIX compliant
# shellcheck disable=SC2292
# shellcheck disable=SC2086
# shellcheck disable=SC2128
if [ ${BASH_VERSINFO:-0} -lt 5 ] || { [ ${BASH_VERSINFO} -eq 5 ] && [ ${BASH_VERSINFO[1]:-0} -lt 1 ]; }; then
printf '%s\n' "❌ Bash 5.1 or higher is required to run valet."
exit 1
fi

Expand Down
49 changes: 45 additions & 4 deletions libraries.d/lib-io
Original file line number Diff line number Diff line change
Expand Up @@ -671,33 +671,74 @@ function io::cat() {
# The number of lines to print.
# - $3: to variable _as bool_:
# (optional) Can be set using the variable `_OPTION_TO_VARIABLE`.
# If true, the output will be stored in the variable `RETURNED_VALUE`
# If true, the output will be stored in the variable `RETURNED_ARRAY`
# instead of being printed to stdout.
# (defaults to false)
#
# ```bash
# io::head "myFile" 10
# ```

#
# > #TODO: faster with mapfile?
function io::head() {
local filePath="${1}"
local -i numberOfLines="${2}"
local toVariable="${3:-${_OPTION_TO_VARIABLE:-false}}"
RETURNED_VALUE=""
RETURNED_ARRAY=()
local IFS=$'\n' line
nb=0
while read -rd $'\n' line || [[ -n ${line:-} ]]; do
if ((nb++ >= numberOfLines)); then
break
fi
if [[ ${toVariable} == "true" ]]; then
RETURNED_VALUE+="${line}"$'\n'
RETURNED_ARRAY+=("${line}")
else
printf '%s\n' "${line}"
fi
done <"${filePath}"
}

# ## io::tail
#
# Print the last lines of a file to stdout.
# This is a pure bash equivalent of tail.
# However, because we have to read the whole file, it is not efficient for large files.
#
# - $1: **path** _as string_:
# The file to print.
# - $2: **number of lines** _as int_:
# The number of lines to print from the end of the file.
# - $3: to variable _as bool_:
# (optional) Can be set using the variable `_OPTION_TO_VARIABLE`.
# If true, the output will be stored in the variable `RETURNED_ARRAY`
# instead of being printed to stdout.
# (defaults to false)
#
# ```bash
# io::tail "myFile" 10
# ```
#
# > #TODO: use mapfile quantum to not have to read the whole file in a single go.
function io::tail() {
local filePath="${1}"
local -i numberOfLines="${2}"
local toVariable="${3:-${_OPTION_TO_VARIABLE:-false}}"
RETURNED_ARRAY=()
local IFS=$'\n'
local -a lines
mapfile -d $'\n' -t lines <"${filePath}"
local -i startLine=$(( ${#lines[@]} - numberOfLines ))
if [[ ${startLine} -lt 0 ]]; then
startLine=0
fi
if [[ ${toVariable} == "true" ]]; then
RETURNED_ARRAY=("${lines[@]:${startLine}}")
else
printf '%s\n' "${lines[@]:${startLine}}"
fi
}

# ## io::readStdIn
#
# Read the content of the standard input.
Expand Down
Loading

0 comments on commit d29fc90

Please sign in to comment.