diff --git a/.gitmodules b/.gitmodules index 13532af5..57b46adc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "test_lib/bats-core"] path = test_lib/bats-core url = https://github.com/bats-core/bats-core + branch = tags/v1.2.0 [submodule "test_lib/bats-support"] path = test_lib/bats-support url = https://github.com/bats-core/bats-support @@ -11,5 +12,5 @@ branch = tags/v2.0.0 [submodule "test_lib/bats-file"] path = test_lib/bats-file - url = https://github.com/tralston/bats-file - branch = tags/v0.3.0 + url = https://github.com/bats-core/bats-file + branch = tags/v0.3.0 diff --git a/test/README.md b/test/README.md index d227e3c1..0b19fc0f 100644 --- a/test/README.md +++ b/test/README.md @@ -1,4 +1,13 @@ -## Testing with [Bats](https://github.com/sstephenson/bats#installing-bats-from-source) +# Testing with Bats + +## Overview + +The Bash-it unit tests leverage the [Bats unit test framework for Bash](https://github.com/bats-core/bats-core). +There is no need to install Bats explicitly, the test run script will automatically download and install Bats and its dependencies. + +When making changes to Bash-it, the tests are automatically executed in a test build environment on [Travis CI](https://travis-ci.com). + +## Test Execution To execute the unit tests, please run the `run` script: @@ -10,4 +19,22 @@ To execute the unit tests, please run the `run` script: test/run ``` -The `run` script will automatically install [Bats](https://github.com/sstephenson/bats#installing-bats-from-source) if it is not already present, and will then run all tests found under the `test` directory, including subdirectories. +The `run` script will automatically install if it is not already present, and will then run all tests found under the `test` directory, including subdirectories. + +To run only a subset of the tests, you can provide the name of the test subdirectory that you want to run, e.g. like this for the tests in the `test/themes` directory: + +```bash +# If you are in the root `.bash_it` directory: +test/run test/themes +``` + +By default, the tests run in single-threaded mode. +If you want to speed up the test execution, you can install the [GNU `parallel` tool](https://www.gnu.org/software/parallel/), which is supported by Bats. +When using `parallel`, the `test/run` script will use a number of threads in parallel, depending on the available CPU cores of your system. +This can speed up test execution significantly. + +## Writing Tests + +When adding or modifying tests, please stick to the format and conventions of the existing test cases. +The `test_helper.bash` script provides a couple of reusable helper functions that you should use when writing a test case, +for example for setting up an isolated test environment. diff --git a/test/bash_it/bash_it.bats b/test/bash_it/bash_it.bats index 886453dd..af2f15e9 100644 --- a/test/bash_it/bash_it.bats +++ b/test/bash_it/bash_it.bats @@ -4,36 +4,10 @@ load ../test_helper load ../../lib/composure function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - echo "Bi : $BASH_IT" - echo "Lib: $lib_directory" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled + setup_test_fixture # Copy the test fixture to the Bash-it folder rsync -a "$BASH_IT/test/fixtures/bash_it/" "$BASH_IT/" - - # Don't pollute the user's actual $HOME directory - # Use a test home directory instead - export BASH_IT_TEST_CURRENT_HOME="${HOME}" - export BASH_IT_TEST_HOME="$(cd "${BASH_IT}/.." && pwd)/BASH_IT_TEST_HOME" - mkdir -p "${BASH_IT_TEST_HOME}" - export HOME="${BASH_IT_TEST_HOME}" -} - -function local_teardown { - export HOME="${BASH_IT_TEST_CURRENT_HOME}" - - rm -rf "${BASH_IT_TEST_HOME}" - - assert_equal "${BASH_IT_TEST_CURRENT_HOME}" "${HOME}" } @test "bash-it: verify that the test fixture is available" { diff --git a/test/completion/bash-it.completion.bats b/test/completion/bash-it.completion.bats index f35b180b..87c697e4 100644 --- a/test/completion/bash-it.completion.bats +++ b/test/completion/bash-it.completion.bats @@ -5,36 +5,7 @@ load ../../lib/composure load ../../completion/available/bash-it.completion function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - - mkdir -p "$BASH_IT"/enabled - mkdir -p "$BASH_IT"/aliases/enabled - mkdir -p "$BASH_IT"/completion/enabled - mkdir -p "$BASH_IT"/plugins/enabled - - # Don't pollute the user's actual $HOME directory - # Use a test home directory instead - export BASH_IT_TEST_CURRENT_HOME="${HOME}" - export BASH_IT_TEST_HOME="$(cd "${BASH_IT}/.." && pwd)/BASH_IT_TEST_HOME" - mkdir -p "${BASH_IT_TEST_HOME}" - export HOME="${BASH_IT_TEST_HOME}" -} - -function local_teardown { - export HOME="${BASH_IT_TEST_CURRENT_HOME}" - - rm -rf "${BASH_IT_TEST_HOME}" - - assert_equal "${BASH_IT_TEST_CURRENT_HOME}" "${HOME}" + setup_test_fixture } @test "completion bash-it: ensure that the _bash-it-comp function is available" { diff --git a/test/install/install.bats b/test/install/install.bats index 7e971d1f..01776f25 100644 --- a/test/install/install.bats +++ b/test/install/install.bats @@ -14,31 +14,7 @@ case $OSTYPE in esac function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - - # Don't pollute the user's actual $HOME directory - # Use a test home directory instead - export BASH_IT_TEST_CURRENT_HOME="${HOME}" - export BASH_IT_TEST_HOME="$(cd "${BASH_IT}/.." && pwd)/BASH_IT_TEST_HOME" - mkdir -p "${BASH_IT_TEST_HOME}" - export HOME="${BASH_IT_TEST_HOME}" -} - -function local_teardown { - export HOME="${BASH_IT_TEST_CURRENT_HOME}" - - rm -rf "${BASH_IT_TEST_HOME}" - - assert_equal "${BASH_IT_TEST_CURRENT_HOME}" "${HOME}" + setup_test_fixture } @test "install: verify that the install script exists" { diff --git a/test/install/uninstall.bats b/test/install/uninstall.bats index d71ac06a..b2439b9d 100644 --- a/test/install/uninstall.bats +++ b/test/install/uninstall.bats @@ -14,31 +14,7 @@ case $OSTYPE in esac function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - - # Don't pollute the user's actual $HOME directory - # Use a test home directory instead - export BASH_IT_TEST_CURRENT_HOME="${HOME}" - export BASH_IT_TEST_HOME="$(cd "${BASH_IT}/.." && pwd)/BASH_IT_TEST_HOME" - mkdir -p "${BASH_IT_TEST_HOME}" - export HOME="${BASH_IT_TEST_HOME}" -} - -function local_teardown { - export HOME="${BASH_IT_TEST_CURRENT_HOME}" - - rm -rf "${BASH_IT_TEST_HOME}" - - assert_equal "${BASH_IT_TEST_CURRENT_HOME}" "${HOME}" + setup_test_fixture } @test "uninstall: verify that the uninstall script exists" { diff --git a/test/lib/helpers.bats b/test/lib/helpers.bats index 6c4f706a..c3f905e7 100644 --- a/test/lib/helpers.bats +++ b/test/lib/helpers.bats @@ -11,21 +11,7 @@ cite _about _param _example _group _author _version load ../../lib/helpers function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - - mkdir -p "$BASH_IT"/enabled - mkdir -p "$BASH_IT"/aliases/enabled - mkdir -p "$BASH_IT"/completion/enabled - mkdir -p "$BASH_IT"/plugins/enabled + setup_test_fixture } # TODO Create global __is_enabled function diff --git a/test/lib/search.bats b/test/lib/search.bats index 5bbc8207..da448acc 100644 --- a/test/lib/search.bats +++ b/test/lib/search.bats @@ -18,22 +18,7 @@ cite _about _param _example _group _author _version load ../../lib/helpers function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - rm -rf "$BASH_IT"/tmp/cache - - mkdir -p "$BASH_IT"/enabled - mkdir -p "$BASH_IT"/aliases/enabled - mkdir -p "$BASH_IT"/completion/enabled - mkdir -p "$BASH_IT"/plugins/enabled + setup_test_fixture export OLD_PATH="$PATH" export PATH="/usr/bin:/bin:/usr/sbin" diff --git a/test/lib/utilities.bats b/test/lib/utilities.bats index be21fa40..6e0f4ac7 100644 --- a/test/lib/utilities.bats +++ b/test/lib/utilities.bats @@ -9,22 +9,7 @@ load ../../lib/search cite _about _param _example _group _author _version function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - rm -rf "$BASH_IT"/tmp/cache - - mkdir -p "$BASH_IT"/enabled - mkdir -p "$BASH_IT"/aliases/enabled - mkdir -p "$BASH_IT"/completion/enabled - mkdir -p "$BASH_IT"/plugins/enabled + setup_test_fixture } function has_match() { diff --git a/test/plugins/ruby.plugin.bats b/test/plugins/ruby.plugin.bats index ab11bda1..a45411e2 100755 --- a/test/plugins/ruby.plugin.bats +++ b/test/plugins/ruby.plugin.bats @@ -6,21 +6,7 @@ load ../../lib/composure load ../../plugins/available/ruby.plugin function local_setup { - mkdir -p "$BASH_IT" - lib_directory="$(cd "$(dirname "$0")" && pwd)" - # Use rsync to copy Bash-it to the temp folder - # rsync is faster than cp, since we can exclude the large ".git" folder - rsync -qavrKL -d --delete-excluded --exclude=.git $lib_directory/../../.. "$BASH_IT" - - rm -rf "$BASH_IT"/enabled - rm -rf "$BASH_IT"/aliases/enabled - rm -rf "$BASH_IT"/completion/enabled - rm -rf "$BASH_IT"/plugins/enabled - - mkdir -p "$BASH_IT"/enabled - mkdir -p "$BASH_IT"/aliases/enabled - mkdir -p "$BASH_IT"/completion/enabled - mkdir -p "$BASH_IT"/plugins/enabled + setup_test_fixture export OLD_PATH="$PATH" export PATH="/usr/bin:/bin:/usr/sbin" diff --git a/test/run b/test/run index da1aa061..88202916 100755 --- a/test/run +++ b/test/run @@ -16,4 +16,33 @@ else test_dirs=( "$1" ) fi -exec $bats_executable ${CI:+--tap} "${test_dirs[@]}" +# Make sure that the `parallel` command is installed, +# AND that it is the GNU version of `parallel`. +# If that is the case, try to guess the number of CPU cores, +# so we can run `bats` in parallel processing mode, which is a lot faster. +if command -v parallel &> /dev/null \ + && parallel -V &> /dev/null \ + && { parallel -V 2> /dev/null | grep -q '^GNU\>'; } +then + # Expect to run at least on a dual-core CPU; slightly degraded performance + # shouldn't matter otherwise. + declare -i -r test_jobs_default=2 + declare -i -r test_jobs_effective="$( + if [ "${TEST_JOBS:-detect}" = "detect" ] \ + && command -v nproc &> /dev/null + then + nproc + elif [ -n "${TEST_JOBS}" ] \ + && [ "${TEST_JOBS}" != "detect" ] + then + echo "${TEST_JOBS}" + else + echo ${test_jobs_default} + fi + )" + exec "$bats_executable" ${CI:+--tap} --jobs ${test_jobs_effective} \ + "${test_dirs[@]}" +else + # Run `bats` in single-threaded mode. + exec "$bats_executable" ${CI:+--tap} "${test_dirs[@]}" +fi diff --git a/test/test_helper.bash b/test/test_helper.bash index cd75f05a..17616f36 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -6,34 +6,12 @@ unset TODO unset SCM_CHECK unset BASH_IT_AUTOMATIC_RELOAD_AFTER_CONFIG_CHANGE -BASH_IT_TEST_DIR="${BATS_TMPDIR}/.bash_it" - -# guard against executing this block twice due to bats internals -if [ "$BASH_IT_ROOT" != "${BASH_IT_TEST_DIR}/root" ]; then - export BASH_IT_ROOT="${BASH_IT_TEST_DIR}/root" - export BASH_IT=$BASH_IT_TEST_DIR -fi - export TEST_MAIN_DIR="${BATS_TEST_DIRNAME}/.." export TEST_DEPS_DIR="${TEST_DEPS_DIR-${TEST_MAIN_DIR}/../test_lib}" # be independent of git's system configuration export GIT_CONFIG_NOSYSTEM -# Some tools, e.g. `git` use configuration files from the $HOME directory, -# which interferes with our tests. The only way to keep `git` from doing this -# seems to set HOME explicitly to a separate location. -# Refer to https://git-scm.com/docs/git-config#FILES. -unset XDG_CONFIG_HOME -export HOME="${BATS_TMPDIR}/home" -mkdir -p "${HOME}" - -# For `git` tests to run well, user name and email need to be set. -# Refer to https://git-scm.com/docs/git-commit#_commit_information. -# This goes to the test-specific config, due to the $HOME overridden above. -git config --global user.name "John Doe" -git config --global user.email "johndoe@example.com" - load "${TEST_DEPS_DIR}/bats-support/load.bash" load "${TEST_DEPS_DIR}/bats-assert/load.bash" load "${TEST_DEPS_DIR}/bats-file/load.bash" @@ -46,9 +24,58 @@ local_teardown() { true } +# This function sets up a local test fixture, i.e. a completely +# fresh and isolated Bash-it directory. This is done to avoid +# messing with your own Bash-it source directory. +# If you need this, call it in your .bats file's `local_setup` function. +setup_test_fixture() { + mkdir -p "$BASH_IT" + lib_directory="$(cd "$(dirname "$0")" && pwd)" + # Use rsync to copy Bash-it to the temp folder + # rsync is faster than cp, since we can exclude the large ".git" folder + rsync -qavrKL -d --delete-excluded --exclude=.git --exclude=enabled $lib_directory/../../../.. "$BASH_IT" + + rm -rf "$BASH_IT"/enabled + rm -rf "$BASH_IT"/aliases/enabled + rm -rf "$BASH_IT"/completion/enabled + rm -rf "$BASH_IT"/plugins/enabled + + mkdir -p "$BASH_IT"/enabled + mkdir -p "$BASH_IT"/aliases/enabled + mkdir -p "$BASH_IT"/completion/enabled + mkdir -p "$BASH_IT"/plugins/enabled + + # Some tests use the BASH_IT_TEST_HOME variable, e.g. install/uninstall + export BASH_IT_TEST_HOME="$TEST_TEMP_DIR" +} + setup() { + # The `temp_make` function from "bats-file" requires the tralston/bats-file fork, + # since the original ztombol/bats-file's `temp_make` does not work on macOS. + TEST_TEMP_DIR="$(temp_make --prefix 'bash-it-test-')" + export TEST_TEMP_DIR + + export BASH_IT_TEST_DIR="${TEST_TEMP_DIR}/.bash_it" + + export BASH_IT_ROOT="${BASH_IT_TEST_DIR}/root" + export BASH_IT=$BASH_IT_TEST_DIR + mkdir -p -- "${BASH_IT_ROOT}" + # Some tools, e.g. `git` use configuration files from the $HOME directory, + # which interferes with our tests. The only way to keep `git` from doing this + # seems to set HOME explicitly to a separate location. + # Refer to https://git-scm.com/docs/git-config#FILES. + unset XDG_CONFIG_HOME + export HOME="${TEST_TEMP_DIR}" + mkdir -p "${HOME}" + + # For `git` tests to run well, user name and email need to be set. + # Refer to https://git-scm.com/docs/git-commit#_commit_information. + # This goes to the test-specific config, due to the $HOME overridden above. + git config --global user.name "John Doe" + git config --global user.email "johndoe@example.com" + local_setup } @@ -56,49 +83,5 @@ teardown() { local_teardown rm -rf "${BASH_IT_TEST_DIR}" -} - -# Fail and display path of the link if it does not exist. Also fails -# if the path exists, but is not a link. -# This function is the logical complement of `assert_file_not_exist'. -# There is no dedicated function for checking that a link does not exist. -# -# Globals: -# BATSLIB_FILE_PATH_REM -# BATSLIB_FILE_PATH_ADD -# Arguments: -# $1 - path -# Returns: -# 0 - link exists and is a link -# 1 - otherwise -# Outputs: -# STDERR - details, on failure -assert_link_exist() { - local -r file="$1" - local -r target="$2" - if [[ ! -L "$file" ]]; then - local -r rem="$BATSLIB_FILE_PATH_REM" - local -r add="$BATSLIB_FILE_PATH_ADD" - if [[ -e "$file" ]]; then - batslib_print_kv_single 4 'path' "${file/$rem/$add}" \ - | batslib_decorate 'exists, but is not a link' \ - | fail - else - batslib_print_kv_single 4 'path' "${file/$rem/$add}" \ - | batslib_decorate 'link does not exist' \ - | fail - fi - else - if [ -n "$target" ]; then - local link_target='' - link_target=$(readlink "$file") - if [[ "$link_target" != "$target" ]]; then - batslib_print_kv_single_or_multi 8 'path' "${file/$rem/$add}" \ - 'expected' "$target" \ - 'actual' "$link_target" \ - | batslib_decorate 'link exists, but does not point to target file' \ - | fail - fi - fi - fi + temp_del "${TEST_TEMP_DIR}" } diff --git a/test_lib/bats-core b/test_lib/bats-core index 85388685..73b8d2f9 160000 --- a/test_lib/bats-core +++ b/test_lib/bats-core @@ -1 +1 @@ -Subproject commit 85388685632f85d5a1c32e6bca2deec401964cf7 +Subproject commit 73b8d2f95513207b319efe34685553b75c0b214e