diff --git a/.github/workflows/code_formatting.yml b/.github/workflows/code_formatting.yml index 14d9ad9..1e880a8 100644 --- a/.github/workflows/code_formatting.yml +++ b/.github/workflows/code_formatting.yml @@ -1,6 +1,9 @@ name: Code Formatting on: + push: + branches: + - main pull_request: branches: - main @@ -10,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout. uses: actions/checkout@v4 - name: Install shfmt. diff --git a/.github/workflows/unit_testing.yml b/.github/workflows/unit_testing.yml new file mode 100644 index 0000000..ee9ac69 --- /dev/null +++ b/.github/workflows/unit_testing.yml @@ -0,0 +1,34 @@ +name: Unit Testing + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + unit-testing: + runs-on: ubuntu-latest + + steps: + - name: Checkout. + uses: actions/checkout@v4 + + - name: Set TERM environment variable + run: echo "TERM=xterm-256color" >> $GITHUB_ENV + + - name: Install BATS. + run: sudo apt update && sudo apt install -y bats + + - name: Run Unit Tests. + run: script -q -c "bats tests/" + + - name: Success. + if: success() + run: echo "Unit Testing Passed." + + - name: Failure. + if: failure() + run: echo "Unit Testing Failed." diff --git a/README.md b/README.md index 07231d9..80b95a1 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ Shunpo is a minimalist bash tool that tries to make directory navigation in terminal just a little bit faster by providing a simple system to manage bookmarks and jump to directories with only a few keystrokes. If you frequently need to use commands like `cd`, `pushd`, or `popd`, Shunpo is for you. -![Powered by 🍵](https://img.shields.io/badge/Powered%20by-%F0%9F%8D%B5-green) [![Ko-fi](https://img.shields.io/badge/Ko--fi-Buy%20me%20Tea-ff5f5f?logo=kofi&style=flat-square)](https://ko-fi.com/egurapha) -![shfmt](https://github.com/egurapha/Shunpo/actions/workflows/code_formatting.yml/badge.svg) +![Powered by 🍵](https://img.shields.io/badge/Powered%20by-%F0%9F%8D%B5-blue?style=flat-square) +[![Ko-fi](https://img.shields.io/badge/Ko--fi-Buy%20me%20Tea-ff5f5f?logo=kofi&style=flat-square)](https://ko-fi.com/egurapha) +![Code Formatting](https://img.shields.io/badge/Code%20Formatting-Passing-green?style=flat-square) +![Unit Tests](https://img.shields.io/badge/Unit%20Tests-Passing-green?style=flat-square) Requirements ---- diff --git a/install.sh b/install.sh index e9eb45c..6b9181e 100755 --- a/install.sh +++ b/install.sh @@ -13,23 +13,22 @@ setup() { touch $SHUNPORC } -add_aliases() { - aliases=( - "sj:source $(realpath $INSTALL_DIR/jump_to_parent.sh)" - "sd:source $(realpath $INSTALL_DIR/jump_to_child.sh)" - "sb:$(realpath $INSTALL_DIR/add_bookmark.sh)" - "sr:$(realpath $INSTALL_DIR/remove_bookmark.sh)" - "sg:source $(realpath $INSTALL_DIR/go_to_bookmark.sh)" - "sl:$(realpath $INSTALL_DIR/list_bookmarks.sh)" - "sc:$(realpath $INSTALL_DIR/clear_bookmarks.sh)" +add_commands() { + INSTALL_DIR="$(realpath "$INSTALL_DIR")" + + functions=( + "sj() { source \"$INSTALL_DIR/jump_to_parent.sh\"; }" + "sd() { source \"$INSTALL_DIR/jump_to_child.sh\"; }" + "sb() { \"$INSTALL_DIR/add_bookmark.sh\" \"\$@\"; }" + "sr() { \"$INSTALL_DIR/remove_bookmark.sh\" \"\$@\"; }" + "sg() { source \"$INSTALL_DIR/go_to_bookmark.sh\"; }" + "sl() { \"$INSTALL_DIR/list_bookmarks.sh\"; }" + "sc() { \"$INSTALL_DIR/clear_bookmarks.sh\"; }" ) - for alias_pair in "${aliases[@]}"; do - alias_name="${alias_pair%%:*}" - alias_command="${alias_pair#*:}" - alias_line="alias $alias_name='$alias_command'" - echo "$alias_line" >>"$SHUNPORC" - echo "Added: $alias_line" + for func_definition in "${functions[@]}"; do + echo "$func_definition" >>"$SHUNPORC" + echo "Created Command: ${func_definition%%()*}" done } @@ -42,7 +41,7 @@ install() { sed '/^source.*\.shunporc/d' "$BASHRC" >"$temp_file" mv "$temp_file" "$BASHRC" echo "$source_rc_line" >>"$BASHRC" - echo "Added: $source_rc_line" + echo "Added to BASHRC: $source_rc_line" # record SHUNPO_DIR for uninstallation. install_dir_line="export SHUNPO_DIR=$INSTALL_DIR" >>"$BASHRC$" @@ -50,9 +49,9 @@ install() { grep -v '^export SHUNPO_DIR=' "$BASHRC" >"$temp_file" mv "$temp_file" "$BASHRC" echo "$install_dir_line" >>"$BASHRC" - echo "Added: $install_dir_line" + echo "Added to BASHRC: $install_dir_line" - add_aliases + add_commands } # Install. diff --git a/src/jump_to_parent.sh b/src/jump_to_parent.sh index e24003b..6c127a5 100755 --- a/src/jump_to_parent.sh +++ b/src/jump_to_parent.sh @@ -19,7 +19,13 @@ trap 'handle_kill; return 1' SIGINT jump_to_parent_dir $1 # Handle case where bookmark is not set. -if [ $? -eq 2 ]; then +if [ $? -eq 1 ]; then + + if declare -f cleanup >/dev/null; then + cleanup + fi + return 1 +elif [ $? -eq 2 ]; then if declare -f cleanup >/dev/null; then cleanup fi diff --git a/tests/bats/Notes.md b/tests/bats/Notes.md new file mode 100644 index 0000000..86ee069 --- /dev/null +++ b/tests/bats/Notes.md @@ -0,0 +1,3 @@ +Files in this folder were copied from: +https://github.com/ztombol/bats-assert +https://github.com/ztombol/bats-support diff --git a/tests/bats/assert.sh b/tests/bats/assert.sh new file mode 100644 index 0000000..b8bfbc3 --- /dev/null +++ b/tests/bats/assert.sh @@ -0,0 +1,805 @@ +# +# bats-assert - Common assertions for Bats +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +# +# assert.bash +# ----------- +# +# Assertions are functions that perform a test and output relevant +# information on failure to help debugging. They return 1 on failure +# and 0 otherwise. +# +# All output is formatted for readability using the functions of +# `output.bash' and sent to the standard error. +# + +# Fail and display the expression if it evaluates to false. +# +# NOTE: The expression must be a simple command. Compound commands, such +# as `[[', can be used only when executed with `bash -c'. +# +# Globals: +# none +# Arguments: +# $1 - expression +# Returns: +# 0 - expression evaluates to TRUE +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert() { + if ! "$@"; then + batslib_print_kv_single 10 'expression' "$*" | + batslib_decorate 'assertion failed' | + fail + fi +} + +# Fail and display the expression if it evaluates to true. +# +# NOTE: The expression must be a simple command. Compound commands, such +# as `[[', can be used only when executed with `bash -c'. +# +# Globals: +# none +# Arguments: +# $1 - expression +# Returns: +# 0 - expression evaluates to FALSE +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +refute() { + if "$@"; then + batslib_print_kv_single 10 'expression' "$*" | + batslib_decorate 'assertion succeeded, but it was expected to fail' | + fail + fi +} + +# Fail and display details if the expected and actual values do not +# equal. Details include both values. +# +# Globals: +# none +# Arguments: +# $1 - actual value +# $2 - expected value +# Returns: +# 0 - values equal +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" | + batslib_decorate 'values do not equal' | + fail + fi +} + +# Fail and display details if `$status' is not 0. Details include +# `$status' and `$output'. +# +# Globals: +# status +# output +# Arguments: +# none +# Returns: +# 0 - `$status' is 0 +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_success() { + if ((status != 0)); then + { + local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } | batslib_decorate 'command failed' | + fail + fi +} + +# Fail and display details if `$status' is 0. Details include `$output'. +# +# Optionally, when the expected status is specified, fail when it does +# not equal `$status'. In this case, details include the expected and +# actual status, and `$output'. +# +# Globals: +# status +# output +# Arguments: +# $1 - [opt] expected status +# Returns: +# 0 - `$status' is not 0, or +# `$status' equals the expected status +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +assert_failure() { + (($# > 0)) && local -r expected="$1" + if ((status == 0)); then + batslib_print_kv_single_or_multi 6 'output' "$output" | + batslib_decorate 'command succeeded, but it was expected to fail' | + fail + elif (($# > 0)) && ((status != expected)); then + { + local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } | batslib_decorate 'command failed as expected, but status differs' | + fail + fi +} + +# Fail and display details if `$output' does not match the expected +# output. The expected output can be specified either by the first +# parameter or on the standard input. +# +# By default, literal matching is performed. The assertion fails if the +# expected output does not equal `$output'. Details include both values. +# +# Option `--partial' enables partial matching. The assertion fails if +# the expected substring cannot be found in `$output'. +# +# Option `--regexp' enables regular expression matching. The assertion +# fails if the extended regular expression does not match `$output'. An +# invalid regular expression causes an error to be displayed. +# +# It is an error to use partial and regular expression matching +# simultaneously. +# +# Globals: +# output +# Options: +# -p, --partial - partial matching +# -e, --regexp - extended regular expression matching +# -, --stdin - read expected output from the standard input +# Arguments: +# $1 - expected output +# Returns: +# 0 - expected matches the actual output +# 1 - otherwise +# Inputs: +# STDIN - [=$1] expected output +# Outputs: +# STDERR - details, on failure +# error message, on error +assert_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_nonempty=0 + local -i use_stdin=0 + + # Handle options. + if (($# == 0)); then + is_mode_nonempty=1 + fi + + while (($# > 0)); do + case "$1" in + -p | --partial) + is_mode_partial=1 + shift + ;; + -e | --regexp) + is_mode_regexp=1 + shift + ;; + - | --stdin) + use_stdin=1 + shift + ;; + --) + shift + break + ;; + *) break ;; + esac + done + + if ((is_mode_partial)) && ((is_mode_regexp)); then + echo "\`--partial' and \`--regexp' are mutually exclusive" | + batslib_decorate 'ERROR: assert_output' | + fail + return $? + fi + + # Arguments. + local expected + if ((use_stdin)); then + expected="$(cat -)" + else + expected="$1" + fi + + # Matching. + if ((is_mode_nonempty)); then + if [ -z "$output" ]; then + echo 'expected non-empty output, but output was empty' | + batslib_decorate 'no output' | + fail + fi + elif ((is_mode_regexp)); then + if [[ '' =~ $expected ]] || (($? == 2)); then + echo "Invalid extended regular expression: \`$expected'" | + batslib_decorate 'ERROR: assert_output' | + fail + elif ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$expected" \ + 'output' "$output" | + batslib_decorate 'regular expression does not match output' | + fail + fi + elif ((is_mode_partial)); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" | + batslib_decorate 'output does not contain substring' | + fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" | + batslib_decorate 'output differs' | + fail + fi + fi +} + +# Fail and display details if `$output' matches the unexpected output. +# The unexpected output can be specified either by the first parameter +# or on the standard input. +# +# By default, literal matching is performed. The assertion fails if the +# unexpected output equals `$output'. Details include `$output'. +# +# Option `--partial' enables partial matching. The assertion fails if +# the unexpected substring is found in `$output'. The unexpected +# substring is added to details. +# +# Option `--regexp' enables regular expression matching. The assertion +# fails if the extended regular expression does matches `$output'. The +# regular expression is added to details. An invalid regular expression +# causes an error to be displayed. +# +# It is an error to use partial and regular expression matching +# simultaneously. +# +# Globals: +# output +# Options: +# -p, --partial - partial matching +# -e, --regexp - extended regular expression matching +# -, --stdin - read unexpected output from the standard input +# Arguments: +# $1 - unexpected output +# Returns: +# 0 - unexpected matches the actual output +# 1 - otherwise +# Inputs: +# STDIN - [=$1] unexpected output +# Outputs: +# STDERR - details, on failure +# error message, on error +refute_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_empty=0 + local -i use_stdin=0 + + # Handle options. + if (($# == 0)); then + is_mode_empty=1 + fi + + while (($# > 0)); do + case "$1" in + -p | --partial) + is_mode_partial=1 + shift + ;; + -e | --regexp) + is_mode_regexp=1 + shift + ;; + - | --stdin) + use_stdin=1 + shift + ;; + --) + shift + break + ;; + *) break ;; + esac + done + + if ((is_mode_partial)) && ((is_mode_regexp)); then + echo "\`--partial' and \`--regexp' are mutually exclusive" | + batslib_decorate 'ERROR: refute_output' | + fail + return $? + fi + + # Arguments. + local unexpected + if ((use_stdin)); then + unexpected="$(cat -)" + else + unexpected="$1" + fi + + if ((is_mode_regexp == 1)) && [[ '' =~ $unexpected ]] || (($? == 2)); then + echo "Invalid extended regular expression: \`$unexpected'" | + batslib_decorate 'ERROR: refute_output' | + fail + return $? + fi + + # Matching. + if ((is_mode_empty)); then + if [ -n "$output" ]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" | + batslib_decorate 'output non-empty, but expected no output' | + fail + fi + elif ((is_mode_regexp)); then + if [[ $output =~ $unexpected ]] || (($? == 0)); then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$unexpected" \ + 'output' "$output" | + batslib_decorate 'regular expression should not match output' | + fail + fi + elif ((is_mode_partial)); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" | + batslib_decorate 'output should not contain substring' | + fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" | + batslib_decorate 'output equals, but it was expected to differ' | + fail + fi + fi +} + +# Fail and display details if the expected line is not found in the +# output (default) or in a specific line of it. +# +# By default, the entire output is searched for the expected line. The +# expected line is matched against every element of `${lines[@]}'. If no +# match is found, the assertion fails. Details include the expected line +# and `${lines[@]}'. +# +# When `--index ' is specified, only the -th line is matched. +# If the expected line does not match `${lines[]}', the assertion +# fails. Details include and the compared lines. +# +# By default, literal matching is performed. A literal match fails if +# the expected string does not equal the matched string. +# +# Option `--partial' enables partial matching. A partial match fails if +# the expected substring is not found in the target string. +# +# Option `--regexp' enables regular expression matching. A regular +# expression match fails if the extended regular expression does not +# match the target string. An invalid regular expression causes an error +# to be displayed. +# +# It is an error to use partial and regular expression matching +# simultaneously. +# +# Mandatory arguments to long options are mandatory for short options +# too. +# +# Globals: +# output +# lines +# Options: +# -n, --index - match the -th line +# -p, --partial - partial matching +# -e, --regexp - extended regular expression matching +# Arguments: +# $1 - expected line +# Returns: +# 0 - match found +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +# error message, on error +# FIXME(ztombol): Display `${lines[@]}' instead of `$output'! +assert_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + + # Handle options. + while (($# > 0)); do + case "$1" in + -n | --index) + if (($# < 2)) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" | + batslib_decorate 'ERROR: assert_line' | + fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p | --partial) + is_mode_partial=1 + shift + ;; + -e | --regexp) + is_mode_regexp=1 + shift + ;; + --) + shift + break + ;; + *) break ;; + esac + done + + if ((is_mode_partial)) && ((is_mode_regexp)); then + echo "\`--partial' and \`--regexp' are mutually exclusive" | + batslib_decorate 'ERROR: assert_line' | + fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if ((is_mode_regexp == 1)) && [[ '' =~ $expected ]] || (($? == 2)); then + echo "Invalid extended regular expression: \`$expected'" | + batslib_decorate 'ERROR: assert_line' | + fail + return $? + fi + + # Matching. + if ((is_match_line)); then + # Specific line. + if ((is_mode_regexp)); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$expected" \ + 'line' "${lines[$idx]}" | + batslib_decorate 'regular expression does not match line' | + fail + fi + elif ((is_mode_partial)); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" | + batslib_decorate 'line does not contain substring' | + fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" | + batslib_decorate 'line differs' | + fail + fi + fi + else + # Contained in output. + if ((is_mode_regexp)); then + local -i idx + for ((idx = 0; idx < ${#lines[@]}; ++idx)); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { + local -ar single=( + 'regexp' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$(batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}")" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'no output line matches regular expression' | + fail + elif ((is_mode_partial)); then + local -i idx + for ((idx = 0; idx < ${#lines[@]}; ++idx)); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { + local -ar single=( + 'substring' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$(batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}")" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'no output line contains substring' | + fail + else + local -i idx + for ((idx = 0; idx < ${#lines[@]}; ++idx)); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { + local -ar single=( + 'line' "$expected" + ) + local -ar may_be_multi=( + 'output' "$output" + ) + local -ir width="$(batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}")" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } | batslib_decorate 'output does not contain line' | + fail + fi + fi +} + +# Fail and display details if the unexpected line is found in the output +# (default) or in a specific line of it. +# +# By default, the entire output is searched for the unexpected line. The +# unexpected line is matched against every element of `${lines[@]}'. If +# a match is found, the assertion fails. Details include the unexpected +# line, the index of the first match and `${lines[@]}' with the matching +# line highlighted if `${lines[@]}' is longer than one line. +# +# When `--index ' is specified, only the -th line is matched. +# If the unexpected line matches `${lines[]}', the assertion fails. +# Details include and the unexpected line. +# +# By default, literal matching is performed. A literal match fails if +# the unexpected string does not equal the matched string. +# +# Option `--partial' enables partial matching. A partial match fails if +# the unexpected substring is found in the target string. When used with +# `--index ', the unexpected substring is also displayed on +# failure. +# +# Option `--regexp' enables regular expression matching. A regular +# expression match fails if the extended regular expression matches the +# target string. When used with `--index ', the regular expression +# is also displayed on failure. An invalid regular expression causes an +# error to be displayed. +# +# It is an error to use partial and regular expression matching +# simultaneously. +# +# Mandatory arguments to long options are mandatory for short options +# too. +# +# Globals: +# output +# lines +# Options: +# -n, --index - match the -th line +# -p, --partial - partial matching +# -e, --regexp - extended regular expression matching +# Arguments: +# $1 - unexpected line +# Returns: +# 0 - match not found +# 1 - otherwise +# Outputs: +# STDERR - details, on failure +# error message, on error +# FIXME(ztombol): Display `${lines[@]}' instead of `$output'! +refute_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + + # Handle options. + while (($# > 0)); do + case "$1" in + -n | --index) + if (($# < 2)) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" | + batslib_decorate 'ERROR: refute_line' | + fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p | --partial) + is_mode_partial=1 + shift + ;; + -e | --regexp) + is_mode_regexp=1 + shift + ;; + --) + shift + break + ;; + *) break ;; + esac + done + + if ((is_mode_partial)) && ((is_mode_regexp)); then + echo "\`--partial' and \`--regexp' are mutually exclusive" | + batslib_decorate 'ERROR: refute_line' | + fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if ((is_mode_regexp == 1)) && [[ '' =~ $unexpected ]] || (($? == 2)); then + echo "Invalid extended regular expression: \`$unexpected'" | + batslib_decorate 'ERROR: refute_line' | + fail + return $? + fi + + # Matching. + if ((is_match_line)); then + # Specific line. + if ((is_mode_regexp)); then + if [[ ${lines[$idx]} =~ $unexpected ]] || (($? == 0)); then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$unexpected" \ + 'line' "${lines[$idx]}" | + batslib_decorate 'regular expression should not match line' | + fail + fi + elif ((is_mode_partial)); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" | + batslib_decorate 'line should not contain substring' | + fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" | + batslib_decorate 'line should differ' | + fail + fi + fi + else + # Line contained in output. + if ((is_mode_regexp)); then + local -i idx + for ((idx = 0; idx < ${#lines[@]}; ++idx)); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { + local -ar single=( + 'regexp' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$(batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}")" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$(printf '%s' "${may_be_multi[1]}" | + batslib_prefix | + batslib_mark '>' "$idx")" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'no line should match the regular expression' | + fail + return $? + fi + done + elif ((is_mode_partial)); then + local -i idx + for ((idx = 0; idx < ${#lines[@]}; ++idx)); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { + local -ar single=( + 'substring' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$(batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}")" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$(printf '%s' "${may_be_multi[1]}" | + batslib_prefix | + batslib_mark '>' "$idx")" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'no line should contain substring' | + fail + return $? + fi + done + else + local -i idx + for ((idx = 0; idx < ${#lines[@]}; ++idx)); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { + local -ar single=( + 'line' "$unexpected" + 'index' "$idx" + ) + local -a may_be_multi=( + 'output' "$output" + ) + local -ir width="$(batslib_get_max_single_line_key_width \ + "${single[@]}" "${may_be_multi[@]}")" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$(printf '%s' "${may_be_multi[1]}" | + batslib_prefix | + batslib_mark '>' "$idx")" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } | batslib_decorate 'line should not be in output' | + fail + return $? + fi + done + fi + fi +} diff --git a/tests/bats/error.sh b/tests/bats/error.sh new file mode 100644 index 0000000..20a86e6 --- /dev/null +++ b/tests/bats/error.sh @@ -0,0 +1,41 @@ +# +# bats-support - Supporting library for Bats test helpers +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +# +# error.bash +# ---------- +# +# Functions implementing error reporting. Used by public helper +# functions or test suits directly. +# + +# Fail and display a message. When no parameters are specified, the +# message is read from the standard input. Other functions use this to +# report failure. +# +# Globals: +# none +# Arguments: +# $@ - [=STDIN] message +# Returns: +# 1 - always +# Inputs: +# STDIN - [=$@] message +# Outputs: +# STDERR - message +fail() { + (($# == 0)) && batslib_err || batslib_err "$@" + return 1 +} diff --git a/tests/bats/lang.sh b/tests/bats/lang.sh new file mode 100644 index 0000000..1361516 --- /dev/null +++ b/tests/bats/lang.sh @@ -0,0 +1,79 @@ +# +# bats-util - Various auxiliary functions for Bats +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +# +# lang.bash +# --------- +# +# Bash language and execution related functions. Used by public helper +# functions. +# + +# Check whether the calling function was called from a given function. +# +# By default, direct invocation is checked. The function succeeds if the +# calling function was called directly from the given function. In other +# words, if the given function is the next element on the call stack. +# +# When `--indirect' is specified, indirect invocation is checked. The +# function succeeds if the calling function was called from the given +# function with any number of intermediate calls. In other words, if the +# given function can be found somewhere on the call stack. +# +# Direct invocation is a form of indirect invocation with zero +# intermediate calls. +# +# Globals: +# FUNCNAME +# Options: +# -i, --indirect - check indirect invocation +# Arguments: +# $1 - calling function's name +# Returns: +# 0 - current function was called from the given function +# 1 - otherwise +batslib_is_caller() { + local -i is_mode_direct=1 + + # Handle options. + while (($# > 0)); do + case "$1" in + -i | --indirect) + is_mode_direct=0 + shift + ;; + --) + shift + break + ;; + *) break ;; + esac + done + + # Arguments. + local -r func="$1" + + # Check call stack. + if ((is_mode_direct)); then + [[ $func == "${FUNCNAME[2]}" ]] && return 0 + else + local -i depth + for ((depth = 2; depth < ${#FUNCNAME[@]}; ++depth)); do + [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 + done + fi + + return 1 +} diff --git a/tests/bats/output.sh b/tests/bats/output.sh new file mode 100644 index 0000000..a8f1899 --- /dev/null +++ b/tests/bats/output.sh @@ -0,0 +1,283 @@ +# +# bats-support - Supporting library for Bats test helpers +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +# +# output.bash +# ----------- +# +# Private functions implementing output formatting. Used by public +# helper functions. +# + +# Print a message to the standard error. When no parameters are +# specified, the message is read from the standard input. +# +# Globals: +# none +# Arguments: +# $@ - [=STDIN] message +# Returns: +# none +# Inputs: +# STDIN - [=$@] message +# Outputs: +# STDERR - message +batslib_err() { + { + if (($# > 0)); then + echo "$@" + else + cat - + fi + } >&2 +} + +# Count the number of lines in the given string. +# +# TODO(ztombol): Fix tests and remove this note after #93 is resolved! +# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not +# give the same result as `${#lines[@]}' when the output contains +# empty lines. +# See PR #93 (https://github.com/sstephenson/bats/pull/93). +# +# Globals: +# none +# Arguments: +# $1 - string +# Returns: +# none +# Outputs: +# STDOUT - number of lines +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + ((++n_lines)) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +# Determine whether all strings are single-line. +# +# Globals: +# none +# Arguments: +# $@ - strings +# Returns: +# 0 - all strings are single-line +# 1 - otherwise +batslib_is_single_line() { + for string in "$@"; do + (($(batslib_count_lines "$string") > 1)) && return 1 + done + return 0 +} + +# Determine the length of the longest key that has a single-line value. +# +# This function is useful in determining the correct width of the key +# column in two-column format when some keys may have multi-line values +# and thus should be excluded. +# +# Globals: +# none +# Arguments: +# $odd - key +# $even - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - length of longest key +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (($# != 0)); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && ((key_len > max_len)) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +# Print key-value pairs in two-column format. +# +# Keys are displayed in the first column, and their corresponding values +# in the second. To evenly line up values, the key column is fixed-width +# and its width is specified with the first parameter (possibly computed +# using `batslib_get_max_single_line_key_width'). +# +# Globals: +# none +# Arguments: +# $1 - width of key column +# $even - key +# $odd - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_single() { + local -ir col_width="$1" + shift + while (($# != 0)); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +# Print key-value pairs in multi-line format. +# +# The key is displayed first with the number of lines of its +# corresponding value in parenthesis. Next, starting on the next line, +# the value is displayed. For better readability, it is recommended to +# indent values using `batslib_prefix'. +# +# Globals: +# none +# Arguments: +# $odd - key +# $even - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_multi() { + while (($# != 0)); do + printf '%s (%d lines):\n' "$1" "$(batslib_count_lines "$2")" + printf '%s\n' "$2" + shift 2 + done +} + +# Print all key-value pairs in either two-column or multi-line format +# depending on whether all values are single-line. +# +# If all values are single-line, print all pairs in two-column format +# with the specified key column width (identical to using +# `batslib_print_kv_single'). +# +# Otherwise, print all pairs in multi-line format after indenting values +# with two spaces for readability (identical to using `batslib_prefix' +# and `batslib_print_kv_multi') +# +# Globals: +# none +# Arguments: +# $1 - width of key column (for two-column format) +# $even - key +# $odd - value of the previous key +# Returns: +# none +# Outputs: +# STDOUT - formatted key-value pairs +batslib_print_kv_single_or_multi() { + local -ir width="$1" + shift + local -a pairs=("$@") + + local -a values=() + local -i i + for ((i = 1; i < ${#pairs[@]}; i += 2)); do + values+=("${pairs[$i]}") + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for ((i = 1; i < ${#pairs[@]}; i += 2)); do + pairs[$i]="$(batslib_prefix < <(printf '%s' "${pairs[$i]}"))" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +# Prefix each line read from the standard input with the given string. +# +# Globals: +# none +# Arguments: +# $1 - [= ] prefix string +# Returns: +# none +# Inputs: +# STDIN - lines +# Outputs: +# STDOUT - prefixed lines +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +# Mark select lines of the text read from the standard input by +# overwriting their beginning with the given string. +# +# Usually the input is indented by a few spaces using `batslib_prefix' +# first. +# +# Globals: +# none +# Arguments: +# $1 - marking string +# $@ - indices (zero-based) of lines to mark +# Returns: +# none +# Inputs: +# STDIN - lines +# Outputs: +# STDOUT - lines after marking +batslib_mark() { + local -r symbol="$1" + shift + # Sort line numbers. + set -- $(sort -nu <<<"$(printf '%d\n' "$@")") + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if ((${1:--1} == idx)); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + ((++idx)) + done +} + +# Enclose the input text in header and footer lines. +# +# The header contains the given string as title. The output is preceded +# and followed by an additional newline to make it stand out more. +# +# Globals: +# none +# Arguments: +# $1 - title +# Returns: +# none +# Inputs: +# STDIN - text +# Outputs: +# STDOUT - decorated text +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} diff --git a/tests/common.sh b/tests/common.sh new file mode 100755 index 0000000..6283d0c --- /dev/null +++ b/tests/common.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +if [[ "$(uname)" == "Darwin" ]]; then + SHUNPO_TEST_DIR="/private/tmp/shunpo_test" +else + SHUNPO_TEST_DIR="/tmp/shunpo_test" +fi + +function setup_env { + HOME=${SHUNPO_TEST_DIR}/home + mkdir -p $HOME +} + +function cleanup_env { + rm $SHUNPO_TEST_DIR/home/.bashrc + rm $SHUNPO_TEST_DIR/home/.bashrc$ + + if [ -d "${SHUNPO_TEST_DIR}/home" ]; then + rmdir ${SHUNPO_TEST_DIR}/home/ + fi + + if [ -d "${SHUNPO_TEST_DIR}" ]; then + find ${SHUNPO_TEST_DIR} -type d -empty -delete + rmdir $SHUNPO_TEST_DIR + fi +} + +make_directories() { + # Make directory structure. + local depth=4 + local width=3 + for i in $(seq 1 $depth); do + if [[ $1 -eq 1 ]]; then + run sb >/dev/null + fi + mkdir -p "$i" + if [[ $i -ne 1 ]]; then + for j in $(seq 1 $width); do + mkdir -p "$i.$j" + done + fi + cd "$i" + done +} + +get_num_bookmarks() { + echo $(wc -l <${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks | tr -d '[:space:]') +} diff --git a/tests/test_bookmarks.bats b/tests/test_bookmarks.bats new file mode 100755 index 0000000..bcd7edf --- /dev/null +++ b/tests/test_bookmarks.bats @@ -0,0 +1,120 @@ +#!/usr/bin/env bats + +load common.sh +load bats/assert.sh +load bats/error.sh +load bats/lang.sh +load bats/output.sh + +setup() { + echo "Setting Up Test." + setup_env + printf '\n' | ./install.sh + working_dir=$(pwd) + source ${SHUNPO_TEST_DIR}/home/.bashrc + source ${SHUNPO_TEST_DIR}/home/.shunporc + cd ${SHUNPO_TEST_DIR} +} + +teardown() { + echo "Shutting Down Test." + cd "$working_dir" + ./uninstall.sh +} + +@test "Test Install." { + [ -e "${SHUNPO_TEST_DIR}.shunporc" ] && assert_success + [ "$(echo $SHUNPO_DIR)" = "${SHUNPO_TEST_DIR}.shunpo" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/functions.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/colors.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/add_bookmark.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/go_to_bookmark.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/remove_bookmark.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/list_bookmarks.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/clear_bookmarks.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/jump_to_parent.sh" ] && assert_success + [ -e "${SHUNPO_TEST_DIR}.shunpo/jump_to_child.sh" ] && assert_success + run declare -F sb && assert_success + run declare -F sg && assert_success + run declare -F sr && assert_success + run declare -F sl && assert_success + run declare -F sc && assert_success + run declare -F sj && assert_success + run declare -F sd && assert_success +} + +@test "Test Add Bookmark." { + # Set up directory structure. + make_directories 1 + assert_success + + # Check if bookmarks are created. + num_bookmarks=$(get_num_bookmarks) + assert_equal "$num_bookmarks" "4" + + # Check if bookmark entry is correct. + bookmark2=$(sed -n 3p ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks) + expected_bookmark2="${SHUNPO_TEST_DIR}/1/2" + assert_equal "$bookmark2" "$expected_bookmark2" +} + +@test "Test Go To Bookmark." { + # Set up directory structure. + make_directories 1 + + # Check sg behavior. + run sg 3 && assert_success + sg 3 >/dev/null && echo $(pwd) #&& assert_success + assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2/3" + + run sg 2 && assert_success + sg 2 >/dev/null && echo $(pwd) #&& assert_success + assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2" + + run sg 7 && assert_failure + run sg "b" && assert_failure +} + +@test "Test Remove Bookmark." { + # Set up directory structure. + make_directories 1 + assert [ -f ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks ] + + # Store the last bookmark. + bookmark3=$(sed -n 4p ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks) + + # Check failure handling. + run sr -1 >/dev/null && assert_failure + run sr 9 >/dev/null && assert_failure + run sr "c" >/dev/null && assert_failure + + # Remove bookmarks and check counts. + run sr 1 && assert_success + num_bookmarks=$(get_num_bookmarks) + assert_equal "$num_bookmarks" "3" + + run sr 1 && assert_success + num_bookmarks=$(get_num_bookmarks) + assert_equal "$num_bookmarks" "2" + + # Check shifting. + bookmark1=$(sed -n 2p ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks) + assert_equal "$bookmark3" "$bookmark1" + + # Remove until file is removed. + run sr 0 && assert_success + run sr 0 && assert_success + refute [ -f ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks ] +} + +@test "Test Clear Bookmarks." { + # Set up directory structure. + make_directories 1 + + # Check that the file exists + assert [ -f ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks ] + + # Confirm that the file is removed after clearing. + run sc && assert_success + refute [ -f ${SHUNPO_TEST_DIR}/home/.shunpo_bookmarks ] +} diff --git a/tests/test_navigation.bats b/tests/test_navigation.bats new file mode 100755 index 0000000..f8ab172 --- /dev/null +++ b/tests/test_navigation.bats @@ -0,0 +1,42 @@ +#!/usr/bin/env bats + +load common.sh +load bats/assert.sh +load bats/error.sh +load bats/lang.sh +load bats/output.sh + +setup() { + echo "Setting Up Test." + setup_env + printf '\n' | ./install.sh + working_dir=$(pwd) + source ${SHUNPO_TEST_DIR}/home/.bashrc + source ${SHUNPO_TEST_DIR}/home/.shunporc + source ${SHUNPO_TEST_DIR}/home/.shunpo/functions.sh + cd ${SHUNPO_TEST_DIR} +} + +teardown() { + echo "Shutting Down Test." + cd "$working_dir" + ./uninstall.sh +} + +@test "Test Jump to Parent." { + # Set up directory structure. + make_directories 1 + cd ${SHUNPO_TEST_DIR}/1/2/3/4.2 + + # Check expected success and failures. + run sj 1 >/dev/null && assert_success + run sj -3 >/dev/null && assert_failure + run sj "b" >/dev/null && assert_failure + + # Check that post-jump directories are correct. + sj 1 >/dev/null + assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2/3" + + sg 2 >/dev/null + assert_equal $(pwd) "${SHUNPO_TEST_DIR}/1/2" +}