Adds fedora testing

pull/662/head
sobolevn 3 years ago committed by Nikita Sobolev
parent 9efae709db
commit 28dbdf3fde

@ -0,0 +1,19 @@
FROM fedora:34
LABEL maintainer="mail@sobolevn.me"
LABEL vendor="git-secret team"
RUN dnf -y update \
&& dnf install -y \
# Direct dependencies:
bash \
gawk \
git \
gnupg \
# Assumed to be present:
diffutils \
findutils \
procps \
make \
&& dnf clean all \
&& rm -rf /var/cache/yum

1
.gitattributes vendored

@ -0,0 +1 @@
vendor/ linguist-vendored

@ -8,7 +8,7 @@ on:
workflow_dispatch:
jobs:
build:
docker-ci:
runs-on: ubuntu-latest
strategy:
matrix:
@ -16,7 +16,7 @@ jobs:
- debian
- ubuntu
- alpine
# - fedora
- fedora
# - centos
steps:

2
.gitignore vendored

@ -136,3 +136,5 @@ build/
# Kitchen files
Gemfile.lock
.kitchen/
.gitsecret/keys/random_seed
!*.secret

@ -54,6 +54,11 @@ ci: clean
docker build -f ".ci/docker/$${GITSECRET_DOCKER_ENV}/Dockerfile" -t "$${GITSECRET_DOCKER_ENV}:latest" .
docker run --rm --volume="$${PWD}:/code" -w /code "$${GITSECRET_DOCKER_ENV}" make test
.PHONY: lint
lint:
find src/ .ci/ utils/ -type f -name '*.sh' -print0 | xargs -0 -I {} shellcheck {}
find tests/ -type f -name '*.bats' -o -name '*.bash' -print0 | xargs -0 -I {} shellcheck {}
#
# Manuals:
#
@ -71,25 +76,6 @@ build-man: install-ronn clean-man git-secret
touch man/*/*.ronn
export GITSECRET_VERSION=`./git-secret --version` && ronn --roff --organization="sobolevn" --manual="git-secret $${GITSECRET_VERSION}" man/*/*.ronn
#
# Development:
#
.PHONY: install-hooks
install-hooks:
ln -fs "${PWD}/utils/hooks/pre-commit.sh" "${PWD}/.git/hooks/pre-commit"; \
chmod +x "${PWD}/.git/hooks/pre-commit"; sync; \
ln -fs "${PWD}/utils/hooks/post-commit.sh" "${PWD}/.git/hooks/post-commit"; \
chmod +x "${PWD}/.git/hooks/post-commit"; sync
.PHONY: develop
develop: clean build install-hooks
.PHONY: lint
lint:
find src/ .ci/ utils/ -type f -name '*.sh' -print0 | xargs -0 -I {} shellcheck {}
find tests/ -type f -name '*.bats' -o -name '*.bash' -print0 | xargs -0 -I {} shellcheck {}
#
# Packaging:
#

@ -1,9 +1,10 @@
#!/usr/bin/env bash
# Folders:
_SECRETS_DIR=${SECRETS_DIR:-".gitsecret"}
_SECRETS_DIR=${SECRETS_DIR:-".gitsecret"}
# if SECRETS_DIR env var is set, use that instead of .gitsecret
# for full path to secrets dir, use _get_secrets_dir() from _git_secret_tools.sh
# for full path to secrets dir, use _get_secrets_dir()
# from _git_secret_tools.sh
_SECRETS_DIR_KEYS="${_SECRETS_DIR}/keys"
_SECRETS_DIR_PATHS="${_SECRETS_DIR}/paths"
@ -12,7 +13,7 @@ _SECRETS_DIR_KEYS_TRUSTDB="${_SECRETS_DIR_KEYS}/trustdb.gpg"
_SECRETS_DIR_PATHS_MAPPING="${_SECRETS_DIR_PATHS}/mapping.cfg"
# _SECRETS_VERBOSE is expected to be empty or '1'.
# _SECRETS_VERBOSE is expected to be empty or '1'.
# Empty means 'off', any other value means 'on'.
# shellcheck disable=SC2153
if [[ -n "$SECRETS_VERBOSE" ]] && [[ "$SECRETS_VERBOSE" -ne 0 ]]; then
@ -125,7 +126,7 @@ function _os_based {
Linux)
"$1_linux" "${@:2}"
;;
MINGW*)
"$1_linux" "${@:2}"
;;
@ -159,6 +160,7 @@ function _clean_windows_path {
echo "$1" | sed 's#^\([a-zA-Z]\):/#/\1/#'
}
function _set_config {
# This function creates a line in the config, or alters it.
@ -193,7 +195,6 @@ function _file_has_line {
}
# this sets the global variable 'temporary_filename'
# currently this function is only used by 'hide'
function _temporary_file {
@ -263,6 +264,7 @@ function _fsdb_rm_record {
_gawk_inplace -v key="'$key'" "'$AWK_FSDB_RM_RECORD'" "$fsdb"
}
function _fsdb_clear_hashes {
# First parameter is the path to fsdb
local fsdb="$1" # required
@ -273,6 +275,7 @@ function _fsdb_clear_hashes {
# Manuals:
function _show_manual_for {
local function_name="$1" # required
@ -293,6 +296,7 @@ function _invalid_option_for {
# VCS:
function _check_ignore {
local filename="$1" # required
@ -347,6 +351,7 @@ function _is_inside_git_tree {
echo "$result"
}
function _is_tracked_in_git {
local filename="$1" # required
local result
@ -360,10 +365,12 @@ function _is_tracked_in_git {
}
# This can give unexpected .git dir when used in a _subdirectory_ of another git repo; See #431 and #433.
# This can give unexpected .git dir when used in a _subdirectory_
# of another git repo; See #431 and #433.
function _get_git_root_path {
# We need this function to get the location of the `.git` folder,
# since `.gitsecret` (or value set by SECRETS_DIR env var) must be in the same dir.
# since `.gitsecret` (or value set by SECRETS_DIR env var)
# must be in the same dir.
local result
result=$(_clean_windows_path "$(git rev-parse --show-toplevel)")
@ -417,6 +424,7 @@ function _message {
echo "git-secret: $message"
}
function _abort {
local message="$1" # required
local exit_code=${2:-"1"} # defaults to 1
@ -425,6 +433,7 @@ function _abort {
exit "$exit_code"
}
# _warn() sends warnings to stdout so user sees them
function _warn {
local message="$1" # required
@ -432,6 +441,7 @@ function _warn {
>&2 echo "git-secret: warning: $message"
}
# _warn_or_abort "$error_message" "$exit_code" "$error_ok"
function _warn_or_abort {
local message="$1" # required
@ -439,8 +449,8 @@ function _warn_or_abort {
local error_ok=${3:-0} # can be 0 or 1
if [[ "$error_ok" -eq "0" ]]; then
if [[ "$exit_code" -eq "0" ]]; then
# if caller sends an exit_code of 0, we change it to 1 before aborting.
if [[ "$exit_code" -eq "0" ]]; then
# if caller sends an exit_code of 0, we change it to 1 before aborting.
exit_code=1
fi
_abort "$message" "$exit_code"
@ -449,6 +459,7 @@ function _warn_or_abort {
fi
}
function _find_and_clean {
# required:
local pattern="$1" # can be any string pattern
@ -471,16 +482,16 @@ function _find_and_clean_formatted {
local pattern="$1" # can be any string pattern
local outputs
outputs=$(_find_and_clean "$pattern" 2>&1)
outputs=$(_find_and_clean "$pattern")
if [[ -n "$_SECRETS_VERBOSE" ]] && [[ -n "$outputs" ]]; then
# shellcheck disable=SC2001
echo "$outputs" | sed "s/^/git-secret: cleaning: /"
# shellcheck disable=SC2001
echo "$outputs" | sed "s/^/git-secret: cleaning: /"
fi
}
# this sets the global array variable 'filenames'
# this sets the global array variable 'filenames'
function _list_all_added_files {
local path_mappings
path_mappings=$(_get_secrets_dir_paths_mapping)
@ -531,7 +542,7 @@ function _secrets_dir_is_not_ignored {
function _exe_is_busybox {
local exe
exe=$1
exe="$1"
# we assume stat is from busybox if it's a symlink
local is_busybox=0
@ -543,6 +554,7 @@ function _exe_is_busybox {
echo "$is_busybox"
}
# this is used by just about every command
function _user_required {
# This function does a bunch of validations:
@ -571,21 +583,23 @@ function _user_required {
_abort "$error_message"
fi
if [[ "$exit_code" -ne 0 ]]; then
# this might catch corner case where gpg --list-keys shows
# 'gpg: skipped packet of type 12 in keybox' warnings but succeeds?
# this might catch corner case where gpg --list-keys shows
# 'gpg: skipped packet of type 12 in keybox' warnings but succeeds?
# See #136
echo "$keys_exist" # show whatever _did_ come out of gpg
_abort "problem listing public keys with gpg: exit code $exit_code"
fi
}
# note: this has the same 'username matching' issue described in
# note: this has the same 'username matching' issue described in
# https://github.com/sobolevn/git-secret/issues/268
# where it will match emails that have other emails as substrings.
# we need to use fingerprints for a unique key id with gpg.
function _get_user_key_expiry {
# This function returns the user's key's expiry, as an epoch.
# It will return the empty string if there is no expiry date for the user's key
# This function returns the user's key's expiry, as an epoch.
# It will return the empty string
# if there is no expiry date for the user's key
local username="$1"
local line
@ -597,30 +611,38 @@ function _get_user_key_expiry {
local expiry_epoch
expiry_epoch=$(echo "$line" | cut -d: -f7)
#echo "# got expiry_epoch: $expiry_epoch" >&3
echo "$expiry_epoch"
}
function _assert_keyring_contains_emails {
local homedir=$1
local keyring_name=$2
local emails=$3
_assert_keyring_emails "$homedir" "$keyring_name" "$emails" 1 # 1 here means 'expect $emails in keyring'
local homedir="$1"
local keyring_name="$2"
local emails="$3"
# 1 here means 'expect $emails in keyring':
_assert_keyring_emails "$homedir" "$keyring_name" "$emails" 1
}
function _assert_keyring_doesnt_contain_emails {
local homedir=$1
local keyring_name=$2
local emails=$3
_assert_keyring_emails "$homedir" "$keyring_name" "$emails" 0 # 0 here means 'don't expect $emails in keyring'
local homedir="$1"
local keyring_name="$2"
local emails="$3"
# 0 here means 'don't expect $emails in keyring':
_assert_keyring_emails "$homedir" "$keyring_name" "$emails" 0
}
function _assert_keyring_emails {
local homedir=$1
local keyring_name=$2
local emails=$3
local expected=$4 # set this to 0 to not expect the email in the keyring; 1 to expect the email in the keyring
local homedir="$1"
local keyring_name="$2"
local emails="$3"
# set this to:
# 0 to not expect the email in the keyring;
# 1 to expect the email in the keyring
local expected="$4"
local gpg_uids
gpg_uids=$(_get_users_in_gpg_keyring "$homedir")
@ -639,25 +661,24 @@ function _assert_keyring_emails {
_abort "no key found in gpg $keyring_name for: $email"
elif [[ $emails_found -gt 1 ]]; then
_abort "$emails_found keys found in gpg $keyring_name for: $email"
fi
fi
else
if [[ $emails_found -gt 0 ]]; then
_abort "$emails_found keys found in gpg $keyring_name for: $email"
fi
fi
fi
done
}
function _get_encrypted_filename {
local filename
filename="$(dirname "$1")/$(basename "$1" "$SECRETS_EXTENSION")"
echo "${filename}${SECRETS_EXTENSION}" | sed -e 's#^\./##'
}
# this is used throughout this file, and in 'whoknows'
function _get_users_in_gpg_keyring {
# show the users in the gpg keyring.
@ -671,7 +692,7 @@ function _get_users_in_gpg_keyring {
fi
## We use --fixed-list-mode so older versions of gpg emit 'uid:' lines.
## Gawk splits on colon as --with-colon, matches field 1 as 'uid',
## Gawk splits on colon as --with-colon, matches field 1 as 'uid',
result=$($SECRETS_GPG_COMMAND "${args[@]}" --no-permission-warning --list-public-keys --with-colon --fixed-list-mode | \
gawk -F: '$1=="uid"' )
@ -694,10 +715,11 @@ function _get_users_in_gpg_keyring {
echo "$emails"
}
function _extract_emails_from_gpg_output {
local result=$1
# gensub() outputs email from <> within field 10, "User-ID". If there's no <>, then field is just an email address
# gensub() outputs email from <> within field 10, "User-ID". If there's no <>, then field is just an email address
# (and maybe a comment) and we pass it through.
# Sed at the end removes any 'comment' that appears in parentheses, for #530
# 3>&- closes fd 3 for bats, see https://github.com/bats-core/bats-core#file-descriptor-3-read-this-if-bats-hangs
@ -706,11 +728,12 @@ function _extract_emails_from_gpg_output {
echo "$emails"
}
function _get_users_in_gitsecret_keyring {
# show the users in the gitsecret keyring.
local secrets_dir_keys
secrets_dir_keys=$(_get_secrets_dir_keys)
local result
result=$(_get_users_in_gpg_keyring "$secrets_dir_keys")
@ -724,7 +747,8 @@ function _get_recipients {
# It basically just parses the `gpg` public keys
local result
result=$(_get_users_in_gitsecret_keyring | sed 's/^/-r/') # put -r before each user
# put -r before each user:
result=$(_get_users_in_gitsecret_keyring | sed 's/^/-r/')
echo "$result"
}
@ -784,7 +808,7 @@ function _decrypt {
set -e # re-enable set -e
# note that according to https://github.com/sobolevn/git-secret/issues/238 ,
# note that according to https://github.com/sobolevn/git-secret/issues/238 ,
# it's possible for gpg to return a 0 exit code but not have decrypted the file
#echo "# gpg exit code: $exit_code, error_ok: $error_ok" >&3
if [[ "$exit_code" -ne "0" ]]; then
@ -794,4 +818,3 @@ function _decrypt {
# at this point the file should be written to disk or output to stdout
}

@ -74,8 +74,7 @@ function _secret_files_exists {
# this test is like above, but uses SECRETS_VERBOSE env var
@test "run 'clean' with 'SECRETS_VERBOSE=1'" {
export SECRETS_VERBOSE=1
run git secret clean
SECRETS_VERBOSE=1 run git secret clean
[ "$status" -eq 0 ]
# Output must be verbose:
@ -85,11 +84,9 @@ function _secret_files_exists {
# this test is like above, but sets SECRETS_VERBOSE env var to 0
# and expected non-verbose output
@test "run 'clean' with 'SECRETS_VERBOSE=0'" {
export SECRETS_VERBOSE=0
run git secret clean
SECRETS_VERBOSE=0 run git secret clean
[ "$status" -eq 0 ]
# Output must not be verbose:
[[ "$output" != *"cleaning"* ]]
}

@ -5,7 +5,7 @@ set -e
# shellcheck disable=SC1090,SC1091
source "${SECRET_PROJECT_ROOT}/utils/build-utils.sh"
preinstall_files "-c"
preinstall_files '-c'
# Building .deb package:
cd "$SCRIPT_DEST_DIR" && build_package "apk"

@ -23,13 +23,13 @@ function integration_tests {
apk add --update-cache
# Testing the installation:
apk info | grep "git-secret"
apk info | grep 'git-secret'
# lint says to use 'command -v' and not 'which'
command -v "git-secret"
command -v 'git-secret'
# Test the manuals:
man --where "git-secret" # .7
man --where "git-secret-init" # .1
man --where 'git-secret' # .7
man --where 'git-secret-init' # .1
}
integration_tests

@ -6,9 +6,9 @@ set -e
READ_PEM=0644
EXEC_PEM=0755
SCRIPT_NAME="git-secret"
SCRIPT_DESCRIPTION="A bash-tool to store your private data inside a git repository."
SCRIPT_VERSION=$(bash "${PWD}"/git-secret --version)
SCRIPT_NAME='git-secret'
SCRIPT_DESCRIPTION='A bash-tool to store your private data inside a git repository.'
SCRIPT_VERSION="$(bash "${PWD}"/git-secret --version)"
# This might be overridden someday:
: "${SCRIPT_EPOCH:=0}"
@ -23,16 +23,16 @@ SCRIPT_DEST_DIR="${SCRIPT_BUILD_DIR}/buildroot"
function locate_apk {
find "$SCRIPT_DEST_DIR" -maxdepth 1 -name "*.apk" | head -1
}
find "$SCRIPT_DEST_DIR" -maxdepth 1 -name '*.apk' | head -1
} # TODO: use an argument instead
function locate_deb {
find "$SCRIPT_DEST_DIR" -maxdepth 1 -name "*.deb" | head -1
find "$SCRIPT_DEST_DIR" -maxdepth 1 -name '*.deb' | head -1
}
function locate_rpm {
find "$SCRIPT_DEST_DIR" -maxdepth 1 -name "*.rpm" | head -1
find "$SCRIPT_DEST_DIR" -maxdepth 1 -name '*.rpm' | head -1
}
@ -45,7 +45,9 @@ function preinstall_files {
mkdir -p "$SCRIPT_DEST_DIR"
# Coping the files inside the build folder:
install -D "${dir_switch}" -b -m "$EXEC_PEM" "${dir_switch}" "git-secret" "${SCRIPT_DEST_DIR}/usr/bin/git-secret"
install -D "${dir_switch}" \
-b -m "$EXEC_PEM" "${dir_switch}" 'git-secret' \
"${SCRIPT_DEST_DIR}/usr/bin/git-secret"
install -m "$EXEC_PEM" -d "${SCRIPT_DEST_DIR}/usr/share/man/man1"
install -m "$EXEC_PEM" -d "${SCRIPT_DEST_DIR}/usr/share/man/man7"
for file in man/man1/* ; do
@ -53,9 +55,12 @@ function preinstall_files {
continue
fi
install -D "${dir_switch}" -b -m "$READ_PEM" "${dir_switch}" "$file" "${SCRIPT_DEST_DIR}/usr/share/$file"
install -D "${dir_switch}" \
-b -m "$READ_PEM" "${dir_switch}" "$file" \
"${SCRIPT_DEST_DIR}/usr/share/$file"
done
install -D "${dir_switch}" -b -m "$READ_PEM" "${dir_switch}" "man/man7/git-secret.7" \
install -D "${dir_switch}" \
-b -m "$READ_PEM" "${dir_switch}" 'man/man7/git-secret.7' \
"${SCRIPT_DEST_DIR}/usr/share/man/man7/git-secret.7"
}

@ -5,10 +5,10 @@ set -e
# shellcheck disable=SC1090,SC1091
source "${SECRET_PROJECT_ROOT}/utils/build-utils.sh"
preinstall_files "-T"
preinstall_files '-T'
# Building .deb package:
cd "$SCRIPT_DEST_DIR" && build_package "deb"
cd "$SCRIPT_DEST_DIR" && build_package 'deb'
# Cleaning up:
clean_up_files && cd "${SECRET_PROJECT_ROOT}"

@ -23,13 +23,13 @@ function integration_tests {
apt-get -f -y install
# Testing the installation:
dpkg --get-selections | grep "git-secret"
dpkg --get-selections | grep 'git-secret'
# lint says to use 'command -v' and not 'which'
command -v "git-secret"
command -v 'git-secret'
# Test the manuals:
man --where "git-secret" # .7
man --where "git-secret-init" # .1
man --where 'git-secret' # .7
man --where 'git-secret-init' # .1
}
integration_tests

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -e
BRANCH_NAME=$(git branch | grep '\*' | sed 's/* //')
if [[ "$BRANCH_NAME" == 'master' ]]; then
# Compare script version and the latest tag:
NEWEST_TAG=$(git describe --abbrev=0 --tags)
SCRIPT_VERSION=$(bash "${PWD}/git-secret" --version)
if [[ "$NEWEST_TAG" != "v${SCRIPT_VERSION}" ]]; then
# Create new release:
git tag -a "v${SCRIPT_VERSION}" -m "version $SCRIPT_VERSION"
echo "Created new tag 'v${SCRIPT_VERSION}'"
fi
fi

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -e
BRANCH_NAME=$(git branch | grep '\*' | sed 's/* //')
if [[ "$BRANCH_NAME" != '(no branch)' ]]; then
unset GIT_WORK_TREE
# Set marker, that we running tests from `git commit`,
# so some tests will be skipped. It is done, because `git rev-parse`
# is not working when running from pre-commit hook. See #334
export BATS_RUNNING_FROM_GIT=1
if [[ "$(uname -s)" == MINGW* ]]; then
export GITSECRET_DIST="windows"
fi
# Run tests:
make test
if [[ "$BRANCH_NAME" == "master" ]]; then
# Build new manuals:
make build-man
# Add new files:
git add man/man1/*
git add man/man7/*
fi
fi

@ -9,6 +9,7 @@ function resolve_link {
$(type -p greadlink readlink | head -1) "$1"
}
function abs_dirname {
local cwd
local path="$1"
@ -26,6 +27,7 @@ function abs_dirname {
cd "$cwd"
}
PREFIX="$1"
if [ -z "$PREFIX" ]; then
echo "usage: $0 <prefix>" >&2

@ -19,11 +19,11 @@ function integration_tests {
# Testing the installation:
# 'command -v' is like 'which'
command -v "git-secret"
command -v 'git-secret'
# Test the manuals:
man --where "git-secret" # .7
man --where "git-secret-init" # .1
man --where 'git-secret' # .7
man --where 'git-secret-init' # .1
}
integration_tests

@ -6,10 +6,10 @@ set -e
source "${SECRET_PROJECT_ROOT}/utils/build-utils.sh"
# Copying all the required files to the build directory:
preinstall_files "-T"
preinstall_files '-T'
# Building .rpm package:
cd "$SCRIPT_DEST_DIR" && build_package "rpm"
cd "$SCRIPT_DEST_DIR" && build_package 'rpm'
# Cleaning up:
clean_up_files && cd "${SECRET_PROJECT_ROOT}"

@ -23,13 +23,13 @@ function integration_tests {
dnf install -y "$RPM_FILE_LOCATION"
# Testing the installation:
dnf info "git-secret"
dnf info 'git-secret'
# 'command -v' is like 'which'
command -v "git-secret"
command -v 'git-secret'
# Test the manuals:
man --where "git-secret" # .7
man --where "git-secret-init" # .1
man --where 'git-secret' # .7
man --where 'git-secret-init' # .1
}
integration_tests

@ -6,25 +6,26 @@ set -e
TEST_DIR=/tmp/git-secret-test
rm -rf "${TEST_DIR}"
rm -rf "${TEST_DIR}"
mkdir "${TEST_DIR}"
echo "# created dir: ${TEST_DIR}"
chmod 0700 "${TEST_DIR}"
(
cd "${TEST_DIR}"
cd "$TEST_DIR"
# test with non-standard SECRETS_DIR (normally .gitsecret) and SECRETS_EXTENSION (normally .secret)
export SECRETS_DIR=.gitsec
export SECRETS_EXTENSION=.sec
# test with non-standard SECRETS_DIR (normally .gitsecret)
# and SECRETS_EXTENSION (normally .secret)
export SECRETS_DIR='.gitsec'
export SECRETS_EXTENSION='.sec'
export TMPDIR="${TEST_DIR}"
echo "# TMPDIR is $TMPDIR"
export TMPDIR="$TEST_DIR"
echo "# TMPDIR is $TMPDIR"
# bats expects diagnostic lines to be sent to fd 3, matching regex '^# '
# bats expects diagnostic lines to be sent to fd 3, matching regex '^# '
# (IE, like: `echo '# message here' >&3`).
# bats ... 3>&1 shows diagnostic output
bats "${SECRET_PROJECT_ROOT}/tests/" 3>&1
bats "${SECRET_PROJECT_ROOT}/tests" 3>&1
)
rm -rf "${TEST_DIR}"

@ -13,5 +13,5 @@ fi
rm -f "$PREFIX"/bin/git-secret
# Manuals:
find "$PREFIX"/share/man/man1 -type f -name "git-secret-*.1" -exec rm -f {} \;
find "$PREFIX"/share/man/man1 -type f -name 'git-secret-*.1' -exec rm -f {} \;
rm -f "$PREFIX"/share/man/man7/git-secret.7

10
vendor/README.md vendored

@ -1,9 +1,15 @@
README for git-secret/vendor directory
We import bats-core v1.1.0 here for
We import bats-core v1.3.0 here for
https://github.com/sobolevn/git-secret/issues/377,
"Don't depend on network during builds (re: bats-core)"
If you want upgrade bats-core, replace the files in vendor/bats-core.
They must remain exactly as distributed by the chosen release of bats-core
They must remain exactly as distributed by the chosen release of bats-core
- see issue linked above for details.
## Changes:
- 2021-05-03: Version update from `bats-core@1.1.0` to `bats-core@1.3.0`
- Initial import of `bats-core@1.1.0`

@ -1,16 +0,0 @@
version: 'v1.1.0.{build}'
build: off
# This presumes that Git bash is installed at `C:\Program Files\Git` and the
# bash we're using is `C:\Program Files\Git\bin\bash.exe`.
#
# If instead it finds the Windows Subsystem for Linux bash at
# `C:\Windows\System32\bash.exe`, it will fail with an error like:
# /mnt/c/.../bats-core/test/test_helper.bash: line 1:
# syntax error near unexpected token `$'{\r''
test_script:
- where bash
- bash --version
- bash -c 'export'
- bash -c 'time PATH="/usr/bin:${PATH}" bin/bats test'

@ -0,0 +1,15 @@
ARG bashver=latest
FROM bash:${bashver}
# Install parallel and accept the citation notice (we aren't using this in a
# context where it make sense to cite GNU Parallel).
RUN echo "@edgecomm http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
apk update && \
apk add --no-cache parallel ncurses shellcheck@edgecomm && \
mkdir -p ~/.parallel && touch ~/.parallel/will-cite
RUN ln -s /opt/bats/bin/bats /usr/sbin/bats
COPY . /opt/bats/
ENTRYPOINT ["bash", "/usr/sbin/bats"]

@ -0,0 +1,5 @@
{
"name": "Bats core development environment",
"dockerFile": "Dockerfile",
"build": {"args": {"bashver": "4.3"}}
}

@ -0,0 +1,32 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
# The JSON files contain newlines inconsistently
[*.json]
indent_size = 2
insert_final_newline = ignore
# YAML
[*.{yml,yaml}]
indent_style = space
indent_size = 2
# Makefiles always use tabs for recipe indentation
[{Makefile,*.mak}]
indent_style = tab
# Markdown
[*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}]
max_line_length = 80
# tabs behave as if they were replaced by spaces with a tab stop of 4 characters
tab_width = 4
# trailing spaces indicates word wrap
trim_trailing_spaces = false
trim_trailing_whitespace = false

@ -0,0 +1,65 @@
name: Release
on:
release: { types: [published] }
workflow_dispatch:
inputs:
version:
description: 'Version to simulate for deploy'
required: true
jobs:
version-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
EXPECTED_VERSION=${{ github.event.inputs.version }}
EXPECTED_VERSION=${EXPECTED_VERSION:-${GITHUB_REF/refs\/tags\//}}
echo "EXPECTED_VERSION=$EXPECTED_VERSION" >> $GITHUB_ENV
- name: Check tag version matches artifact versions
run: |
echo "Expected version: $EXPECTED_VERSION"
# use double negation to see the result unless we get a match
(./bin/bats --version | grep -F "$EXPECTED_VERSION") || (echo "Bats version check failed: "; ./bin/bats --version; exit -1)
(npm view . version | grep -F "$EXPECTED_VERSION") || (echo "npm version check failed: "; npm view . version; exit -1)
(grep '^Version:' 'contrib/rpm/bats.spec' | grep -F "$EXPECTED_VERSION") || (echo "debian package version check failed: "; grep '^Version:' 'contrib/rpm/bats.spec'; exit -1)
npmjs:
runs-on: ubuntu-latest
needs: version-check
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
registry-url: "https://registry.npmjs.org"
- run: npm publish --ignore-scripts
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
github-npm:
runs-on: ubuntu-latest
needs: version-check
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
registry-url: "https://npm.pkg.github.com"
- name: scope package name as required by GHPR
run: npm init -y --scope ${{ github.repository_owner }}
- run: npm publish --ignore-scripts
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dockerhub:
runs-on: ubuntu-latest
needs: version-check
steps:
- uses: actions/checkout@v2
- uses: docker/build-push-action@v1
with:
file: ./Dockerfile
platforms: linux/amd64
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tags: bats/bats:${GITHUB_REF/refs\/tags\//}

@ -0,0 +1,91 @@
name: Tests
# Controls when the action will run.
on: [push, pull_request, workflow_dispatch]
jobs:
shellcheck:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Run shellcheck
run: |
sudo apt-get update -y
sudo apt-get install shellcheck
./shellcheck.sh
linux:
strategy:
matrix:
os: ['ubuntu-20.04', 'ubuntu-18.04', 'ubuntu-16.04']
env_vars:
- ''
# allow for some parallelity without GNU parallel, since it is not installed by default
- 'BATS_NO_PARALLELIZE_ACROSS_FILES=1 BATS_NUMBER_OF_PARALLEL_JOBS=2'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Run test on OS ${{ matrix.os }}
shell: 'script -q -e -c "bash {0}"' # work around tty issues
env:
TERM: linux # fix tput for tty issue work around
run: |
bash --version
bash -c "time ${{ matrix.env_vars }} bin/bats --formatter tap test"
windows:
strategy:
matrix:
os: ['windows-2019']
env_vars:
- ''
# allow for some parallelity without GNU parallel, since it is not installed by default
- 'BATS_NO_PARALLELIZE_ACROSS_FILES=1 BATS_NUMBER_OF_PARALLEL_JOBS=2'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Run test on OS ${{ matrix.os }}
run: |
bash --version
bash -c "time ${{ matrix.env_vars }} bin/bats --formatter tap test"
macos:
strategy:
matrix:
os: ['macos-10.15']
env_vars:
- ''
# allow for some parallelity without GNU parallel, since it is not installed by default
- 'BATS_NO_PARALLELIZE_ACROSS_FILES=1 BATS_NUMBER_OF_PARALLEL_JOBS=2'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Install unbuffer via expect
run: brew install expect
- name: Run test on OS ${{ matrix.os }}
shell: 'unbuffer bash {0}' # work around tty issues
env:
TERM: linux # fix tput for tty issue work around
run: |
bash --version
bash -c "time ${{ matrix.env_vars }} bin/bats --formatter tap test"
bash-version:
strategy:
matrix:
version: ['3.2', '4.0', '4.1', '4.2', '4.3', '4.4', '4', '5.0', '5.1', '5', 'latest']
env_vars:
- ''
# also test running (recursively!) in parallel
- '-e BATS_NUMBER_OF_PARALLEL_JOBS=2'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run test on bash version ${{ matrix.version }}
shell: 'script -q -e -c "bash {0}"' # work around tty issues
run: |
set -e
docker build --build-arg bashver="${{ matrix.version }}" --tag "bats/bats:bash-${{ matrix.version }}" .
docker run -it "bash:${{ matrix.version }}" --version
time docker run -it ${{ matrix.env_vars }} "bats/bats:bash-${{ matrix.version }}" --tap /opt/bats/test

@ -0,0 +1,7 @@
/docker-compose.override.yml
/docs/build
# npm
/bats-*.tgz
# we don't have any deps; un-ignore if that changes
/package-lock.json

@ -0,0 +1,9 @@
version: 2
sphinx:
configuration: docs/source/conf.py
python:
version: 3.7
install:
- requirements: docs/source/requirements.txt

@ -1,34 +0,0 @@
language: bash
os:
- linux
env:
- BASHVER=
- BASHVER=3.2
- BASHVER=4.0
- BASHVER=4.1
- BASHVER=4.2
- BASHVER=4.3
- BASHVER=4.4
matrix:
include:
- os: osx
services:
- docker
script:
- |
if [[ "$TRAVIS_OS_NAME" == 'linux' && -n "$BASHVER" ]]; then
docker build --build-arg bashver=${BASHVER} --tag bats/bats:bash-${BASHVER} .
docker run -it bash:${BASHVER} --version
time docker run -it bats/bats:bash-${BASHVER} --tap /opt/bats/test
else
time bin/bats --tap test
fi
notifications:
email:
on_success: never

@ -2,8 +2,14 @@ ARG bashver=latest
FROM bash:${bashver}
RUN ln -s /opt/bats/bin/bats /usr/sbin/bats
# Install parallel and accept the citation notice (we aren't using this in a
# context where it make sense to cite GNU Parallel).
RUN apk add --no-cache parallel ncurses && \
mkdir -p ~/.parallel && touch ~/.parallel/will-cite
RUN ln -s /opt/bats/bin/bats /usr/local/bin/bats
COPY . /opt/bats/
ENTRYPOINT ["bash", "/usr/sbin/bats"]
WORKDIR /code/
ENTRYPOINT ["bash", "bats"]

@ -3,12 +3,12 @@
[![Latest release](https://img.shields.io/github/release/bats-core/bats-core.svg)](https://github.com/bats-core/bats-core/releases/latest)
[![npm package](https://img.shields.io/npm/v/bats.svg)](https://www.npmjs.com/package/bats)
[![License](https://img.shields.io/github/license/bats-core/bats-core.svg)](https://github.com/bats-core/bats-core/blob/master/LICENSE.md)
[![Continuous integration status for Linux and macOS](https://img.shields.io/travis/bats-core/bats-core/master.svg?label=travis%20build)](https://travis-ci.org/bats-core/bats-core)
[![Continuous integration status for Windows](https://img.shields.io/appveyor/ci/bats-core/bats-core/master.svg?label=appveyor%20build)](https://ci.appveyor.com/project/bats-core/bats-core)
[[![Continuous integration status](https://github.com/bats-core/bats-core/workflows/Tests/badge.svg)](https://github.com/bats-core/bats-core/actions?query=workflow%3ATests)](https://github.com/bats-core/bats-core/actions?query=workflow%3ATests)
[![Read the docs status](https://readthedocs.org/projects/bats-core/badge/?version=latest&style=plastic)](https://bats-core.readthedocs.io)
[![Join the chat in bats-core/bats-core on gitter](https://badges.gitter.im/bats-core/bats-core.svg)][gitter]
Bats is a [TAP][]-compliant testing framework for Bash. It provides a simple
Bats is a [TAP](https://testanything.org/)-compliant testing framework for Bash. It provides a simple
way to verify that the UNIX programs you write behave as expected.
[TAP]: https://testanything.org
@ -38,385 +38,32 @@ Test cases consist of standard shell commands. Bats makes use of Bash's
test case exits with a `0` status code (success), the test passes. In this way,
each line is an assertion of truth.
**Tuesday, September 19, 2017:** This is a mirrored fork of [Bats][bats-orig] at
commit [0360811][]. It was created via `git clone --bare` and `git push
--mirror`. See the [Background](#background) section below for more information.
## Table of contents
[bats-orig]: https://github.com/sstephenson/bats
[0360811]: https://github.com/sstephenson/bats/commit/03608115df2071fff4eaaff1605768c275e5f81f
**NOTE** The documentation has moved to <https://bats-core.readthedocs.io>
## Table of contents
<!-- toc -->
- [Installation](#installation)
- [Supported Bash versions](#supported-bash-versions)
- [Homebrew](#homebrew)
- [npm](#npm)
- [Installing Bats from source](#installing-bats-from-source)
- [Running Bats in Docker](#running-bats-in-docker)
- [Building a Docker image](#building-a-docker-image)
- [Usage](#usage)
- [Writing tests](#writing-tests)
- [`run`: Test other commands](#run-test-other-commands)
- [`load`: Share common code](#load-share-common-code)
- [`skip`: Easily skip tests](#skip-easily-skip-tests)
- [`setup` and `teardown`: Pre- and post-test hooks](#setup-and-teardown-pre--and-post-test-hooks)
- [Code outside of test cases](#code-outside-of-test-cases)
- [File descriptor 3 (read this if Bats hangs)](#file-descriptor-3-read-this-if-bats-hangs)
- [Printing to the terminal](#printing-to-the-terminal)
- [Special variables](#special-variables)
- [Testing](#testing)
- [Support](#support)
- [Contributing](#contributing)
- [Contact](#contact)
- [Version history](#version-history)
- [Background](#background)
- [Why was this fork created?](#why-was-this-fork-created)
- [What's the plan and why?](#whats-the-plan-and-why)
- [Contact us](#contact-us)
- [Why was this fork created?](#why-was-this-fork-created)
- [Copyright](#copyright)
## Installation
### Supported Bash versions
The following is a list of Bash versions that are currently supported by Bats.
This list is composed of platforms that Bats has been tested on and is known to
work on without issues.
- Bash versions:
- Everything from `3.2.57(1)` and higher (macOS's highest version)
- Operating systems:
- Arch Linux
- Alpine Linux
- Ubuntu Linux
- FreeBSD `10.x` and `11.x`
- macOS
- Windows 10
- Latest version for the following Windows platforms:
- Git for Windows Bash (MSYS2 based)
- Windows Subsystem for Linux
- MSYS2
- Cygwin
### Homebrew
On macOS, you can install [Homebrew](https://brew.sh/) if you haven't already,
then run:
```bash
$ brew install bats-core
```
### npm
You can install the [Bats npm package](https://www.npmjs.com/package/bats) via:
```
# To install globally:
$ npm install -g bats
# To install into your project and save it as one of the "devDependencies" in
# your package.json:
$ npm install --save-dev bats
```
### Installing Bats from source
Check out a copy of the Bats repository. Then, either add the Bats `bin`
directory to your `$PATH`, or run the provided `install.sh` command with the
location to the prefix in which you want to install Bats. For example, to
install Bats into `/usr/local`,
$ git clone https://github.com/bats-core/bats-core.git
$ cd bats-core
$ ./install.sh /usr/local
Note that you may need to run `install.sh` with `sudo` if you do not have
permission to write to the installation prefix.
### Running Bats in Docker
There is an official image on the Docker Hub:
$ docker run -it bats/bats:latest --version
#### Building a Docker image
Check out a copy of the Bats repository, then build a container image:
$ git clone https://github.com/bats-core/bats-core.git
$ cd bats-core
$ docker build --tag bats/bats:latest .
This creates a local Docker image called `bats/bats:latest` based on [Alpine
Linux](https://github.com/gliderlabs/docker-alpine/blob/master/docs/usage.md)
(to push to private registries, tag it with another organisation, e.g.
`my-org/bats:latest`).
To run Bats' internal test suite (which is in the container image at
`/opt/bats/test`):
$ docker run -it bats/bats:latest /opt/bats/test
To run a test suite from your local machine, mount in a volume and direct Bats
to its path inside the container:
$ docker run -it -v "$(pwd):/code" bats/bats:latest /code/test
This is a minimal Docker image. If more tools are required this can be used as a
base image in a Dockerfile using `FROM <Docker image>`. In the future there may
be images based on Debian, and/or with more tools installed (`curl` and `openssl`,
for example). If you require a specific configuration please search and +1 an
issue or [raise a new issue](https://github.com/bats-core/bats-core/issues).
Further usage examples are in [the wiki](https://github.com/bats-core/bats-core/wiki/Docker-Usage-Examples).
## Usage
Bats comes with two manual pages. After installation you can view them with `man
1 bats` (usage manual) and `man 7 bats` (writing test files manual). Also, you
can view the available command line options that Bats supports by calling Bats
with the `-h` or `--help` options. These are the options that Bats currently
supports:
```
Bats x.y.z
Usage: bats [-c] [-r] [-p | -t] <test> [<test> ...]
<test> is the path to a Bats test file, or the path to a directory
containing Bats test files.
-c, --count Count the number of test cases without running any tests
-h, --help Display this help message
-p, --pretty Show results in pretty format (default for terminals)
-r, --recursive Include tests in subdirectories
-t, --tap Show results in TAP format
-v, --version Display the version number
```
To run your tests, invoke the `bats` interpreter with one or more paths to test
files ending with the `.bats` extension, or paths to directories containing test
files. (`bats` will not only discover `.bats` files at the top level of each
directory; it will not recurse.)
Test cases from each file are run sequentially and in isolation. If all the test
cases pass, `bats` exits with a `0` status code. If there are any failures,
`bats` exits with a `1` status code.
When you run Bats from a terminal, you'll see output as each test is performed,
with a check-mark next to the test's name if it passes or an "X" if it fails.
$ bats addition.bats
✓ addition using bc
✓ addition using dc
2 tests, 0 failures
If Bats is not connected to a terminal—in other words, if you run it from a
continuous integration system, or redirect its output to a file—the results are
displayed in human-readable, machine-parsable [TAP format][TAP].
<!-- tocstop -->
You can force TAP output from a terminal by invoking Bats with the `--tap`
option.
## Testing
$ bats --tap addition.bats
1..2
ok 1 addition using bc
ok 2 addition using dc
## Writing tests
Each Bats test file is evaluated _n+1_ times, where _n_ is the number of
test cases in the file. The first run counts the number of test cases,
then iterates over the test cases and executes each one in its own
process.
For more details about how Bats evaluates test files, see [Bats Evaluation
Process][bats-eval] on the wiki.
[bats-eval]: https://github.com/bats-core/bats-core/wiki/Bats-Evaluation-Process
### `run`: Test other commands
Many Bats tests need to run a command and then make assertions about its exit
status and output. Bats includes a `run` helper that invokes its arguments as a
command, saves the exit status and output into special global variables, and
then returns with a `0` status code so you can continue to make assertions in
your test case.
For example, let's say you're testing that the `foo` command, when passed a
nonexistent filename, exits with a `1` status code and prints an error message.
```bash
@test "invoking foo with a nonexistent file prints an error" {
run foo nonexistent_filename
[ "$status" -eq 1 ]
[ "$output" = "foo: no such file 'nonexistent_filename'" ]
}
```
The `$status` variable contains the status code of the command, and the
`$output` variable contains the combined contents of the command's standard
output and standard error streams.
A third special variable, the `$lines` array, is available for easily accessing
individual lines of output. For example, if you want to test that invoking `foo`
without any arguments prints usage information on the first line:
```bash
@test "invoking foo without arguments prints usage" {
run foo
[ "$status" -eq 1 ]
[ "${lines[0]}" = "usage: foo <filename>" ]
}
```sh
bin/bats --tap test
```
### `load`: Share common code
You may want to share common code across multiple test files. Bats includes a
convenient `load` command for sourcing a Bash source file relative to the
location of the current test file. For example, if you have a Bats test in
`test/foo.bats`, the command
```bash
load test_helper
```
will source the script `test/test_helper.bash` in your test file. This can be
useful for sharing functions to set up your environment or load fixtures.
### `skip`: Easily skip tests
Tests can be skipped by using the `skip` command at the point in a test you wish
to skip.
```bash
@test "A test I don't want to execute for now" {
skip
run foo
[ "$status" -eq 0 ]
}
```
Optionally, you may include a reason for skipping:
```bash
@test "A test I don't want to execute for now" {
skip "This command will return zero soon, but not now"
run foo
[ "$status" -eq 0 ]
}
```
Or you can skip conditionally:
```bash
@test "A test which should run" {
if [ foo != bar ]; then
skip "foo isn't bar"
fi
run foo
[ "$status" -eq 0 ]
}
```
### `setup` and `teardown`: Pre- and post-test hooks
You can define special `setup` and `teardown` functions, which run before and
after each test case, respectively. Use these to load fixtures, set up your
environment, and clean up when you're done.
### Code outside of test cases
You can include code in your test file outside of `@test` functions. For
example, this may be useful if you want to check for dependencies and fail
immediately if they're not present. However, any output that you print in code
outside of `@test`, `setup` or `teardown` functions must be redirected to
`stderr` (`>&2`). Otherwise, the output may cause Bats to fail by polluting the
TAP stream on `stdout`.
### File descriptor 3 (read this if Bats hangs)
Bats makes a separation between output from the code under test and output that
forms the TAP stream (which is produced by Bats internals). This is done in
order to produce TAP-compliant output. In the [Printing to the
terminal](#printing-to-the-terminal) section, there are details on how to use
file descriptor 3 to print custom text properly.
A side effect of using file descriptor 3 is that, under some circumstances, it
can cause Bats to block and execution to seem dead without reason. This can
happen if a child process is spawned in the background from a test. In this
case, the child process will inherit file descriptor 3. Bats, as the parent
process, will wait for the file descriptor to be closed by the child process
before continuing execution. If the child process takes a lot of time to
complete (eg if the child process is a `sleep 100` command or a background
service that will run indefinitely), Bats will be similarly blocked for the same
amount of time.
**To prevent this from happening, close FD 3 explicitly when running any command
that may launch long-running child processes**, e.g. `command_name 3>- &`.
### Printing to the terminal
Bats produces output compliant with [version 12 of the TAP protocol][TAP]. The
produced TAP stream is by default piped to a pretty formatter for human
consumption, but if Bats is called with the `-t` flag, then the TAP stream is
directly printed to the console.
This has implications if you try to print custom text to the terminal. As
mentioned in [File descriptor 3](#file-descriptor-3), bats provides a special
file descriptor, `&3`, that you should use to print your custom text. Here are
some detailed guidelines to refer to:
- Printing **from within a test function**:
- To have text printed from within a test function you need to redirect the
output to file descriptor 3, eg `echo 'text' >&3`. This output will become
part of the TAP stream. You are encouraged to prepend text printed this way
with a hash (eg `echo '# text' >&3`) in order to produce 100% TAP compliant
output. Otherwise, depending on the 3rd-party tools you use to analyze the
TAP stream, you can encounter unexpected behavior or errors.
- The pretty formatter that Bats uses by default to process the TAP stream
will filter out and not print text output to file descriptor 3.
- Text that is output directly to stdout or stderr (file descriptor 1 or 2),
ie `echo 'text'` is considered part of the test function output and is
printed only on test failures for diagnostic purposes, regardless of the
formatter used (TAP or pretty).
- Printing **from within the `setup` or `teardown` functions**: The same hold
true as for printing with test functions.
- Printing **outside test or `setup`/`teardown` functions**:
- Regardless of where text is redirected to (stdout, stderr or file descriptor
3) text is immediately visible in the terminal.
- Text printed in such a way, will disable pretty formatting. Also, it will
make output non-compliant with the TAP spec. The reason for this is that
each test file is evaluated n+1 times (as metioned
[earlier](#writing-tests)). The first run will cause such output to be
produced before the [_plan line_][tap-plan] is printed, contrary to the spec
that requires the _plan line_ to be either the first or the last line of the
output.
- Due to internal pipes/redirects, output to stderr is always printed first.
[tap-plan]: https://testanything.org/tap-specification.html#the-plan
### Special variables
There are several global variables you can use to introspect on Bats tests:
* `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test file.
* `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is located.
* `$BATS_TEST_NAMES` is an array of function names for each test case.
* `$BATS_TEST_NAME` is the name of the function containing the current test
case.
* `$BATS_TEST_DESCRIPTION` is the description of the current test case.
* `$BATS_TEST_NUMBER` is the (1-based) index of the current test case in the
test file.
* `$BATS_TMPDIR` is the location to a directory that may be used to store
temporary files.
See also the [CI](./.github/workflows/tests.yml) settings for the current test environment and
scripts.
## Support
@ -432,173 +79,52 @@ To learn how to set up your editor for Bats syntax highlighting, see [Syntax
Highlighting](https://github.com/bats-core/bats-core/wiki/Syntax-Highlighting)
on the wiki.
## Version history
## Contributing
Bats is [SemVer compliant](https://semver.org/).
*1.1.0* (July 8, 2018)
This is the first release with new features relative to the original Bats 0.4.0.
Added:
* The `-r, --recursive` flag to scan directory arguments recursively for
`*.bats` files (#109)
* The `contrib/rpm/bats.spec` file to build RPMs (#111)
Changed:
* Travis exercises latest versions of Bash from 3.2 through 4.4 (#116, #117)
* Error output highlights invalid command line options (#45, #46, #118)
* Replaced `echo` with `printf` (#120)
Fixed:
* Fixed `BATS_ERROR_STATUS` getting lost when `bats_error_trap` fired multiple
times under Bash 4.2.x (#110)
* Updated `bin/bats` symlink resolution, handling the case on CentOS where
`/bin` is a symlink to `/usr/bin` (#113, #115)
*1.0.2* (June 18, 2018)
* Fixed sstephenson/bats#240, whereby `skip` messages containing parentheses
were truncated (#48)
* Doc improvements:
* Docker usage (#94)
* Better README badges (#101)
* Better installation instructions (#102, #104)
* Packaging/installation improvements:
* package.json update (#100)
* Moved `libexec/` files to `libexec/bats-core/`, improved `install.sh` (#105)
*1.0.1* (June 9, 2018)
* Fixed a `BATS_CWD` bug introduced in #91 whereby it was set to the parent of
`PWD`, when it should've been set to `PWD` itself (#98). This caused file
names in stack traces to contain the basename of `PWD` as a prefix, when the
names should've been purely relative to `PWD`.
* Ensure the last line of test output prints when it doesn't end with a newline
(#99). This was a quasi-bug introduced by replacing `sed` with `while` in #88.
*1.0.0* (June 8, 2018)
`1.0.0` generally preserves compatibility with `0.4.0`, but with some Bash
compatibility improvements and a massive performance boost. In other words:
- all existing tests should remain compatible
- tests that might've failed or exhibited unexpected behavior on earlier
versions of Bash should now also pass or behave as expected
Changes:
* Added support for Docker.
* Added support for test scripts that have the [unofficial strict
mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) enabled.
* Improved stability on Windows and macOS platforms.
* Massive performance improvements, especially on Windows (#8)
* Workarounds for inconsistent behavior between Bash versions (#82)
* Workaround for preserving stack info after calling an exported function under
Bash < 4.4 (#87)
* Fixed TAP compliance for skipped tests
* Added support for tabs in test names.
* `bin/bats` and `install.sh` now work reliably on Windows (#91)
*0.4.0* (August 13, 2014)
* Improved the display of failing test cases. Bats now shows the source code of
failing test lines, along with full stack traces including function names,
filenames, and line numbers.
* Improved the display of the pretty-printed test summary line to include the
number of skipped tests, if any.
* Improved the speed of the preprocessor, dramatically shortening test and suite
startup times.
* Added support for absolute pathnames to the `load` helper.
* Added support for single-line `@test` definitions.
* Added bats(1) and bats(7) manual pages.
* Modified the `bats` command to default to TAP output when the `$CI` variable
is set, to better support environments such as Travis CI.
*0.3.1* (October 28, 2013)
* Fixed an incompatibility with the pretty formatter in certain environments
such as tmux.
* Fixed a bug where the pretty formatter would crash if the first line of a test
file's output was invalid TAP.
*0.3.0* (October 21, 2013)
* Improved formatting for tests run from a terminal. Failing tests are now
colored in red, and the total number of failing tests is displayed at the end
of the test run. When Bats is not connected to a terminal (e.g. in CI runs),
or when invoked with the `--tap` flag, output is displayed in standard TAP
format.
* Added the ability to skip tests using the `skip` command.
* Added a message to failing test case output indicating the file and line
number of the statement that caused the test to fail.
* Added "ad-hoc" test suite support. You can now invoke `bats` with multiple
filename or directory arguments to run all the specified tests in aggregate.
* Added support for test files with Windows line endings.
* Fixed regular expression warnings from certain versions of Bash.
* Fixed a bug running tests containing lines that begin with `-e`.
*0.2.0* (November 16, 2012)
* Added test suite support. The `bats` command accepts a directory name
containing multiple test files to be run in aggregate.
* Added the ability to count the number of test cases in a file or suite by
passing the `-c` flag to `bats`.
* Preprocessed sources are cached between test case runs in the same file for
better performance.
*0.1.0* (December 30, 2011)
* Initial public release.
---
For now see the [`docs`](docs) folder for project guides, work with us on the wiki
or look at the other communication channels.
## Background
## Contact
### Why was this fork created?
- We are `#bats` on freenode;
- Or leave a message on [gitter].
## Version history
See `docs/CHANGELOG.md`.
The original Bats repository needed new maintainers, and has not been actively
maintained since 2013. While there were volunteers for maintainers, attempts to
organize issues, and outstanding PRs, the lack of write-access to the repo
hindered progress severely.
## Background
<!-- markdownlint-disable MD026 -->
### What's the plan and why?
<!-- markdownlint-enable MD026 -->
The rough plan, originally [outlined
here](https://github.com/sstephenson/bats/issues/150#issuecomment-323845404) is
to create a new, mirrored mainline (this repo!). An excerpt:
**Tuesday, September 19, 2017:** This was forked from [Bats][bats-orig] at
commit [0360811][]. It was created via `git clone --bare` and `git push
--mirror`.
> **1. Roadmap 1.0:**
> There are already existing high-quality PRs, and often-requested features and
> issues, especially here at
> [#196](https://github.com/sstephenson/bats/issues/196). Leverage these and
> **consolidate into a single roadmap**.
>
> **2. Create or choose a fork or *mirror* of this repo to use as the new
> mainline:**
> Repoint existing PRs (whichever ones are possible) to the new mainline, get
> that repo to a stable 1.0. IMO we should create an organization and grant 2-3
> people admin and write access.
[bats-orig]: https://github.com/sstephenson/bats
[0360811]: https://github.com/sstephenson/bats/commit/03608115df2071fff4eaaff1605768c275e5f81f
Doing it this way accomplishes a number of things:
This [bats-core repo](https://github.com/bats-core/bats-core) is the community-maintained Bats project.
1. Removes the dependency on the original maintainer
1. Enables collaboration and contribution flow again
1. Allows the possibility of merging back to original, or merging from original
if or when the need arises
1. Prevents lock-out by giving administrative access to more than one person,
increases transferability
<!-- markdownlint-disable MD026 -->
### Why was this fork created?
<!-- markdownlint-enable MD026 -->
### Contact us
There was an initial [call for maintainers][call-maintain] for the original Bats repository, but write access to it could not be obtained. With development activity stalled, this fork allowed ongoing maintenance and forward progress for Bats.
- We are `#bats` on freenode
[call-maintain]: https://github.com/sstephenson/bats/issues/150
## Copyright
© 2018 bats-core organization
© 2017-2020 bats-core organization
© 2014 Sam Stephenson
© 2011-2016 Sam Stephenson
Bats is released under an MIT-style license; see `LICENSE.md` for details.
See the [parent project](https://github.com/bats-core) at GitHub or the
[AUTHORS](AUTHORS) file for the current project maintainer team.
[gitter]: https://gitter.im/bats-core/bats-core?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

@ -1,50 +1,59 @@
#!/usr/bin/env bash
set -e
export BATS_READLINK='true'
if command -v 'greadlink' >/dev/null; then
BATS_READLINK='greadlink'
elif command -v 'readlink' >/dev/null; then
BATS_READLINK='readlink'
set -euo pipefail
if command -v greadlink >/dev/null; then
bats_readlinkf() {
greadlink -f "$1"
}
else
bats_readlinkf() {
readlink -f "$1"
}
fi
bats_resolve_link() {
if ! "$BATS_READLINK" "$1"; then
return 0
fi
fallback_to_readlinkf_posix() {
bats_readlinkf() {
[ "${1:-}" ] || return 1
max_symlinks=40
CDPATH='' # to avoid changing to an unexpected directory
target=$1
[ -e "${target%/}" ] || target=${1%"${1##*[!/]}"} # trim trailing slashes
[ -d "${target:-/}" ] && target="$target/"
cd -P . 2>/dev/null || return 1
while [ "$max_symlinks" -ge 0 ] && max_symlinks=$((max_symlinks - 1)); do
if [ ! "$target" = "${target%/*}" ]; then
case $target in
/*) cd -P "${target%/*}/" 2>/dev/null || break ;;
*) cd -P "./${target%/*}" 2>/dev/null || break ;;
esac
target=${target##*/}
fi
if [ ! -L "$target" ]; then
target="${PWD%/}${target:+/}${target}"
printf '%s\n' "${target:-/}"
return 0
fi
# `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n",
# <file mode>, <number of links>, <owner name>, <group name>,
# <size>, <date and time>, <pathname of link>, <contents of link>
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html
link=$(ls -dl -- "$target" 2>/dev/null) || break
target=${link#*" $target -> "}
done
return 1
}
}
bats_resolve_absolute_root_dir() {
local cwd="$PWD"
local path="$1"
local result="$2"
local target_dir
local target_name
local original_shell_options="$-"
# Resolve the parent directory, e.g. /bin => /usr/bin on CentOS (#113).
set -P
while true; do
target_dir="${path%/*}"
target_name="${path##*/}"
if [[ "$target_dir" != "$path" ]]; then
cd "$target_dir"
fi
if [[ -L "$target_name" ]]; then
path="$(bats_resolve_link "$target_name")"
else
printf -v "$result" -- '%s' "${PWD%/*}"
set +P "-$original_shell_options"
cd "$cwd"
return
fi
done
}
if ! BATS_PATH=$(bats_readlinkf "${BASH_SOURCE[0]}" 2>/dev/null); then
fallback_to_readlinkf_posix
BATS_PATH=$(bats_readlinkf "${BASH_SOURCE[0]}")
fi
export BATS_ROOT
bats_resolve_absolute_root_dir "$0" 'BATS_ROOT'
exec "$BATS_ROOT/libexec/bats-core/bats" "$@"
export BATS_ROOT=${BATS_PATH%/*/*}
export -f bats_readlinkf
exec env BATS_ROOT="$BATS_ROOT" "$BATS_ROOT/libexec/bats-core/bats" "$@"

@ -0,0 +1,177 @@
#!/usr/bin/env bash
#
# bats-core git releaser
#
## Usage: %SCRIPT_NAME% [options]
##
## Options:
## --major Major version bump
## --minor Minor version bump
## --patch Patch version bump
##
## -v, --version Print version
## --debug Enable debug mode
## -h, --help Display this message
##
set -Eeuo pipefail
DIR=$(cd "$(dirname "${0}")" && pwd)
THIS_SCRIPT="${DIR}/$(basename "${0}")"
BATS_VERSION=$(
# shellcheck disable=SC1090
source <(grep '^export BATS_VERSION=' libexec/bats-core/bats)
echo "${BATS_VERSION}"
)
declare -r DIR
declare -r THIS_SCRIPT
declare -r BATS_VERSION
BUMP_INTERVAL=""
NEW_BATS_VERSION=""
main() {
handle_arguments "${@}"
if [[ "${BUMP_INTERVAL:-}" == "" ]]; then
echo "${BATS_VERSION}"
exit 0
fi
local NEW_BATS_VERSION
NEW_BATS_VERSION=$(semver bump "${BUMP_INTERVAL}" "${BATS_VERSION}")
declare -r NEW_BATS_VERSION
local BATS_RELEASE_NOTES="/tmp/bats-release-${NEW_BATS_VERSION}"
echo "Releasing: ${BATS_VERSION} to ${NEW_BATS_VERSION}"
echo
echo "Ensure docs/CHANGELOG.md is correctly updated"
replace_in_files
write_changelog
git diff --staged
cat <<EOF
1. Version numbers have been updated. Commit the changes:
git commit -m "feat: release Bats v${NEW_BATS_VERSION}"
2. Verify this autogenerated changelog (from docs/CHANGELOG.md):
# changelog start
EOF
local DELIM=$(echo -en "\001");
sed -E -n "\\${DELIM}^## \[${NEW_BATS_VERSION}\]${DELIM},\\${DELIM}^## ${DELIM}p" docs/CHANGELOG.md \
| head -n -1 \
| sed -E \
-e 's,^## \[([0-9\.]+)] - (.*),Bats \1\n\nReleased: \2,' \
-e 's,^### (.*),\1:,g' \
| tee "${BATS_RELEASE_NOTES}"
cat <<EOF
# changelog end
3. Tag the release using the autogenerated changelog:
git tag -a -s "v${NEW_BATS_VERSION}" --message "${BATS_RELEASE_NOTES}"
4. Push the changes:
git push --follow-tags
5. Use Github hub to make a draft release:
hub release create "v${NEW_BATS_VERSION}" --draft --file "${BATS_RELEASE_NOTES}"
6. Navigate to the provided URL, verify changes, and release Bats ${NEW_BATS_VERSION}.
EOF
exit 0
}
replace_in_files() {
declare -a FILE_REPLACEMENTS=(
"contrib/rpm/bats.spec,^Version:"
"libexec/bats-core/bats,^export BATS_VERSION="
"package.json,^ \"version\":"
)
for FILE_REPLACEMENT in "${FILE_REPLACEMENTS[@]}"; do
FILE="${FILE_REPLACEMENT/,*/}"
MATCH="${FILE_REPLACEMENT/*,/}"
sed -E -i.bak "/${MATCH}/ { s,${BATS_VERSION},${NEW_BATS_VERSION},g; }" "${FILE}"
rm "${FILE}.bak" || true
git add -f "${FILE}"
done
}
write_changelog() {
local FILE="docs/CHANGELOG.md"
sed -E -i.bak "/## \[Unreleased\]/ a \\\n## [${NEW_BATS_VERSION}] - $(date +%Y-%m-%d)" "${FILE}"
rm "${FILE}.bak" || true
cp "${FILE}" "${FILE}.new"
sed -E -i.bak '/## \[Unreleased\]/,+1d' "${FILE}"
git add -f "${FILE}"
mv "${FILE}.new" "${FILE}"
}
handle_arguments() {
parse_arguments "${@:-}"
}
parse_arguments() {
local CURRENT_ARG
if [[ "${#}" == 1 && "${1:-}" == "" ]]; then
return 0
fi
while [[ "${#}" -gt 0 ]]; do
CURRENT_ARG="${1}"
case ${CURRENT_ARG} in
--major)
BUMP_INTERVAL="major"
;;
# ---
--minor)
BUMP_INTERVAL="minor"
;;
--patch)
BUMP_INTERVAL="patch"
;;
-h | --help) usage ;;
-v | --version)
get_version
exit 0
;;
--debug)
set -xe
;;
-*) usage "${CURRENT_ARG}: unknown option" ;;
esac
shift
done
}
semver() {
"${DIR}/semver" "${@:-}"
}
usage() {
sed -n '/^##/,/^$/s/^## \{0,1\}//p' "${THIS_SCRIPT}" | sed "s/%SCRIPT_NAME%/$(basename "${THIS_SCRIPT}")/g"
exit 2
} 2>/dev/null
get_version() {
echo "${THIS_SCRIPT_VERSION:-0.1}"
}
main "${@}"

@ -3,7 +3,7 @@
%global repo bats-core
Name: bats
Version: 1.1.0
Version: 1.3.0
Release: 1%{?dist}
Summary: Bash Automated Testing System
@ -25,7 +25,7 @@ Bats is most useful when testing software written in Bash, but you can use it to
%setup -q -n %{repo}-%{version}
%install
mkdir -p ${RPM_BUILD_ROOT}%{_prefix} ${RPM_BUILD_ROOT}%{_libexecdir} ${RPM_BUILD_ROOT}%{_mandir}
mkdir -p ${RPM_BUILD_ROOT}%{_prefix} ${RPM_BUILD_ROOT}%{_libexecdir} ${RPM_BUILD_ROOT}%{_mandir}
./install.sh ${RPM_BUILD_ROOT}%{_prefix}
%clean

@ -0,0 +1,281 @@
#!/usr/bin/env bash
# v3.0.0
# https://github.com/fsaintjacques/semver-tool
set -o errexit -o nounset -o pipefail
NAT='0|[1-9][0-9]*'
ALPHANUM='[0-9]*[A-Za-z-][0-9A-Za-z-]*'
IDENT="$NAT|$ALPHANUM"
FIELD='[0-9A-Za-z-]+'
SEMVER_REGEX="\
^[vV]?\
($NAT)\\.($NAT)\\.($NAT)\
(\\-(${IDENT})(\\.(${IDENT}))*)?\
(\\+${FIELD}(\\.${FIELD})*)?$"
PROG=semver
PROG_VERSION="3.0.0"
USAGE="\
Usage:
$PROG bump (major|minor|patch|release|prerel <prerel>|build <build>) <version>
$PROG compare <version> <other_version>
$PROG get (major|minor|patch|release|prerel|build) <version>
$PROG --help
$PROG --version
Arguments:
<version> A version must match the following regular expression:
\"${SEMVER_REGEX}\"
In English:
-- The version must match X.Y.Z[-PRERELEASE][+BUILD]
where X, Y and Z are non-negative integers.
-- PRERELEASE is a dot separated sequence of non-negative integers and/or
identifiers composed of alphanumeric characters and hyphens (with
at least one non-digit). Numeric identifiers must not have leading
zeros. A hyphen (\"-\") introduces this optional part.
-- BUILD is a dot separated sequence of identifiers composed of alphanumeric
characters and hyphens. A plus (\"+\") introduces this optional part.
<other_version> See <version> definition.
<prerel> A string as defined by PRERELEASE above.
<build> A string as defined by BUILD above.
Options:
-v, --version Print the version of this tool.
-h, --help Print this help message.
Commands:
bump Bump by one of major, minor, patch; zeroing or removing
subsequent parts. \"bump prerel\" sets the PRERELEASE part and
removes any BUILD part. \"bump build\" sets the BUILD part.
\"bump release\" removes any PRERELEASE or BUILD parts.
The bumped version is written to stdout.
compare Compare <version> with <other_version>, output to stdout the
following values: -1 if <other_version> is newer, 0 if equal, 1 if
older. The BUILD part is not used in comparisons.
get Extract given part of <version>, where part is one of major, minor,
patch, prerel, build, or release.
See also:
https://semver.org -- Semantic Versioning 2.0.0"
function error {
echo -e "$1" >&2
exit 1
}
function usage-help {
error "$USAGE"
}
function usage-version {
echo -e "${PROG}: $PROG_VERSION"
exit 0
}
function validate-version {
local version=$1
if [[ "$version" =~ $SEMVER_REGEX ]]; then
# if a second argument is passed, store the result in var named by $2
if [ "$#" -eq "2" ]; then
local major=${BASH_REMATCH[1]}
local minor=${BASH_REMATCH[2]}
local patch=${BASH_REMATCH[3]}
local prere=${BASH_REMATCH[4]}
local build=${BASH_REMATCH[8]}
eval "$2=(\"$major\" \"$minor\" \"$patch\" \"$prere\" \"$build\")"
else
echo "$version"
fi
else
error "version $version does not match the semver scheme 'X.Y.Z(-PRERELEASE)(+BUILD)'. See help for more information."
fi
}
function is-nat {
[[ "$1" =~ ^($NAT)$ ]]
}
function is-null {
[ -z "$1" ]
}
function order-nat {
[ "$1" -lt "$2" ] && { echo -1 ; return ; }
[ "$1" -gt "$2" ] && { echo 1 ; return ; }
echo 0
}
function order-string {
[[ $1 < $2 ]] && { echo -1 ; return ; }
[[ $1 > $2 ]] && { echo 1 ; return ; }
echo 0
}
# given two (named) arrays containing NAT and/or ALPHANUM fields, compare them
# one by one according to semver 2.0.0 spec. Return -1, 0, 1 if left array ($1)
# is less-than, equal, or greater-than the right array ($2). The longer array
# is considered greater-than the shorter if the shorter is a prefix of the longer.
#
function compare-fields {
local l="$1[@]"
local r="$2[@]"
local leftfield=( "${!l}" )
local rightfield=( "${!r}" )
local left
local right
local i=$(( -1 ))
local order=$(( 0 ))
while true
do
[ $order -ne 0 ] && { echo $order ; return ; }
: $(( i++ ))
left="${leftfield[$i]}"
right="${rightfield[$i]}"
is-null "$left" && is-null "$right" && { echo 0 ; return ; }
is-null "$left" && { echo -1 ; return ; }
is-null "$right" && { echo 1 ; return ; }
is-nat "$left" && is-nat "$right" && { order=$(order-nat "$left" "$right") ; continue ; }
is-nat "$left" && { echo -1 ; return ; }
is-nat "$right" && { echo 1 ; return ; }
{ order=$(order-string "$left" "$right") ; continue ; }
done
}
# shellcheck disable=SC2206 # checked by "validate"; ok to expand prerel id's into array
function compare-version {
local order
validate-version "$1" V
validate-version "$2" V_
# compare major, minor, patch
local left=( "${V[0]}" "${V[1]}" "${V[2]}" )
local right=( "${V_[0]}" "${V_[1]}" "${V_[2]}" )
order=$(compare-fields left right)
[ "$order" -ne 0 ] && { echo "$order" ; return ; }
# compare pre-release ids when M.m.p are equal
local prerel="${V[3]:1}"
local prerel_="${V_[3]:1}"
local left=( ${prerel//./ } )
local right=( ${prerel_//./ } )
# if left and right have no pre-release part, then left equals right
# if only one of left/right has pre-release part, that one is less than simple M.m.p
[ -z "$prerel" ] && [ -z "$prerel_" ] && { echo 0 ; return ; }
[ -z "$prerel" ] && { echo 1 ; return ; }
[ -z "$prerel_" ] && { echo -1 ; return ; }
# otherwise, compare the pre-release id's
compare-fields left right
}
function command-bump {
local new; local version; local sub_version; local command;
case $# in
2) case $1 in
major|minor|patch|release) command=$1; version=$2;;
*) usage-help;;
esac ;;
3) case $1 in
prerel|build) command=$1; sub_version=$2 version=$3 ;;
*) usage-help;;
esac ;;
*) usage-help;;
esac
validate-version "$version" parts
# shellcheck disable=SC2154
local major="${parts[0]}"
local minor="${parts[1]}"
local patch="${parts[2]}"
local prere="${parts[3]}"
local build="${parts[4]}"
case "$command" in
major) new="$((major + 1)).0.0";;
minor) new="${major}.$((minor + 1)).0";;
patch) new="${major}.${minor}.$((patch + 1))";;
release) new="${major}.${minor}.${patch}";;
prerel) new=$(validate-version "${major}.${minor}.${patch}-${sub_version}");;
build) new=$(validate-version "${major}.${minor}.${patch}${prere}+${sub_version}");;
*) usage-help ;;
esac
echo "$new"
exit 0
}
function command-compare {
local v; local v_;
case $# in
2) v=$(validate-version "$1"); v_=$(validate-version "$2") ;;
*) usage-help ;;
esac
set +u # need unset array element to evaluate to null
compare-version "$v" "$v_"
exit 0
}
# shellcheck disable=SC2034
function command-get {
local part version
if [[ "$#" -ne "2" ]] || [[ -z "$1" ]] || [[ -z "$2" ]]; then
usage-help
exit 0
fi
part="$1"
version="$2"
validate-version "$version" parts
local major="${parts[0]}"
local minor="${parts[1]}"
local patch="${parts[2]}"
local prerel="${parts[3]:1}"
local build="${parts[4]:1}"
local release="${major}.${minor}.${patch}"
case "$part" in
major|minor|patch|release|prerel|build) echo "${!part}" ;;
*) usage-help ;;
esac
exit 0
}
case $# in
0) echo "Unknown command: $*"; usage-help;;
esac
case $1 in
--help|-h) echo -e "$USAGE"; exit 0;;
--version|-v) usage-version ;;
bump) shift; command-bump "$@";;
get) shift; command-get "$@";;
compare) shift; command-compare "$@";;
*) echo "Unknown arguments: $*"; usage-help;;
esac

@ -0,0 +1,8 @@
# Copy this file to docker-compose.override.yml
version: '3.6'
services:
bats:
entrypoint:
- "bash"
networks:
default:

@ -0,0 +1,13 @@
version: '3.6'
services:
bats:
build:
context: "."
dockerfile: "Dockerfile"
networks:
- "default"
user: "root"
volumes:
- "./:/opt/bats"
networks:
default:

@ -0,0 +1,3 @@
{
"MD024": { "siblings_only": true }
}

@ -0,0 +1,231 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog][kac] and this project adheres to
[Semantic Versioning][semver].
[kac]: https://keepachangelog.com/en/1.0.0/
[semver]: https://semver.org/
## [Unreleased]
## [1.3.0] - 2021-03-08
### Added
* custom test-file extension via `BATS_FILE_EXTENSION` when searching for test
files in a directory (#376)
* TAP13 formatter, including millisecond timing (#337)
* automatic release to NPM via Github Actions (#406)
#### Documentation
* added documentation about overusing `run` (#343)
* improved documentation of `load` (#332)
### Changed
* recursive suite mode will follow symlinks now (#370)
* split options for (file-) `--report-formatter` and (stdout) `--formatter` (#345)
* **WARNING**: This changes the meaning of `--formatter junit`.
stdout will now show unified xml instead of TAP. From now on, please use
`--report-formatter junit` to obtain the `.xml` report file!
* removed `--parallel-preserve-environment` flag, as this is the default
behavior (#324)
* moved CI from Travis/Appveyor to Github Actions (#405)
* preprocessed files are no longer removed if `--no-tempdir-cleanup` is
specified (#395)
#### Documentation
* moved documentation to [readthedocs](https://bats-core.readthedocs.io/en/latest/)
### Fixed
#### Correctness
* fix internal failures due to unbound variables when test files use `set -u` (#392)
* fix internal failures due to changes to `$PATH` in test files (#387)
* fix test duration always being 0 on busybox installs (#363)
* fix hangs on CTRL+C (#354)
* make `BATS_TEST_NUMBER` count per file again (#326)
* include `lib/` in npm package (#352)
#### Performance
* don't fork bomb in parallel mode (#339)
* preprocess each file only once (#335)
* avoid running duplicate files n^2 times (#338)
#### Documentation
* fix documentation for `--formatter junit` (#334)
* fix documentation for `setup_file` variables (#333)
* fix link to examples page (#331)
* fix link to "File Descriptor 3" section (#301)
## [1.2.1] - 2020-07-06
### Added
* JUnit output and extensible formatter rewrite (#246)
* `load` function now reads from absolute and relative paths, and $PATH (#282)
* Beginner-friendly examples in /docs/examples (#243)
* @peshay's `bats-file` fork contributed to `bats-core/bats-file` (#276)
### Changed
* Duplicate test names now error (previous behaviour was to issue a warning) (#286)
* Changed default formatter in Docker to pretty by adding `ncurses` to
Dockerfile, override with `--tap` (#239)
* Replace "readlink -f" dependency with Bash solution (#217)
## [1.2.0] - 2020-04-25
Support parallel suite execution and filtering by test name.
### Added
* docs/CHANGELOG.md and docs/releasing.md (#122)
* The `-f, --filter` flag to run only the tests matching a regular expression (#126)
* Optimize stack trace capture (#138)
* `--jobs n` flag to support parallel execution of tests with GNU parallel (#172)
### Changed
* AppVeyor builds are now semver-compliant (#123)
* Add Bash 5 as test target (#181)
* Always use upper case signal names to avoid locale dependent err… (#215)
* Fix for tests reading from stdin (#227)
* Fix wrong line numbers of errors in bash < 4.4 (#229)
* Remove preprocessed source after test run (#232)
## [1.1.0] - 2018-07-08
This is the first release with new features relative to the original Bats 0.4.0.
### Added
* The `-r, --recursive` flag to scan directory arguments recursively for
`*.bats` files (#109)
* The `contrib/rpm/bats.spec` file to build RPMs (#111)
### Changed
* Travis exercises latest versions of Bash from 3.2 through 4.4 (#116, #117)
* Error output highlights invalid command line options (#45, #46, #118)
* Replaced `echo` with `printf` (#120)
### Fixed
* Fixed `BATS_ERROR_STATUS` getting lost when `bats_error_trap` fired multiple
times under Bash 4.2.x (#110)
* Updated `bin/bats` symlink resolution, handling the case on CentOS where
`/bin` is a symlink to `/usr/bin` (#113, #115)
## [1.0.2] - 2018-06-18
* Fixed sstephenson/bats#240, whereby `skip` messages containing parentheses
were truncated (#48)
* Doc improvements:
* Docker usage (#94)
* Better README badges (#101)
* Better installation instructions (#102, #104)
* Packaging/installation improvements:
* package.json update (#100)
* Moved `libexec/` files to `libexec/bats-core/`, improved `install.sh` (#105)
## [1.0.1] - 2018-06-09
* Fixed a `BATS_CWD` bug introduced in #91 whereby it was set to the parent of
`PWD`, when it should've been set to `PWD` itself (#98). This caused file
names in stack traces to contain the basename of `PWD` as a prefix, when the
names should've been purely relative to `PWD`.
* Ensure the last line of test output prints when it doesn't end with a newline
(#99). This was a quasi-bug introduced by replacing `sed` with `while` in #88.
## [1.0.0] - 2018-06-08
`1.0.0` generally preserves compatibility with `0.4.0`, but with some Bash
compatibility improvements and a massive performance boost. In other words:
* all existing tests should remain compatible
* tests that might've failed or exhibited unexpected behavior on earlier
versions of Bash should now also pass or behave as expected
Changes:
* Added support for Docker.
* Added support for test scripts that have the [unofficial strict
mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) enabled.
* Improved stability on Windows and macOS platforms.
* Massive performance improvements, especially on Windows (#8)
* Workarounds for inconsistent behavior between Bash versions (#82)
* Workaround for preserving stack info after calling an exported function under
Bash < 4.4 (#87)
* Fixed TAP compliance for skipped tests
* Added support for tabs in test names.
* `bin/bats` and `install.sh` now work reliably on Windows (#91)
## [0.4.0] - 2014-08-13
* Improved the display of failing test cases. Bats now shows the source code of
failing test lines, along with full stack traces including function names,
filenames, and line numbers.
* Improved the display of the pretty-printed test summary line to include the
number of skipped tests, if any.
* Improved the speed of the preprocessor, dramatically shortening test and suite
startup times.
* Added support for absolute pathnames to the `load` helper.
* Added support for single-line `@test` definitions.
* Added bats(1) and bats(7) manual pages.
* Modified the `bats` command to default to TAP output when the `$CI` variable
is set, to better support environments such as Travis CI.
## [0.3.1] - 2013-10-28
* Fixed an incompatibility with the pretty formatter in certain environments
such as tmux.
* Fixed a bug where the pretty formatter would crash if the first line of a test
file's output was invalid TAP.
## [0.3.0] - 2013-10-21
* Improved formatting for tests run from a terminal. Failing tests are now
colored in red, and the total number of failing tests is displayed at the end
of the test run. When Bats is not connected to a terminal (e.g. in CI runs),
or when invoked with the `--tap` flag, output is displayed in standard TAP
format.
* Added the ability to skip tests using the `skip` command.
* Added a message to failing test case output indicating the file and line
number of the statement that caused the test to fail.
* Added "ad-hoc" test suite support. You can now invoke `bats` with multiple
filename or directory arguments to run all the specified tests in aggregate.
* Added support for test files with Windows line endings.
* Fixed regular expression warnings from certain versions of Bash.
* Fixed a bug running tests containing lines that begin with `-e`.
## [0.2.0] - 2012-11-16
* Added test suite support. The `bats` command accepts a directory name
containing multiple test files to be run in aggregate.
* Added the ability to count the number of test cases in a file or suite by
passing the `-c` flag to `bats`.
* Preprocessed sources are cached between test case runs in the same file for
better performance.
## [0.1.0] - 2011-12-30
* Initial public release.
[Unreleased]: https://github.com/bats-core/bats-core/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/bats-core/bats-core/compare/v1.0.2...v1.1.0
[1.0.2]: https://github.com/bats-core/bats-core/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/bats-core/bats-core/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/bats-core/bats-core/compare/v0.4.0...v1.0.0
[0.4.0]: https://github.com/bats-core/bats-core/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/bats-core/bats-core/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/bats-core/bats-core/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/bats-core/bats-core/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/bats-core/bats-core/commits/v0.1.0

@ -193,8 +193,7 @@ your own fork of the repository and issuing pull requests to the original.
## Testing
- Continuous integration status for Linux and macOS: [![Build Status on Travis](https://travis-ci.org/bats-core/bats-core.svg?branch=ci-configs)](https://travis-ci.org/bats-core/bats-core)
- Continuous integration status for Windows: [![Build status on AppVeyor](https://ci.appveyor.com/api/projects/status/tokwm9t9jp5fe7af?svg=true)](https://ci.appveyor.com/project/bats-core/bats-core)
- Continuous integration status: [![Tests](https://github.com/bats-core/bats-core/workflows/Tests/badge.svg)](https://github.com/bats-core/bats-core/actions?query=workflow%3ATests)
## Coding conventions
@ -336,6 +335,21 @@ The following are intended to prevent too-compact code:
[printf-vs-echo]: https://unix.stackexchange.com/a/65819
### Signal names
Always use upper case signal names (e.g. `trap - INT EXIT`) to avoid locale
dependent errors. In some locales (for example Turkish, see
[Turkish dotless i](https://en.wikipedia.org/wiki/Dotted_and_dotless_I)) lower
case signal names cause Bash to error. An example of the problem:
```bash
$ echo "tr_TR.UTF-8 UTF-8" >> /etc/locale.gen && locale-gen tr_TR.UTF-8 # Ubuntu derivatives
$ LC_CTYPE=tr_TR.UTF-8 LC_MESSAGES=C bash -c 'trap - int && echo success'
bash: line 0: trap: int: invalid signal specification
$ LC_CTYPE=tr_TR.UTF-8 LC_MESSAGES=C bash -c 'trap - INT && echo success'
success
```
### Gotchas
- If you wish to use command substitution to initialize a `local` variable, and

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

@ -0,0 +1,6 @@
# Examples
This directory contains example .bats files.
See the [bats-core wiki][examples] for more details.
[examples]: https://github.com/bats-core/bats-core/wiki/Examples

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# "unofficial" bash strict mode
# See: http://redsymbol.net/articles/unofficial-bash-strict-mode
set -o errexit # Exit when simple command fails 'set -e'
set -o errtrace # Exit on error inside any functions or subshells.
set -o nounset # Trigger error when expanding unset variables 'set -u'
set -o pipefail # Do not hide errors within pipes 'set -o pipefail'
set -o xtrace # Display expanded command and arguments 'set -x'
IFS=$'\n\t' # Split words on \n\t rather than spaces
main() {
tar -czf "$dst_tarball" -C "$src_dir" .
}
main "$@"

@ -0,0 +1,47 @@
#!/usr/bin/env bats
setup() {
export dst_tarball="${BATS_TMPDIR}/dst.tar.gz"
export src_dir="${BATS_TMPDIR}/src_dir"
rm -rf "${dst_tarball}" "${src_dir}"
mkdir "${src_dir}"
touch "${src_dir}"/{a,b,c}
}
main() {
bash "${BATS_TEST_DIRNAME}"/package-tarball
}
@test 'fail when \$src_dir and \$dst_tarball are unbound' {
unset src_dir dst_tarball
run main
[ "${status}" -ne 0 ]
}
@test 'fail when \$src_dir is a non-existent directory' {
src_dir='not-a-dir'
run main
[ "${status}" -ne 0 ]
}
@test 'pass when \$src_dir directory is empty' {
rm -rf "${src_dir:?}/*"
run main
echo "$output"
[ "${status}" -eq 0 ]
}
@test 'files in \$src_dir are added to tar archive' {
run main
[ "${status}" -eq 0 ]
run tar tf "$dst_tarball"
[ "${status}" -eq 0 ]
[[ "${output}" =~ a ]]
[[ "${output}" =~ b ]]
[[ "${output}" =~ c ]]
}

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

@ -0,0 +1,127 @@
# Releasing a new Bats version
These notes reflect the current process. There's a lot more we could do, in
terms of automation and expanding the number of platforms to which we formally
release (see #103).
## Update docs/CHANGELOG.md
Create a new entry at the top of `docs/CHANGELOG.md` that enumerates the
significant updates to the new version.
## Bumping the version number
Bump the version numbers in the following files:
- contrib/rpm/bats.spec
- libexec/bats-core/bats
- package.json
Commit these changes (including the `docs/CHANGELOG.md` changes) in a commit
with the message `Bats <VERSION>`, where `<VERSION>` is the new version number.
Create a new signed, annotated tag with:
```bash
$ git tag -a -s <VERSION>
```
Include the `docs/CHANGELOG.md` notes corresponding to the new version as the
tag annotation, except the first line should be: `Bats <VERSION> - YYYY-MM-DD`
and any Markdown headings should become plain text, e.g.:
```md
### Added
```
should become:
```md
Added:
```
## Create a GitHub release
Push the new version commit and tag to GitHub via the following:
```bash
$ git push --follow-tags
```
Then visit https://github.com/bats-core/bats-core/releases, and:
* Click **Draft a new release**.
* Select the new version tag.
* Name the release: `Bats <VERSION>`.
* Paste the same notes from the version tag annotation as the description,
except change the first line to read: `Released: YYYY-MM-DD`.
* Click **Publish release**.
For more on `git push --follow-tags`, see:
* [git push --follow-tags in the online manual][ft-man]
* [Stack Overflow: How to push a tag to a remote repository using Git?][ft-so]
[ft-man]: https://git-scm.com/docs/git-push#git-push---follow-tags
[ft-so]: https://stackoverflow.com/a/26438076
## NPM
`npm publish`. Pretty easy!
For the paranoid, use `npm pack` and install the resulting tarball locally with
`npm install` before publishing.
## Homebrew
The basic instructions are in the [Submit a new version of an existing
formula][brew] section of the Homebrew docs.
[brew]: https://github.com/Homebrew/brew/blob/master/docs/How-To-Open-a-Homebrew-Pull-Request.md#submit-a-new-version-of-an-existing-formula
An example using v1.1.0 (notice that this uses the sha256 sum of the tarball):
```bash
$ curl -LOv https://github.com/bats-core/bats-core/archive/v1.1.0.tar.gz
$ openssl sha256 v1.1.0.tar.gz
SHA256(v1.1.0.tar.gz)=855d8b8bed466bc505e61123d12885500ef6fcdb317ace1b668087364717ea82
# Add the --dry-run flag to see the individual steps without executing.
$ brew bump-formula-pr \
--url=https://github.com/bats-core/bats-core/archive/v1.1.0.tar.gz \
--sha256=855d8b8bed466bc505e61123d12885500ef6fcdb317ace1b668087364717ea82
```
This resulted in https://github.com/Homebrew/homebrew-core/pull/29864, which was
automatically merged once the build passed.
## Alpine Linux
An example using v1.1.0 (notice that this uses the sha512 sum of the Zip file):
```bash
$ curl -LOv https://github.com/bats-core/bats-core/archive/v1.1.0.zip
$ openssl sha512 v1.1.0.zip
SHA512(v1.1.0.zip)=accd83cfec0025a2be40982b3f9a314c2bbf72f5c85daffa9e9419611904a8d34e376919a5d53e378382e0f3794d2bd781046d810225e2a77812474e427bed9e
```
After cloning alpinelinux/aports, I used the above information to create:
https://github.com/alpinelinux/aports/pull/4696
**Note:** Currently users must enable the `edge` branch of the `community` repo
by adding/uncommenting the corresponding entry in `/etc/apk/repositories`.
## Announce
It's worth making a brief announcement like [the v1.1.0 announcement via
Gitter][gitter]:
[gitter]: https://gitter.im/bats-core/bats-core?at=5b42c9a57b811a6d63daacb5
```
v1.1.0 is now available via Homebrew and npm:
https://github.com/bats-core/bats-core/releases/tag/v1.1.0
It'll eventually be available in Alpine via the edge branch of the community
repo once alpinelinux/aports#4696 gets merged. (Check /etc/apk/repositories to
ensure this repo is enabled.)
```

@ -0,0 +1,72 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'bats-core'
copyright = '2020, bats-core origanization'
author = 'bats-core origanization'
# The full version, including alpha/beta/rc tags
release = '1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'recommonmark',
'sphinxcontrib.programoutput'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
html_sidebars = { '**': [
'about.html',
'navigation.html',
'relations.html',
'searchbox.html',
'donate.html'] }
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
#html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
#man_pages = [ ('man.1', 'bats', 'bats documentation', ['bats-core Contributors'], 1)]
def setup(app):
app.add_config_value('recommonmark_config', {'enable_eval_rst': True}, True)
import recommonmark
from recommonmark.transform import AutoStructify
app.add_transform(AutoStructify)

@ -20,7 +20,7 @@ Resolving deltas: 100% (661/661), done.
$ cd bats-core/
$ docker build --tag bats:latest .
...
$ docker run -it bats:latest --tap /opt/bats/test
$ docker run -it bats:latest --formatter tap /opt/bats/test
```
To mount your tests into the container, first build the image as above. Then, for example with `bats`:

@ -0,0 +1,14 @@
Welcome to bats-core's documentation!
=====================================
Versions before v1.2.1 are documented over `there <https://github.com/bats-core/bats-core/blob/master/docs/versions.md>`_.
.. toctree::
:maxdepth: 2
:caption: Contents:
installation
usage
docker-usage
writing-tests

@ -0,0 +1,152 @@
Installation
============
Supported Bash versions
^^^^^^^^^^^^^^^^^^^^^^^
The following is a list of Bash versions that are currently supported by Bats.
This list is composed of platforms that Bats has been tested on and is known to
work on without issues.
*
Bash versions:
* Everything from ``3.2.57(1)`` and higher (macOS's highest version)
*
Operating systems:
* Arch Linux
* Alpine Linux
* Ubuntu Linux
* FreeBSD ``10.x`` and ``11.x``
* macOS
* Windows 10
*
Latest version for the following Windows platforms:
* Git for Windows Bash (MSYS2 based)
* Windows Subsystem for Linux
* MSYS2
* Cygwin
Homebrew
^^^^^^^^
On macOS, you can install `Homebrew <https://brew.sh/>`_ if you haven't already,
then run:
.. code-block:: bash
$ brew install bats-core
npm
^^^
You can install the `Bats npm package <https://www.npmjs.com/package/bats>`_ via:
.. code-block::
# To install globally:
$ npm install -g bats
# To install into your project and save it as one of the "devDependencies" in
# your package.json:
$ npm install --save-dev bats
Installing Bats from source
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Check out a copy of the Bats repository. Then, either add the Bats ``bin``
directory to your ``$PATH``\ , or run the provided ``install.sh`` command with the
location to the prefix in which you want to install Bats. For example, to
install Bats into ``/usr/local``\ ,
.. code-block::
$ git clone https://github.com/bats-core/bats-core.git
$ cd bats-core
$ ./install.sh /usr/local
**Note:** You may need to run ``install.sh`` with ``sudo`` if you do not have
permission to write to the installation prefix.
Installing Bats from source onto Windows Git Bash
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Check out a copy of the Bats repository and install it to ``$HOME``. This
will place the ``bats`` executable in ``$HOME/bin``\ , which should already be
in ``$PATH``.
.. code-block::
$ git clone https://github.com/bats-core/bats-core.git
$ cd bats-core
$ ./install.sh $HOME
Running Bats in Docker
^^^^^^^^^^^^^^^^^^^^^^
There is an official image on the Docker Hub:
.. code-block::
$ docker run -it bats/bats:latest --version
Building a Docker image
~~~~~~~~~~~~~~~~~~~~~~~
Check out a copy of the Bats repository, then build a container image:
.. code-block::
$ git clone https://github.com/bats-core/bats-core.git
$ cd bats-core
$ docker build --tag bats/bats:latest .
This creates a local Docker image called ``bats/bats:latest`` based on `Alpine
Linux <https://github.com/gliderlabs/docker-alpine/blob/master/docs/usage.md>`_
(to push to private registries, tag it with another organisation, e.g.
``my-org/bats:latest``\ ).
To run Bats' internal test suite (which is in the container image at
``/opt/bats/test``\ ):
.. code-block::
$ docker run -it bats/bats:latest /opt/bats/test
To run a test suite from a directory called ``test`` in the current directory of
your local machine, mount in a volume and direct Bats to its path inside the
container:
.. code-block::
$ docker run -it -v "${PWD}:/code" bats/bats:latest test
..
``/code`` is the working directory of the Docker image. "${PWD}/test" is the
location of the test directory on the local machine.
This is a minimal Docker image. If more tools are required this can be used as a
base image in a Dockerfile using ``FROM <Docker image>``. In the future there may
be images based on Debian, and/or with more tools installed (\ ``curl`` and ``openssl``\ ,
for example). If you require a specific configuration please search and +1 an
issue or `raise a new issue <https://github.com/bats-core/bats-core/issues>`_.
Further usage examples are in
`the wiki <https://github.com/bats-core/bats-core/wiki/Docker-Usage-Examples>`_.

@ -0,0 +1,2 @@
sphinxcontrib-programoutput
recommonmark

@ -0,0 +1,94 @@
# Usage
Bats comes with two manual pages. After installation you can view them with `man
1 bats` (usage manual) and `man 7 bats` (writing test files manual). Also, you
can view the available command line options that Bats supports by calling Bats
with the `-h` or `--help` options. These are the options that Bats currently
supports:
``` eval_rst
.. program-output:: ../../bin/bats --help
```
To run your tests, invoke the `bats` interpreter with one or more paths to test
files ending with the `.bats` extension, or paths to directories containing test
files. (`bats` will only execute `.bats` files at the top level of each
directory; it will not recurse unless you specify the `-r` flag.)
Test cases from each file are run sequentially and in isolation. If all the test
cases pass, `bats` exits with a `0` status code. If there are any failures,
`bats` exits with a `1` status code.
When you run Bats from a terminal, you'll see output as each test is performed,
with a check-mark next to the test's name if it passes or an "X" if it fails.
```text
$ bats addition.bats
✓ addition using bc
✓ addition using dc
2 tests, 0 failures
```
If Bats is not connected to a terminal—in other words, if you run it from a
continuous integration system, or redirect its output to a file—the results are
displayed in human-readable, machine-parsable [TAP format][TAP].
You can force TAP output from a terminal by invoking Bats with the `--formatter tap`
option.
```text
$ bats --formatter tap addition.bats
1..2
ok 1 addition using bc
ok 2 addition using dc
```
With `--formatter junit`, it is possible
to output junit-compatible report files.
```text
$ bats --formatter junit addition.bats
1..2
ok 1 addition using bc
ok 2 addition using dc
```
Test reports will be output in the executing directory, but may be placed elsewhere
by specifying the `--output` flag.
```text
$ bats --formatter junit addition.bats --output /tmp
1..2
ok 1 addition using bc
ok 2 addition using dc
```
## Parallel Execution
``` eval_rst
.. versionadded:: 1.0.0
```
By default, Bats will execute your tests serially. However, Bats supports
parallel execution of tests (provided you have [GNU parallel][gnu-parallel] or
a compatible replacement installed) using the `--jobs` parameter. This can
result in your tests completing faster (depending on your tests and the testing
hardware).
Ordering of parallised tests is not guaranteed, so this mode may break suites
with dependencies between tests (or tests that write to shared locations). When
enabling `--jobs` for the first time be sure to re-run bats multiple times to
identify any inter-test dependencies or non-deterministic test behaviour.
When parallelizing, the results of a file only become visible after it has been finished.
You can use `--no-parallelize-across-files` to get immediate output at the cost of reduced
overall parallelity, as parallelization will only happen within files and files will be run
sequentially.
If you have files where tests within the file would interfere with each other, you can use
`--no-parallelize-within-files` to disable parallelization within all files.
If you want more finegrained control, you can `export BATS_NO_PARALLELIZE_WITHIN_FILE=true` in `setup_file()`
or outside any function to disable parallelization only within the containing file.
[gnu-parallel]: https://www.gnu.org/software/parallel/

@ -0,0 +1,323 @@
# Writing tests
Each Bats test file is evaluated _n+1_ times, where _n_ is the number of
test cases in the file. The first run counts the number of test cases,
then iterates over the test cases and executes each one in its own
process.
For more details about how Bats evaluates test files, see [Bats Evaluation
Process][bats-eval] on the wiki.
For sample test files, see [examples](https://github.com/bats-core/bats-core/tree/master/docs/examples).
[bats-eval]: https://github.com/bats-core/bats-core/wiki/Bats-Evaluation-Process
## `run`: Test other commands
Many Bats tests need to run a command and then make assertions about its exit
status and output. Bats includes a `run` helper that invokes its arguments as a
command, saves the exit status and output into special global variables, and
then returns with a `0` status code so you can continue to make assertions in
your test case.
For example, let's say you're testing that the `foo` command, when passed a
nonexistent filename, exits with a `1` status code and prints an error message.
```bash
@test "invoking foo with a nonexistent file prints an error" {
run foo nonexistent_filename
[ "$status" -eq 1 ]
[ "$output" = "foo: no such file 'nonexistent_filename'" ]
}
```
The `$status` variable contains the status code of the command, and the
`$output` variable contains the combined contents of the command's standard
output and standard error streams.
A third special variable, the `$lines` array, is available for easily accessing
individual lines of output. For example, if you want to test that invoking `foo`
without any arguments prints usage information on the first line:
```bash
@test "invoking foo without arguments prints usage" {
run foo
[ "$status" -eq 1 ]
[ "${lines[0]}" = "usage: foo <filename>" ]
}
```
__Note:__ The `run` helper executes its argument(s) in a subshell, so if
writing tests against environmental side-effects like a variable's value
being changed, these changes will not persist after `run` completes.
### When not to use `run`
In some cases, using `run` is redundant and results in a longer and less readable code.
Here are a few examples.
#### 1. In case you only need to check the command succeeded, it is better to not use run, since
```bash
run command args ...
echo "$output"
[ "$status" -eq 0 ]
```
is equivalent to
```bash
command args ...
```
since bats sets `set -e` for all tests.
#### 2. In case you want to hide the command output (which `run` does), use output redirection instead
This
```bash
run command ...
[ "$status" -eq 0 ]
```
is equivalent to
```bash
command ... >/dev/null
```
Note that the output is only shown if the test case fails.
#### 3. In case you need to assign command output to a variable (and maybe check the command exit status), it is better to not use run, since
```bash
run command args ...
[ "$status" -eq 0 ]
var="$output"
```
is equivalent to
```bash
var=$(command args ...)
```
#### Comment syntax
External tools (like `shellcheck`, `shfmt`, and various IDE's) may not support
the standard `.bats` syntax. Because of this, we provide a valid `bash`
alterntative:
```bash
function invoking_foo_without_arguments_prints_usage { #@test
run foo
[ "$status" -eq 1 ]
[ "${lines[0]}" = "usage: foo <filename>" ]
}
```
When using this syntax, the function name will be the title in the result output
and the value checked when using `--filter`.
### `load`: Share common code
You may want to share common code across multiple test files. Bats includes a
convenient `load` command for sourcing a Bash source file relative to the
location of the current test file. For example, if you have a Bats test in
`test/foo.bats`, the command
```bash
load test_helper.bash
```
will source the script `test/test_helper.bash` in your test file (limitations
apply, see below). This can be useful for sharing functions to set up your
environment or load fixtures. `load` delegates to Bash's `source` command after
resolving relative paths.
As pointed out by @iatrou in <https://www.tldp.org/LDP/abs/html/declareref.html>,
using the `declare` builtin restricts scope of a variable. Thus, since actual
`source`-ing is performed in context of the `load` function, `declare`d symbols
will _not_ be made available to callers of `load`.
> For backwards compatibility `load` first searches for a file ending in
> `.bash` (e.g. `load test_helper` searches for `test_helper.bash` before
> it looks for `test_helper`). This behaviour is deprecated and subject to
> change, please use exact filenames instead.
### `skip`: Easily skip tests
Tests can be skipped by using the `skip` command at the point in a test you wish
to skip.
```bash
@test "A test I don't want to execute for now" {
skip
run foo
[ "$status" -eq 0 ]
}
```
Optionally, you may include a reason for skipping:
```bash
@test "A test I don't want to execute for now" {
skip "This command will return zero soon, but not now"
run foo
[ "$status" -eq 0 ]
}
```
Or you can skip conditionally:
```bash
@test "A test which should run" {
if [ foo != bar ]; then
skip "foo isn't bar"
fi
run foo
[ "$status" -eq 0 ]
}
```
__Note:__ `setup` and `teardown` hooks still run for skipped tests.
### `setup` and `teardown`: Pre- and post-test hooks
You can define special `setup` and `teardown` functions, which run before and
after each test case, respectively. Use these to load fixtures, set up your
environment, and clean up when you're done.
You can also define `setup_file` and `teardown_file`, which will run once before the first test's `setup` and after the last test's `teardown` for the containing file. Variables that are exported in `setup_file` will be visible to all following functions (`setup`, the test itself, `teardown`, `teardown_file`).
<!-- markdownlint-disable MD033 -->
<details>
<summary>Example of setup/setup_file/teardown/teardown_file call order</summary>
For example the following call order would result from two files (file 1 with tests 1 and 2, and file 2 with test3) beeing tested:
```text
setup_file # from file 1, on entering file 1
setup
test1
teardown
setup
test2
teardown
teardown_file # from file 1, on leaving file 1
setup_file # from file 2, on enter file 2
setup
test3
teardown
teardown_file # from file 2, on leaving file 2
```
</details>
<!-- markdownlint-enable MD033 -->
### Code outside of test cases
You can include code in your test file outside of `@test` functions. For
example, this may be useful if you want to check for dependencies and fail
immediately if they're not present. However, any output that you print in code
outside of `@test`, `setup` or `teardown` functions must be redirected to
`stderr` (`>&2`). Otherwise, the output may cause Bats to fail by polluting the
TAP stream on `stdout`.
### File descriptor 3 (read this if Bats hangs)
Bats makes a separation between output from the code under test and output that
forms the TAP stream (which is produced by Bats internals). This is done in
order to produce TAP-compliant output. In the [Printing to the
terminal](#printing-to-the-terminal) section, there are details on how to use
file descriptor 3 to print custom text properly.
A side effect of using file descriptor 3 is that, under some circumstances, it
can cause Bats to block and execution to seem dead without reason. This can
happen if a child process is spawned in the background from a test. In this
case, the child process will inherit file descriptor 3. Bats, as the parent
process, will wait for the file descriptor to be closed by the child process
before continuing execution. If the child process takes a lot of time to
complete (eg if the child process is a `sleep 100` command or a background
service that will run indefinitely), Bats will be similarly blocked for the same
amount of time.
**To prevent this from happening, close FD 3 explicitly when running any command
that may launch long-running child processes**, e.g. `command_name 3>&-` .
### Printing to the terminal
Bats produces output compliant with [version 12 of the TAP protocol][TAP]. The
produced TAP stream is by default piped to a pretty formatter for human
consumption, but if Bats is called with the `-t` flag, then the TAP stream is
directly printed to the console.
This has implications if you try to print custom text to the terminal. As
mentioned in [File descriptor 3](#file-descriptor-3-read-this-if-bats-hangs),
bats provides a special file descriptor, `&3`, that you should use to print
your custom text. Here are some detailed guidelines to refer to:
- Printing **from within a test function**:
- To have text printed from within a test function you need to redirect the
output to file descriptor 3, eg `echo 'text' >&3`. This output will become
part of the TAP stream. You are encouraged to prepend text printed this way
with a hash (eg `echo '# text' >&3`) in order to produce 100% TAP compliant
output. Otherwise, depending on the 3rd-party tools you use to analyze the
TAP stream, you can encounter unexpected behavior or errors.
- The pretty formatter that Bats uses by default to process the TAP stream
will filter out and not print text output to file descriptor 3.
- Text that is output directly to stdout or stderr (file descriptor 1 or 2),
ie `echo 'text'` is considered part of the test function output and is
printed only on test failures for diagnostic purposes, regardless of the
formatter used (TAP or pretty).
- Printing **from within the `setup` or `teardown` functions**: The same hold
true as for printing with test functions.
- Printing **outside test or `setup`/`teardown` functions**:
- Regardless of where text is redirected to (stdout, stderr or file descriptor
3) text is immediately visible in the terminal.
- Text printed in such a way, will disable pretty formatting. Also, it will
make output non-compliant with the TAP spec. The reason for this is that
each test file is evaluated n+1 times (as mentioned
[earlier](#writing-tests)). The first run will cause such output to be
produced before the [_plan line_][tap-plan] is printed, contrary to the spec
that requires the _plan line_ to be either the first or the last line of the
output.
- Due to internal pipes/redirects, output to stderr is always printed first.
[tap-plan]: https://testanything.org/tap-specification.html#the-plan
### Special variables
There are several global variables you can use to introspect on Bats tests:
- `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test file.
- `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is located.
- `$BATS_TEST_NAMES` is an array of function names for each test case.
- `$BATS_TEST_NAME` is the name of the function containing the current test case.
- `$BATS_TEST_DESCRIPTION` is the description of the current test case.
- `$BATS_TEST_NUMBER` is the (1-based) index of the current test case in the test file.
- `$BATS_SUITE_TEST_NUMBER` is the (1-based) index of the current test case in the test suite (over all files).
- `$BATS_TMPDIR` is the location to a directory that may be used to store temporary files.
- `$BATS_FILE_EXTENSION` (default: `bats`) specifies the extension of test files that should be found when running a suite (via `bats [-r] suite_folder/`)
### Libraries and Add-ons
Bats supports loading external assertion libraries and helpers. Those under `bats-core` are officially supported libraries (integration tests welcome!):
- <https://github.com/bats-core/bats-assert> - common assertions for Bats
- <https://github.com/bats-core/bats-support> - supporting library for Bats test helpers
- <https://github.com/bats-core/bats-file> - common filesystem assertions for Bats
- <https://github.com/bats-core/bats-detik> - e2e tests of applications in K8s environments
and some external libraries, supported on a "best-effort" basis:
- <https://github.com/ztombol/bats-docs> (still relevant? Requires review)
- <https://github.com/grayhemp/bats-mock> (as per #147)
- <https://github.com/jasonkarns/bats-mock> (how is this different from grayhemp/bats-mock?)

@ -0,0 +1,9 @@
Here are the docs of following versions:
* [v1.2.0](../../v1.2.0/README.md)
* [v1.1.0](../../v1.1.0/README.md)
* [v1.0.2](../../v1.0.2/README.md)
* [v0.4.0](../../v0.4.0/README.md)
* [v0.3.1](../../v0.3.1/README.md)
* [v0.2.0](../../v0.2.0/README.md)
* [v0.1.0](../../v0.1.0/README.md)

@ -12,9 +12,10 @@ if [[ -z "$PREFIX" ]]; then
exit 1
fi
install -d -m 755 "$PREFIX"/{bin,libexec/bats-core,share/man/man{1,7}}
install -d -m 755 "$PREFIX"/{bin,libexec/bats-core,lib/bats-core,share/man/man{1,7}}
install -m 755 "$BATS_ROOT/bin"/* "$PREFIX/bin"
install -m 755 "$BATS_ROOT/libexec/bats-core"/* "$PREFIX/libexec/bats-core"
install -m 755 "$BATS_ROOT/lib/bats-core"/* "$PREFIX/lib/bats-core"
install -m 644 "$BATS_ROOT/man/bats.1" "$PREFIX/share/man/man1"
install -m 644 "$BATS_ROOT/man/bats.7" "$PREFIX/share/man/man7"

@ -0,0 +1,111 @@
#!/usr/bin/env bash
# reads (extended) bats tap streams from stdin and calls callback functions for each line
# bats_tap_stream_plan <number of tests> -> when the test plan is encountered
# bats_tap_stream_begin <test index> <test name> -> when a new test is begun WARNING: extended only
# bats_tap_stream_ok [--duration <milliseconds] <test index> <test name> -> when a test was successful
# bats_tap_stream_not_ok [--duration <milliseconds>] <test index> <test name> -> when a test has failed
# bats_tap_stream_skipped <test index> <test name> <skip reason> -> when a test was skipped
# bats_tap_stream_comment <comment text without leading '# '> <scope> -> when a comment line was encountered,
# scope tells the last encountered of plan, begin, ok, not_ok, skipped, suite
# bats_tap_stream_suite <file name> -> when a new file is begun WARNING: extended only
# bats_tap_stream_unknown <full line> <scope> -> when a line is encountered that does not match the previous entries,
# scope @see bats_tap_stream_comment
# forwards all input as is, when there is no TAP test plan header
function bats_parse_internal_extended_tap() {
local header_pattern='[0-9]+\.\.[0-9]+'
IFS= read -r header
if [[ "$header" =~ $header_pattern ]]; then
bats_tap_stream_plan "${header:3}"
else
# If the first line isn't a TAP plan, print it and pass the rest through
printf '%s\n' "$header"
exec cat
fi
ok_line_regexpr="ok ([0-9]+) (.*)"
skip_line_regexpr="ok ([0-9]+) (.*) # skip( (.*))?$"
not_ok_line_regexpr="not ok ([0-9]+) (.*)"
timing_expr="in ([0-9]+)ms$"
local test_name begin_index ok_index not_ok_index index scope
begin_index=0
index=0
scope=plan
while IFS= read -r line; do
case "$line" in
'begin '*) # this might only be called in extended tap output
((++begin_index))
scope=begin
test_name="${line#* $begin_index }"
bats_tap_stream_begin "$begin_index" "$test_name"
;;
'ok '*)
((++index))
scope=ok
if [[ "$line" =~ $ok_line_regexpr ]]; then
ok_index="${BASH_REMATCH[1]}"
test_name="${BASH_REMATCH[2]}"
if [[ "$line" =~ $skip_line_regexpr ]]; then
test_name="${BASH_REMATCH[2]}" # cut off name before "# skip"
local skip_reason="${BASH_REMATCH[4]}"
bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason"
else
if [[ "$line" =~ $timing_expr ]]; then
bats_tap_stream_ok --duration "${BASH_REMATCH[1]}" "$ok_index" "$test_name"
else
bats_tap_stream_ok "$ok_index" "$test_name"
fi
fi
else
printf "ERROR: could not match ok line: %s" "$line" >&2
exit 1
fi
;;
'not ok '*)
((++index))
scope=not_ok
if [[ "$line" =~ $not_ok_line_regexpr ]]; then
not_ok_index="${BASH_REMATCH[1]}"
test_name="${BASH_REMATCH[2]}"
if [[ "$line" =~ $timing_expr ]]; then
bats_tap_stream_not_ok --duration "${BASH_REMATCH[1]}" "$not_ok_index" "$test_name"
else
bats_tap_stream_not_ok "$not_ok_index" "$test_name"
fi
else
printf "ERROR: could not match not ok line: %s" "$line" >&2
exit 1
fi
;;
'# '*)
bats_tap_stream_comment "${line:2}" "$scope"
;;
'suite '*)
scope=suite
# pass on the
bats_tap_stream_suite "${line:6}"
;;
*)
bats_tap_stream_unknown "$line" "$scope"
;;
esac
done
}
# given a prefix and a path, remove the prefix if the path starts with it
# e.g.
# remove_prefix /usr/bin /usr/bin/bash -> bash
# remove_prefix /usr /usr/lib/bash -> lib/bash
# remove_prefix /usr/bin /usr/local/bin/bash -> /usr/local/bin/bash
remove_prefix() {
base_path="$1"
path="$2"
if [[ "$path" == "$base_path"* ]]; then
# cut off the common prefix
printf "%s" "${path:${#base_path}}"
else
printf "%s" "$path"
fi
}

@ -0,0 +1,28 @@
#!/usr/bin/env bash
if [[ -z "${TMPDIR:-}" ]]; then
export BATS_TMPDIR='/tmp'
else
export BATS_TMPDIR="${TMPDIR%/}"
fi
BATS_TMPNAME="$BATS_RUN_TMPDIR/bats.$$"
BATS_PARENT_TMPNAME="$BATS_RUN_TMPDIR/bats.$PPID"
# shellcheck disable=SC2034
BATS_OUT="${BATS_TMPNAME}.out" # used in bats-exec-file
bats_preprocess_source() {
# export to make it visible to bats_evaluate_preprocessed_source
# since the latter runs in bats-exec-test's bash while this runs in bats-exec-file's
export BATS_TEST_SOURCE="${BATS_TMPNAME}.src"
bats-preprocess "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE"
}
bats_evaluate_preprocessed_source() {
if [[ -z "${BATS_TEST_SOURCE:-}" ]]; then
BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src"
fi
# Dynamically loaded user files provided outside of Bats.
# shellcheck disable=SC1090
source "$BATS_TEST_SOURCE"
}

@ -0,0 +1,101 @@
#!/usr/bin/env bash
# $1 - output directory for stdout/stderr
# $@ - command to run
# run the given command in a semaphore
# block when there is no free slot for the semaphore
# when there is a free slot, run the command in background
# gather the output of the command in files in the given directory
bats_semaphore_run() {
local output_dir=$1
shift
local semaphore_slot
semaphore_slot=$(bats_semaphore_acquire_slot)
bats_semaphore_release_wrapper "$output_dir" "$semaphore_slot" "$@" &
printf "%d\n" "$!"
}
export BATS_SEMAPHORE_DIR="$BATS_RUN_TMPDIR/semaphores"
# $1 - output directory for stdout/stderr
# $@ - command to run
# this wraps the actual function call to install some traps on exiting
bats_semaphore_release_wrapper() {
local output_dir="$1"
local semaphore_name="$2"
shift 2 # all other parameters will be use for the command to execute
# shellcheck disable=SC2064 # we want to expand the semaphore_name right now!
trap "status=$?; bats_semaphore_release_slot '$semaphore_name'; exit $status" EXIT
mkdir -p "$output_dir"
"$@" 2>"$output_dir/stderr" >"$output_dir/stdout"
local status=$?
# bash bug: the exit trap is not called for the background process
bats_semaphore_release_slot "$semaphore_name"
trap - EXIT # avoid calling release twice
return $status
}
bats_semaphore_acquire_while_locked() {
if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then
local slot=0
while [[ -e "$BATS_SEMAPHORE_DIR/slot-$slot" ]]; do
(( ++slot ))
done
if [[ $slot -lt $BATS_SEMAPHORE_NUMBER_OF_SLOTS ]]; then
touch "$BATS_SEMAPHORE_DIR/slot-$slot" && printf "%d\n" "$slot" && return 0
fi
fi
return 1
}
export -f bats_semaphore_acquire_while_locked
if command -v flock >/dev/null; then
bats_run_under_lock() {
flock "$BATS_SEMAPHORE_DIR" "$@"
}
elif command -v shlock >/dev/null; then
bats_run_under_lock() {
local lockfile="$BATS_SEMAPHORE_DIR/shlock.lock"
while ! shlock -p $$ -f "$lockfile"; do
sleep 1
done
# we got the lock now, execute the command
"$@"
# free the lock
rm -f "$lockfile"
}
fi
# block until a semaphore slot becomes free
# prints the number of the slot that it received
bats_semaphore_acquire_slot() {
mkdir -p "$BATS_SEMAPHORE_DIR"
# wait for a slot to become free
# TODO: avoid busy waiting by using signals -> this opens op prioritizing possibilities as well
while true; do
# don't lock for reading, we are fine with spuriously getting no free slot
if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then
bats_run_under_lock bash -c bats_semaphore_acquire_while_locked && break
fi
sleep 1
done
}
bats_semaphore_release_slot() {
# we don't need to lock this, since only our process owns this file
# and freeing a semaphore cannot lead to conflicts with others
rm "$BATS_SEMAPHORE_DIR/slot-$1" # this will fail if we had not aqcuired a semaphore!
}
bats_semaphore_get_free_slot_count() {
# find might error out without returning something useful when a file is deleted,
# while the directory is traversed -> only continue when there was no error
until used_slots=$(find "$BATS_SEMAPHORE_DIR" -name 'slot-*' 2>/dev/null | wc -l); do :; done
echo $(( BATS_SEMAPHORE_NUMBER_OF_SLOTS - used_slots ))
}
export -f bats_semaphore_get_free_slot_count

@ -0,0 +1,91 @@
#!/usr/bin/env bash
BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}"
BATS_TEST_NAMES=()
# Shorthand for source-ing files relative to the BATS_TEST_DIRNAME,
# optionally with a .bash suffix appended. If the argument doesn't
# resolve relative to BATS_TEST_DIRNAME it is sourced as-is.
load() {
local file="${1:?}"
# For backwards-compatibility first look for a .bash-suffixed file.
# TODO consider flipping the order here; it would be more consistent
# and less surprising to look for an exact-match first.
if [[ -f "${BATS_TEST_DIRNAME}/${file}.bash" ]]; then
file="${BATS_TEST_DIRNAME}/${file}.bash"
elif [[ -f "${BATS_TEST_DIRNAME}/${file}" ]]; then
file="${BATS_TEST_DIRNAME}/${file}"
fi
if [[ ! -f "$file" ]] && ! type -P "$file" >/dev/null; then
printf 'bats: %s does not exist\n' "$file" >&2
exit 1
fi
# Dynamically loaded user file provided outside of Bats.
# Note: 'source "$file" || exit' doesn't work on bash3.2.
# shellcheck disable=SC1090
source "${file}"
}
run() {
local origFlags="$-"
set +eET
local origIFS="$IFS"
# 'output', 'status', 'lines' are global variables available to tests.
# shellcheck disable=SC2034
output="$("$@" 2>&1)"
# shellcheck disable=SC2034
status="$?"
# shellcheck disable=SC2034,SC2206
IFS=$'\n' lines=($output)
IFS="$origIFS"
set "-$origFlags"
}
setup() {
return 0
}
teardown() {
return 0
}
skip() {
# if this is a skip in teardown ...
if [[ -n "${BATS_TEARDOWN_STARTED-}" ]]; then
# ... we want to skip the rest of teardown.
# communicate to bats_exit_trap that the teardown was completed without error
# shellcheck disable=SC2034
BATS_TEARDOWN_COMPLETED=1
# if we are already in the exit trap (e.g. due to previous skip) ...
if [[ "$BATS_TEARDOWN_STARTED" == as-exit-trap ]]; then
# ... we need to do the rest of the tear_down_trap that would otherwise be skipped after the next call to exit
bats_exit_trap
# and then do the exit (at the end of this function)
fi
# if we aren't in exit trap, the normal exit handling should suffice
else
# ... this is either skip in test or skip in setup.
# Following variables are used in bats-exec-test which sources this file
# shellcheck disable=SC2034
BATS_TEST_SKIPPED="${1:-1}"
# shellcheck disable=SC2034
BATS_TEST_COMPLETED=1
fi
exit 0
}
bats_test_begin() {
BATS_TEST_DESCRIPTION="$1"
if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then
printf 'begin %d %s\n' "$BATS_SUITE_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
fi
setup
}
bats_test_function() {
local test_name="$1"
BATS_TEST_NAMES+=("$test_name")
}

@ -0,0 +1,154 @@
#!/usr/bin/env bash
bats_capture_stack_trace() {
local test_file
local funcname
local i
BATS_STACK_TRACE=()
for ((i = 2; i != ${#FUNCNAME[@]}; ++i)); do
# Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby
# calling an exported function erases the test file's BASH_SOURCE entry.
test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}"
funcname="${FUNCNAME[$i]}"
BATS_STACK_TRACE+=("${BASH_LINENO[$((i - 1))]} $funcname $test_file")
if [[ "$test_file" == "$BATS_TEST_SOURCE" ]]; then
case "$funcname" in
"$BATS_TEST_NAME" | setup | teardown | setup_file | teardown_file)
break
;;
esac
fi
done
}
bats_print_stack_trace() {
local frame
local index=1
local count="${#@}"
local filename
local lineno
for frame in "$@"; do
bats_frame_filename "$frame" 'filename'
bats_trim_filename "$filename" 'filename'
bats_frame_lineno "$frame" 'lineno'
if [[ $index -eq 1 ]]; then
printf '# ('
else
printf '# '
fi
local fn
bats_frame_function "$frame" 'fn'
if [[ "$fn" != "$BATS_TEST_NAME" ]]; then
printf "from function \`%s' " "$fn"
fi
if [[ $index -eq $count ]]; then
printf 'in test file %s, line %d)\n' "$filename" "$lineno"
else
printf 'in file %s, line %d,\n' "$filename" "$lineno"
fi
((++index))
done
}
bats_print_failed_command() {
local frame="${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]} - 1]}"
local filename
local lineno
local failed_line
local failed_command
bats_frame_filename "$frame" 'filename'
bats_frame_lineno "$frame" 'lineno'
bats_extract_line "$filename" "$lineno" 'failed_line'
bats_strip_string "$failed_line" 'failed_command'
printf '%s' "# \`${failed_command}' "
if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then
printf 'failed\n'
else
printf 'failed with status %d\n' "$BATS_ERROR_STATUS"
fi
}
bats_frame_lineno() {
printf -v "$2" '%s' "${1%% *}"
}
bats_frame_function() {
local __bff_function="${1#* }"
printf -v "$2" '%s' "${__bff_function%% *}"
}
bats_frame_filename() {
local __bff_filename="${1#* }"
__bff_filename="${__bff_filename#* }"
if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then
__bff_filename="$BATS_TEST_FILENAME"
fi
printf -v "$2" '%s' "$__bff_filename"
}
bats_extract_line() {
local __bats_extract_line_line
local __bats_extract_line_index=0
while IFS= read -r __bats_extract_line_line; do
if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then
printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}"
break
fi
done <"$1"
}
bats_strip_string() {
[[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]
printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}
bats_trim_filename() {
printf -v "$2" '%s' "${1#$BATS_CWD/}"
}
bats_debug_trap() {
# don't update the trace within library functions or we get backtraces from inside traps
if [[ "$1" != $BATS_ROOT/lib/* && "$1" != $BATS_ROOT/libexec/* ]]; then
# The last entry in the stack trace is not useful when en error occured:
# It is either duplicated (kinda correct) or has wrong line number (Bash < 4.4)
# Therefore we capture the stacktrace but use it only after the next debug
# trap fired.
# Expansion is required for empty arrays which otherwise error
BATS_CURRENT_STACK_TRACE=("${BATS_STACK_TRACE[@]+"${BATS_STACK_TRACE[@]}"}")
bats_capture_stack_trace
fi
}
# For some versions of Bash, the `ERR` trap may not always fire for every
# command failure, but the `EXIT` trap will. Also, some command failures may not
# set `$?` properly. See #72 and #81 for details.
#
# For this reason, we call `bats_error_trap` at the very beginning of
# `bats_teardown_trap` (the `DEBUG` trap for the call will fix the stack trace)
# and check the value of `$BATS_TEST_COMPLETED` before taking other actions.
# We also adjust the exit status value if needed.
#
# See `bats_exit_trap` for an additional EXIT error handling case when `$?`
# isn't set properly during `teardown()` errors.
bats_error_trap() {
local status="$?"
if [[ -z "$BATS_TEST_COMPLETED" ]]; then
BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}"
if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
BATS_ERROR_STATUS=1
fi
BATS_STACK_TRACE=("${BATS_CURRENT_STACK_TRACE[@]}")
trap - DEBUG
fi
}

@ -0,0 +1,36 @@
#!/usr/bin/env bash
bats_test_count_validator() {
header_pattern='[0-9]+\.\.[0-9]+'
IFS= read -r header
# repeat the header
printf "%s\n" "$header"
# if we detect a TAP plan
if [[ "$header" =~ $header_pattern ]]; then
# extract the number of tests ...
local expected_number_of_tests="${header:3}"
# ... count the actual number of [not ] oks...
local actual_number_of_tests=0
while IFS= read -r line; do
# forward line
printf "%s\n" "$line"
case "$line" in
'ok '*)
(( ++actual_number_of_tests ))
;;
'not ok'*)
(( ++actual_number_of_tests ))
;;
esac
done
# ... and error if they are not the same
if [[ "${actual_number_of_tests}" != "${expected_number_of_tests}" ]]; then
printf '# bats warning: Executed %s instead of expected %s tests\n' "$actual_number_of_tests" "$expected_number_of_tests"
return 1
fi
else
# forward output unchanged
cat
fi
}

@ -1,13 +1,11 @@
#!/usr/bin/env bash
set -e
version() {
printf 'Bats 1.1.0\n'
}
export BATS_VERSION='1.3.0'
VALID_FORMATTERS="pretty, junit, tap, tap13"
usage() {
version
printf 'Usage: bats [-c] [-r] [-p | -t] <test> [<test> ...]\n'
version() {
printf 'Bats %s\n' "$BATS_VERSION"
}
abort() {
@ -16,26 +14,43 @@ abort() {
exit 1
}
help() {
usage() {
local cmd="${0##*/}"
local line
usage
while read -r line; do
printf '%s\n' "$line"
done <<END_OF_HELP_TEXT
<test> is the path to a Bats test file, or the path to a directory
containing Bats test files.
cat <<HELP_TEXT_HEADER
Usage: ${cmd} [OPTIONS] <tests>
${cmd} [-h | -v]
-c, --count Count the number of test cases without running any tests
-h, --help Display this help message
-p, --pretty Show results in pretty format (default for terminals)
-r, --recursive Include tests in subdirectories
-t, --tap Show results in TAP format
-v, --version Display the version number
HELP_TEXT_HEADER
For more information, see https://github.com/bats-core/bats-core
cat <<'HELP_TEXT_BODY'
<tests> is the path to a Bats test file, or the path to a directory
containing Bats test files (ending with ".bats")
-c, --count Count test cases without running any tests
-f, --filter <regex> Only run tests that match the regular expression
-F, --formatter <type> Switch between formatters: pretty (default),
tap (default w/o term), tap13, junit
-h, --help Display this help message
-j, --jobs <jobs> Number of parallel jobs (requires GNU parallel)
--no-tempdir-cleanup Preserve test output temporary directory
--no-parallelize-across-files
Serialize test file execution instead of running
them in parallel (requires --jobs >1)
--no-parallelize-within-files
Serialize test execution within files instead of
running them in parallel (requires --jobs >1)
--report-formatter <type> Switch between reporters (same options as --formatter)
-o, --output <dir> Directory to write report files
-p, --pretty Shorthand for "--formatter pretty"
-r, --recursive Include tests in subdirectories
-t, --tap Shorthand for "--formatter tap"
-T, --timing Add timing information to tests
-v, --version Display the version number
END_OF_HELP_TEXT
For more information, see https://github.com/bats-core/bats-core
HELP_TEXT_BODY
}
expand_path() {
@ -53,71 +68,197 @@ expand_path() {
printf -v "$result" '%s/%s' "$dirname" "${path##*/}"
}
BATS_LIBEXEC="$(dirname "$(bats_readlinkf "${BASH_SOURCE[0]}")")"
export BATS_LIBEXEC
export BATS_CWD="$PWD"
export BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$"
export PATH="$BATS_ROOT/libexec/bats-core:$PATH"
export BATS_TEST_PATTERN_COMMENT="[[:blank:]]*([^[:blank:]()]+)[[:blank:]]*\(?\)?[[:blank:]]+\{[[:blank:]]+#[[:blank:]]*@test[[:blank:]]*\$"
export BATS_TEST_FILTER=
export PATH="$BATS_LIBEXEC:$PATH"
export BATS_ROOT_PID=$$
if [[ -z "${TMPDIR-}" ]]; then
export BATS_TMPDIR="/tmp"
else
export BATS_TMPDIR="${TMPDIR%/}"
fi
export BATS_RUN_TMPDIR="$BATS_TMPDIR/bats-run-$BATS_ROOT_PID"
options=()
arguments=()
# Unpack single-character options bundled together, e.g. -cr, -pr.
for arg in "$@"; do
if [[ "${arg:0:1}" = "-" ]]; then
if [[ "${arg:1:1}" = "-" ]]; then
options[${#options[*]}]="${arg:2}"
else
index=1
while option="${arg:$index:1}"; do
if [[ -z "$option" ]]; then
break
fi
options[${#options[*]}]="$option"
let index+=1
done
fi
if [[ "$arg" =~ ^-[^-]. ]]; then
index=1
while option="${arg:$((index++)):1}"; do
if [[ -z "$option" ]]; then
break
fi
arguments+=("-$option")
done
else
arguments[${#arguments[*]}]="$arg"
arguments+=("$arg")
fi
shift
done
unset count_flag pretty recursive
count_flag=''
pretty=''
recursive=''
if [[ -z "${CI:-}" && -t 0 && -t 1 ]]; then
pretty=1
set -- "${arguments[@]}"
arguments=()
unset flags recursive formatter_flags
flags=('--dummy-flag') # add a dummy flag to prevent unset varialeb errors on empty array expansion in old bash versions
formatter_flags=('--dummy-flag') # add a dummy flag to prevent unset varialeb errors on empty array expansion in old bash versions
formatter='tap'
report_formatter=''
recursive=
export BATS_TEMPDIR_CLEANUP=1
output=
if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then
formatter='pretty'
fi
if [[ "${#options[@]}" -ne 0 ]]; then
for option in "${options[@]}"; do
case "$option" in
"h" | "help" )
help
exit 0
;;
"v" | "version" )
version
exit 0
;;
"c" | "count" )
count_flag="-c"
;;
"r" | "recursive" )
recursive=1
;;
"t" | "tap" )
pretty=""
while [[ "$#" -ne 0 ]]; do
case "$1" in
-h | --help)
version
usage
exit 0
;;
-v | --version)
version
exit 0
;;
-c | --count)
flags+=('-c')
;;
-f | --filter)
shift
flags+=('-f' "$1")
;;
-F | --formatter)
shift
# allow cat formatter to see extended output but don't advertise to users
if [[ $1 =~ ^(pretty|junit|tap|tap13|cat)$ ]]; then
formatter="$1"
else
printf "Unknown formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
exit 1
fi
;;
--report-formatter)
shift
if [[ $1 =~ ^(pretty|junit|tap|tap13)$ ]]; then
report_formatter="$1"
else
printf "Unknown report formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
exit 1
fi
;;
-o | --output)
shift
output="$1"
;;
-p | --pretty)
formatter='pretty'
;;
-j | --jobs)
shift
flags+=('-j' "$1")
;;
-r | --recursive)
recursive=1
;;
-t | --tap)
formatter='tap'
;;
-T | --timing)
flags+=('-T')
formatter_flags+=('-T')
;;
# this flag is now a no-op, as it is the parallel default
--parallel-preserve-environment)
;;
--no-parallelize-across-files)
flags+=("--no-parallelize-across-files")
;;
--no-parallelize-within-files)
flags+=("--no-parallelize-within-files")
;;
--no-tempdir-cleanup)
BATS_TEMPDIR_CLEANUP=''
;;
--tempdir) # for internal test consumption only!
BATS_RUN_TMPDIR="$2"
shift
;;
-*)
abort "Bad command line option '$1'"
;;
*)
arguments+=("$1")
;;
esac
shift
done
if [[ -d "$BATS_RUN_TMPDIR" ]]; then
printf "Error: BATS_RUN_TMPDIR (%s) already exists\n" "$BATS_RUN_TMPDIR" >&2
printf "Reusing old run directories can lead to unexpected results ... aborting!\n" >&2
exit 1
fi
mkdir -p "$BATS_RUN_TMPDIR"
if [[ -n "$BATS_TEMPDIR_CLEANUP" ]]; then
trap 'rm -rf "$BATS_RUN_TMPDIR"' ERR EXIT
fi
if [[ "$formatter" != "tap" ]]; then
flags+=('-x')
fi
if [[ -n "$report_formatter" && "$report_formatter" != "tap" ]]; then
flags+=('-x')
fi
if [[ "$formatter" == "junit" ]]; then
flags+=('-T')
formatter_flags+=('--base-path' "${arguments[0]}")
fi
if [[ "$report_formatter" == "junit" ]]; then
flags+=('-T')
report_formatter_flags+=('--base-path' "${arguments[0]}")
fi
# if we don't need to filter extended syntax, use the faster formatter
if [[ "$formatter" == tap && -z "$report_formatter" ]]; then
formatter="cat"
fi
if [[ "${#arguments[@]}" -eq 0 ]]; then
abort 'Must specify at least one <test>'
fi
if [[ -n "$report_formatter" ]]; then
# default to the current directory for output
if [[ -z "$output" ]]; then
output=.
fi
case "$report_formatter" in
tap|tap13)
BATS_REPORT_FILE_NAME="report.tap"
;;
"p" | "pretty" )
pretty=1
junit)
BATS_REPORT_FILE_NAME="report.xml"
;;
* )
abort "Bad command line option '-$option'"
pretty)
BATS_REPORT_FILE_NAME="report.log"
;;
esac
done
esac
fi
if [[ "${#arguments[@]}" -eq 0 ]]; then
abort 'Must specify at least one <test>'
if [[ -n "$output" ]]; then
if [[ ! -w "${output}" ]]; then
abort "Output path ${output} is not writeable"
fi
export BATS_REPORT_OUTPUT_PATH="$output"
fi
filenames=()
@ -128,31 +269,26 @@ for filename in "${arguments[@]}"; do
shopt -s nullglob
if [[ "$recursive" -eq 1 ]]; then
while IFS= read -r -d $'\0' file; do
filenames["${#filenames[@]}"]="$file"
done < <(find "$filename" -type f -name "*.bats" -print0 | sort -z)
filenames+=("$file")
done < <(find -L "$filename" -type f -name "*.${BATS_FILE_EXTENSION:-bats}" -print0 | sort -z)
else
for suite_filename in "$filename"/*.bats; do
filenames["${#filenames[@]}"]="$suite_filename"
for suite_filename in "$filename"/*."${BATS_FILE_EXTENSION:-bats}"; do
filenames+=("$suite_filename")
done
fi
shopt -u nullglob
else
filenames["${#filenames[@]}"]="$filename"
filenames+=("$filename")
fi
done
if [[ "${#filenames[@]}" -eq 1 ]]; then
command="bats-exec-test"
else
command="bats-exec-suite"
fi
# shellcheck source=lib/bats-core/validator.bash
source "$BATS_ROOT/lib/bats-core/validator.bash"
set -o pipefail execfail
if [[ -z "$pretty" ]]; then
exec "$command" $count_flag "${filenames[@]}"
if [[ -n "$report_formatter" ]]; then
exec bats-exec-suite "${flags[@]}" "${filenames[@]}" | tee >("bats-format-${report_formatter}" "${report_formatter_flags[@]}" >"${BATS_REPORT_OUTPUT_PATH}/${BATS_REPORT_FILE_NAME}") | bats_test_count_validator | "bats-format-${formatter}" "${formatter_flags[@]}"
else
extended_syntax_flag="-x"
formatter="bats-format-tap-stream"
exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" |
"$formatter"
exec bats-exec-suite "${flags[@]}" "${filenames[@]}" | bats_test_count_validator | "bats-format-${formatter}" "${formatter_flags[@]}"
fi

@ -0,0 +1,262 @@
#!/usr/bin/env bash
set -eET
export flags=()
num_jobs=1
filter=''
extended_syntax=''
while [[ "$#" -ne 0 ]]; do
case "$1" in
-c) ;;
-f)
shift
filter="$1"
flags+=('-f' "$filter")
;;
-j)
shift
num_jobs="$1"
;;
-T)
flags+=('-T')
;;
-x)
flags+=('-x')
extended_syntax=1
;;
--no-parallelize-within-files)
# use singular to allow for users to override in file
BATS_NO_PARALLELIZE_WITHIN_FILE=1
;;
--dummy-flag)
;;
*)
break
;;
esac
shift
done
filename="$1"
TESTS_FILE="$2"
if [[ ! -f "$filename" ]]; then
printf 'Testfile "%s" not found\n' "$filename" >&2
exit 1
fi
BATS_TEST_FILENAME="$filename"
# shellcheck source=lib/bats-core/preprocessing.bash
# shellcheck disable=SC2153
source "$BATS_ROOT/lib/bats-core/preprocessing.bash"
bats_run_setup_file() {
# shellcheck source=lib/bats-core/tracing.bash
# shellcheck disable=SC2153
source "$BATS_ROOT/lib/bats-core/tracing.bash"
# shellcheck source=lib/bats-core/test_functions.bash
# shellcheck disable=SC2153
source "$BATS_ROOT/lib/bats-core/test_functions.bash"
exec 3<&1
BATS_STACK_TRACE=()
# shellcheck disable=2034
BATS_CURRENT_STACK_TRACE=() # used in tracing.bash
# these are defined only to avoid errors when referencing undefined variables down the line
# shellcheck disable=2034
BATS_TEST_NAME= # used in tracing.bash
# shellcheck disable=2034
BATS_TEST_COMPLETED= # used in tracing.bash
BATS_SETUP_FILE_COMPLETED=
BATS_TEARDOWN_FILE_COMPLETED=
# shellcheck disable=2034
BATS_ERROR_STATUS= # used in tracing.bash
trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG
trap 'bats_error_trap' ERR
trap 'bats_file_teardown_trap' EXIT
touch "$BATS_OUT"
# get the setup_file/teardown_file functions for this file (if it has them)
# shellcheck disable=SC1090
source "$BATS_TEST_SOURCE"
setup_file >>"$BATS_OUT" 2>&1
BATS_SETUP_FILE_COMPLETED=1
}
bats_run_teardown_file() {
# avoid running the therdown trap due to errors in teardown_file
trap 'bats_file_exit_trap' EXIT
local status=0
# rely on bats_error_trap to catch failures
teardown_file >>"$BATS_OUT" 2>&1
BATS_TEARDOWN_FILE_COMPLETED=1
}
bats_file_teardown_trap() {
bats_error_trap
local status=0
bats_run_teardown_file
bats_file_exit_trap
}
bats_file_exit_trap() {
trap - ERR EXIT
if [[ -z "$BATS_SETUP_FILE_COMPLETED" || -z "$BATS_TEARDOWN_FILE_COMPLETED" ]]; then
if [[ -z "$BATS_SETUP_FILE_COMPLETED" ]]; then
FAILURE_REASON='setup_file'
else
FAILURE_REASON='teardown_file'
fi
printf "not ok %d %s\n" "$((test_number_in_suite + 1))" "$FAILURE_REASON failed" >&3
bats_print_stack_trace "${BATS_STACK_TRACE[@]}" >&3
bats_print_failed_command >&3
while IFS= read -r line; do
printf "# %s\n" "$line"
done <"$BATS_OUT" >&3
if [[ -n "$line" ]]; then
printf '# %s\n' "$line"
fi
rm -rf "$BATS_OUT"
status=1
fi
exit $status
}
function setup_file() {
return 0
}
function teardown_file() {
return 0
}
bats_forward_output_of_parallel_test() {
local test_number_in_suite=$1
local status=0
wait "$(cat "$output_folder/$test_number_in_suite/pid")" || status=1
cat "$output_folder/$test_number_in_suite/stdout"
cat "$output_folder/$test_number_in_suite/stderr" >&2
return $status
}
bats_is_next_parallel_test_finished() {
local PID
# get the pid of the next potentially finished test
PID=$(cat "$output_folder/$(( test_number_in_suite_of_last_finished_test + 1 ))/pid")
# try to send a signal to this process
# if it fails, the process exited,
# if it succeeds, the process is still running
if kill -0 "$PID" 2>/dev/null; then
return 1
fi
}
# prints output from all tests in the order they were started
# $1 == "blocking": wait for a test to finish before printing
# != "blocking": abort printing, when a test has not finished
bats_forward_output_for_parallel_tests() {
local status=0
# was the next test already started?
while [[ $(( test_number_in_suite_of_last_finished_test + 1 )) -le $test_number_in_suite ]]; do
# if we are okay with waiting or if the test has already been finished
if [[ "$1" == "blocking" ]] || bats_is_next_parallel_test_finished ; then
(( ++test_number_in_suite_of_last_finished_test ))
bats_forward_output_of_parallel_test "$test_number_in_suite_of_last_finished_test" || status=1
else
# non-blocking and the process has not finished -> abort the printing
break
fi
done
return $status
}
bats_run_tests_in_parallel() {
local output_folder="$BATS_RUN_TMPDIR/parallel_output"
local status=0
mkdir -p "$output_folder"
# shellcheck source=lib/bats-core/semaphore.bash
source "$BATS_ROOT/lib/bats-core/semaphore.bash"
# the test_number_in_file is not yet incremented -> one before the next test to run
local test_number_in_suite_of_last_finished_test="$test_number_in_suite" # stores which test was printed last
for test_name in "${tests_to_run[@]}"; do
# Only handle non-empty lines
if [[ $test_name ]]; then
((++test_number_in_suite))
((++test_number_in_file))
mkdir -p "$output_folder/$test_number_in_suite"
bats_semaphore_run "$output_folder/$test_number_in_suite" \
"$BATS_LIBEXEC/bats-exec-test" "${flags[@]}" "$filename" "$test_name" "$test_number_in_suite" "$test_number_in_file" \
> "$output_folder/$test_number_in_suite/pid"
fi
# print results early to get interactive feedback
bats_forward_output_for_parallel_tests non-blocking || status=1 # ignore if we did not finish yet
done
bats_forward_output_for_parallel_tests blocking || status=1
return $status
}
bats_run_tests() {
status=0
tests_to_run=()
local line_number=0
# the global test number must be visible to traps -> not local
first_test_number_in_suite=''
while read -r test_line; do
# check if the line begins with filename
# filename might contain some hard to parse characters,
# use simple string operations to work around that issue
if [[ "$filename" == "${test_line::${#filename}}" ]]; then
# get the rest of the line without the separator \t
test_name=${test_line:$((1 + ${#filename} ))}
tests_to_run+=("$test_name")
# save the first test's number for later iteration
# this assumes that tests for a file are stored consecutive in the file!
if [[ -z "$first_test_number_in_suite" ]]; then
first_test_number_in_suite=$line_number
fi
fi
((++line_number))
done <"$TESTS_FILE"
test_number_in_suite="$first_test_number_in_suite"
test_number_in_file=0
if [[ "$num_jobs" != 1 && "${BATS_NO_PARALLELIZE_WITHIN_FILE-False}" == False ]]; then
export BATS_SEMAPHORE_NUMBER_OF_SLOTS="$num_jobs"
bats_run_tests_in_parallel "$BATS_RUN_TMPDIR/parallel_output" || status=1
else
for test_name in "${tests_to_run[@]}"; do
# Only handle non-empty lines
if [[ $test_name ]]; then
((++test_number_in_suite))
((++test_number_in_file))
# deal with empty flags to avoid spurious "unbound variable" errors on Bash 4.3 and lower
if [[ "${#flags[@]}" -gt 0 ]]; then
"$BATS_LIBEXEC/bats-exec-test" "${flags[@]}" "$filename" "$test_name" "$test_number_in_suite" "$test_number_in_file" || status=1
else
"$BATS_LIBEXEC/bats-exec-test" "$filename" "$test_name" "$test_number_in_suite" "$test_number_in_file" || status=1
fi
fi
done
fi
export status
}
if [[ -n "$extended_syntax" ]]; then
printf "suite %s\n" "$filename"
fi
bats_preprocess_source "$filename"
bats_run_setup_file
bats_run_tests
bats_run_teardown_file
exit $status

@ -1,63 +1,146 @@
#!/usr/bin/env bash
set -e
count_only_flag=""
if [[ "$1" = "-c" ]]; then
count_only_flag=1
shift
fi
count_only_flag=''
filter=''
num_jobs=${BATS_NUMBER_OF_PARALLEL_JOBS-1}
have_gnu_parallel=
bats_parallel_args=()
bats_no_parallelize_across_files=${BATS_NO_PARALLELIZE_ACROSS_FILES-}
bats_no_parallelize_within_files=
flags=('--dummy-flag') # add a dummy flag to prevent unset varialeb errors on empty array expansion in old bash versions
abort() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
extended_syntax_flag=""
if [[ "$1" = "-x" ]]; then
extended_syntax_flag="-x"
while [[ "$#" -ne 0 ]]; do
case "$1" in
-c)
count_only_flag=1
;;
-f)
shift
filter="$1"
flags+=('-f' "$filter")
;;
-j)
shift
num_jobs="$1"
flags+=('-j' "$num_jobs")
;;
-T)
flags+=('-T')
;;
-x)
flags+=('-x')
;;
--no-parallelize-across-files)
bats_no_parallelize_across_files=1
;;
--no-parallelize-within-files)
bats_no_parallelize_within_files=1
flags+=("--no-parallelize-within-files")
;;
--dummy-flag)
;;
*)
break
;;
esac
shift
done
if (type -p parallel &>/dev/null); then
# shellcheck disable=SC2034
have_gnu_parallel=1
elif [[ "$num_jobs" != 1 && -z "$bats_no_parallelize_across_files" ]]; then
abort "Cannot execute \"${num_jobs}\" jobs without GNU parallel"
exit 1
fi
trap "kill 0; exit 1" int
trap 'kill 0; exit 1' INT
# create a file that contains all (filtered) tests to run from all files
TESTS_LIST_FILE="${BATS_RUN_TMPDIR}/test_list_file.txt"
count=0
all_tests=()
for filename in "$@"; do
while IFS= read -r line; do
if [[ "$line" =~ $BATS_TEST_PATTERN ]]; then
let count+=1
if [[ ! -f "$filename" ]]; then
abort "Test file \"${filename}\" does not exist"
fi
test_names=()
test_dupes=()
while read -r line; do
if [[ ! "$line" =~ ^bats_test_function\ ]]; then
continue
fi
line="${line%$'\r'}"
line="${line#* }"
test_line=$(printf "%s\t%s" "$filename" "$line")
all_tests+=("$test_line")
printf "%s\n" "$test_line" >>"$TESTS_LIST_FILE"
# avoid unbound variable errors on empty array expansion with old bash versions
if [[ ${#test_names[@]} -gt 0 && " ${test_names[*]} " == *" $line "* ]]; then
test_dupes+=("$line")
continue
fi
done <"$filename"
test_names+=("$line")
done < <(BATS_TEST_FILTER="$filter" bats-preprocess "$filename")
if [[ "${#test_dupes[@]}" -ne 0 ]]; then
abort "Duplicate test name(s) in file \"${filename}\": ${test_dupes[*]}"
fi
done
test_count="${#all_tests[@]}"
if [[ -n "$count_only_flag" ]]; then
printf '%d\n' "$count"
printf '%d\n' "${test_count}"
exit
fi
printf '1..%d\n' "$count"
if [[ -n "$bats_no_parallelize_across_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then
abort "The flag --no-parallelize-across-files requires at least --jobs 2"
exit 1
fi
if [[ -n "$bats_no_parallelize_within_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then
abort "The flag --no-parallelize-across-files requires at least --jobs 2"
exit 1
fi
status=0
offset=0
for filename in "$@"; do
index=0
{
IFS= read -r # 1..n
while IFS= read -r line; do
case "$line" in
"begin "* )
let index+=1
printf '%s\n' "${line/ $index / $(($offset + $index)) }"
;;
"ok "* | "not ok "* )
if [[ -z "$extended_syntax_flag" ]]; then
let index+=1
fi
printf '%s\n' "${line/ $index / $(($offset + $index)) }"
if [[ "${line:0:6}" == "not ok" ]]; then
status=1
fi
;;
* )
printf '%s\n' "$line"
;;
esac
done
} < <( bats-exec-test $extended_syntax_flag "$filename" )
offset=$(($offset + $index))
done
printf '1..%d\n' "${test_count}"
# No point on continuing if there's no tests.
if [[ "${test_count}" == 0 ]]; then
exit
fi
# Deduplicate filenames (without reordering) to avoid running duplicate tests n by n times.
# (see https://github.com/bats-core/bats-core/issues/329)
# If a file was specified multiple times, we already got it repeatedly in our TESTS_LIST_FILE.
# Thus, it suffices to bats-exec-file it once to run all repeated tests on it.
IFS=$'\n' read -d '' -r -a BATS_UNIQUE_TEST_FILENAMES < <(printf "%s\n" "$@"| nl | sort -k 2 | uniq -f 1 | sort -n | cut -f 2-) || true
if [[ "$num_jobs" -gt 1 ]] && [[ -z "$bats_no_parallelize_across_files" ]]; then
# run files in parallel to get the maximum pool of parallel tasks
if [[ ${#flags[@]} -eq 0 ]]; then
# if there are no flags, our quoting below would keep an empty arg, which is wrong
parallel "${bats_parallel_args[@]}" --keep-order --jobs "$num_jobs" bats-exec-file "{}" "$TESTS_LIST_FILE" ::: "${BATS_UNIQUE_TEST_FILENAMES[@]}" 2>&1 || status=1
else
# shellcheck disable=SC2086,SC2068
# we need to handle the quoting of ${flags[@]} ourselves,
# because parallel can only quote it as one
parallel --keep-order --jobs "$num_jobs" bats-exec-file "$(printf "%q " "${flags[@]}")" "{}" "$TESTS_LIST_FILE" ::: "${BATS_UNIQUE_TEST_FILENAMES[@]}" 2>&1 || status=1
fi
else
for filename in "${BATS_UNIQUE_TEST_FILENAMES[@]}"; do
bats-exec-file "${flags[@]}" "$filename" "${TESTS_LIST_FILE}" || status=1
done
fi
exit "$status"

@ -1,259 +1,67 @@
#!/usr/bin/env bash
set -eET
BATS_COUNT_ONLY=""
if [[ "$1" = "-c" ]]; then
BATS_COUNT_ONLY=1
# Variables used in other scripts.
BATS_COUNT_ONLY=''
BATS_TEST_FILTER=''
BATS_ENABLE_TIMING=''
BATS_EXTENDED_SYNTAX=''
while [[ "$#" -ne 0 ]]; do
case "$1" in
-c)
# shellcheck disable=SC2034
BATS_COUNT_ONLY=1
;;
-f)
shift
# shellcheck disable=SC2034
BATS_TEST_FILTER="$1"
;;
-T)
BATS_ENABLE_TIMING='-T'
BATS_PERFORM_TEST_CMD+=('-T')
;;
-x)
# shellcheck disable=SC2034
BATS_EXTENDED_SYNTAX='-x'
;;
*)
break
;;
esac
shift
fi
BATS_EXTENDED_SYNTAX=""
if [[ "$1" = "-x" ]]; then
BATS_EXTENDED_SYNTAX="$1"
shift
fi
done
BATS_TEST_FILENAME="$1"
shift
if [[ -z "$BATS_TEST_FILENAME" ]]; then
printf 'usage: bats-exec-test <filename>\n' >&2
exit 1
elif [[ ! -f "$BATS_TEST_FILENAME" ]]; then
printf 'bats: %s does not exist\n' "$BATS_TEST_FILENAME" >&2
exit 1
else
shift
fi
BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}"
BATS_TEST_NAMES=()
load() {
local name="$1"
local filename
if [[ "${name:0:1}" = "/" ]]; then
filename="${name}"
else
filename="$BATS_TEST_DIRNAME/${name}.bash"
fi
if [[ ! -f "$filename" ]]; then
printf 'bats: %s does not exist\n' "$filename" >&2
exit 1
fi
source "${filename}"
}
run() {
local origFlags="$-"
set +eET
local origIFS="$IFS"
output="$("$@" 2>&1)"
status="$?"
IFS=$'\n' lines=($output)
IFS="$origIFS"
set "-$origFlags"
}
setup() {
return 0
}
teardown() {
return 0
}
BATS_TEST_SKIPPED=''
skip() {
BATS_TEST_SKIPPED="${1:-1}"
BATS_TEST_COMPLETED=1
exit 0
}
bats_test_begin() {
BATS_TEST_DESCRIPTION="$1"
if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then
printf 'begin %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
fi
setup
}
bats_test_function() {
local test_name="$1"
BATS_TEST_NAMES+=("$test_name")
}
BATS_CURRENT_STACK_TRACE=()
BATS_PREVIOUS_STACK_TRACE=()
BATS_ERROR_STACK_TRACE=()
bats_capture_stack_trace() {
if [[ "${#BATS_CURRENT_STACK_TRACE[@]}" -ne 0 ]]; then
BATS_PREVIOUS_STACK_TRACE=("${BATS_CURRENT_STACK_TRACE[@]}")
fi
BATS_CURRENT_STACK_TRACE=()
local test_pattern=" $BATS_TEST_NAME $BATS_TEST_SOURCE"
local setup_pattern=" setup $BATS_TEST_SOURCE"
local teardown_pattern=" teardown $BATS_TEST_SOURCE"
local source_file
local frame
local i
for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do
# Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby
# calling an exported function erases the test file's BASH_SOURCE entry.
source_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}"
frame="${BASH_LINENO[$((i-1))]} ${FUNCNAME[$i]} $source_file"
BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame"
if [[ "$frame" = *"$test_pattern" || \
"$frame" = *"$setup_pattern" || \
"$frame" = *"$teardown_pattern" ]]; then
break
fi
done
bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}" 'BATS_SOURCE'
bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}" 'BATS_LINENO'
}
bats_print_stack_trace() {
local frame
local index=1
local count="${#@}"
local filename
local lineno
for frame in "$@"; do
bats_frame_filename "$frame" 'filename'
bats_trim_filename "$filename" 'filename'
bats_frame_lineno "$frame" 'lineno'
if [[ $index -eq 1 ]]; then
printf '# ('
else
printf '# '
fi
local fn
bats_frame_function "$frame" 'fn'
if [[ "$fn" != "$BATS_TEST_NAME" ]]; then
printf "from function \`%s' " "$fn"
fi
if [[ $index -eq $count ]]; then
printf 'in test file %s, line %d)\n' "$filename" "$lineno"
else
printf 'in file %s, line %d,\n' "$filename" "$lineno"
fi
let index+=1
done
}
bats_print_failed_command() {
local frame="$1"
local status="$2"
local filename
local lineno
local failed_line
local failed_command
bats_frame_filename "$frame" 'filename'
bats_frame_lineno "$frame" 'lineno'
bats_extract_line "$filename" "$lineno" 'failed_line'
bats_strip_string "$failed_line" 'failed_command'
printf '%s' "# \`${failed_command}' "
if [[ $status -eq 1 ]]; then
printf 'failed\n'
else
printf 'failed with status %d\n' "$status"
fi
}
bats_frame_lineno() {
printf -v "$2" '%s' "${1%% *}"
}
bats_frame_function() {
local __bff_function="${1#* }"
printf -v "$2" '%s' "${__bff_function%% *}"
}
bats_frame_filename() {
local __bff_filename="${1#* }"
__bff_filename="${__bff_filename#* }"
if [[ "$__bff_filename" = "$BATS_TEST_SOURCE" ]]; then
__bff_filename="$BATS_TEST_FILENAME"
fi
printf -v "$2" '%s' "$__bff_filename"
}
bats_extract_line() {
local __bats_extract_line_line
local __bats_extract_line_index=0
while IFS= read -r __bats_extract_line_line; do
if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then
printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}"
break
fi
done <"$1"
}
bats_strip_string() {
[[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]
printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}
bats_trim_filename() {
printf -v "$2" '%s' "${1#$BATS_CWD/}"
}
bats_debug_trap() {
if [[ "$BASH_SOURCE" != "$1" ]]; then
bats_capture_stack_trace
fi
}
# For some versions of Bash, the `ERR` trap may not always fire for every
# command failure, but the `EXIT` trap will. Also, some command failures may not
# set `$?` properly. See #72 and #81 for details.
#
# For this reason, we call `bats_error_trap` at the very beginning of
# `bats_teardown_trap` (the `DEBUG` trap for the call will move
# `BATS_CURRENT_STACK_TRACE` to `BATS_PREVIOUS_STACK_TRACE`) and check the value
# of `$BATS_TEST_COMPLETED` before taking other actions. We also adjust the exit
# status value if needed.
#
# See `bats_exit_trap` for an additional EXIT error handling case when `$?`
# isn't set properly during `teardown()` errors.
bats_error_trap() {
local status="$?"
if [[ -z "$BATS_TEST_COMPLETED" ]]; then
BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}"
if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
BATS_ERROR_STATUS=1
fi
BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" )
trap - debug
fi
}
# load the test helper functions like `load` or `run` that are needed to run a (preprocessed) .bats file without bash errors
# shellcheck source=lib/bats-core/test_functions.bash disable=SC2153
source "$BATS_ROOT/lib/bats-core/test_functions.bash"
# shellcheck source=lib/bats-core/tracing.bash disable=SC2153
source "$BATS_ROOT/lib/bats-core/tracing.bash"
bats_teardown_trap() {
bats_error_trap
local status=0
# mark the start of this function to distinguish where skip is called
# parameter 1 will signify the reason why this function was called
# this is used to identify when this is called as exit trap function
BATS_TEARDOWN_STARTED=${1:-1}
teardown >>"$BATS_OUT" 2>&1 || status="$?"
if [[ $status -eq 0 ]]; then
BATS_TEARDOWN_COMPLETED=1
elif [[ -n "$BATS_TEST_COMPLETED" ]]; then
BATS_ERROR_STATUS="$status"
BATS_ERROR_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" )
fi
bats_exit_trap
@ -263,17 +71,22 @@ bats_exit_trap() {
local line
local status
local skipped=''
trap - err exit
trap - ERR EXIT
if [[ -n "$BATS_TEST_SKIPPED" ]]; then
skipped=" # skip"
skipped=' # skip'
if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then
skipped+=" $BATS_TEST_SKIPPED"
fi
fi
BATS_TEST_TIME=''
if [[ -z "${skipped}" && -n "$BATS_ENABLE_TIMING" ]]; then
BATS_TEST_TIME=" in "$(( $(get_mills_since_epoch) - BATS_TEST_START_TIME ))"ms"
fi
if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" ]]; then
if [[ "${#BATS_ERROR_STACK_TRACE[@]}" -eq 0 ]]; then
if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
# For some versions of bash, `$?` may not be set properly for some error
# conditions before triggering the EXIT trap directly (see #72 and #81).
# Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such
@ -283,24 +96,22 @@ bats_exit_trap() {
# If instead the test fails, and the `teardown()` error happens while
# `bats_teardown_trap` runs as the EXIT trap, the test will fail with no
# output, since there's no way to reach the `bats_exit_trap` call.
BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" )
BATS_STACK_TRACE=("${BATS_CURRENT_STACK_TRACE[@]}")
BATS_ERROR_STATUS=1
fi
printf 'not ok %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3
bats_print_stack_trace "${BATS_ERROR_STACK_TRACE[@]}" >&3
bats_print_failed_command \
"${BATS_ERROR_STACK_TRACE[${#BATS_ERROR_STACK_TRACE[@]}-1]}" \
"$BATS_ERROR_STATUS" >&3
printf 'not ok %d %s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_DESCRIPTION}${BATS_TEST_TIME}" >&3
bats_print_stack_trace "${BATS_STACK_TRACE[@]}" >&3
bats_print_failed_command >&3
while IFS= read -r line; do
printf '# %s\n' "$line"
printf '# %s\n' "$line" || break # avoid feedback loop when errors are redirected into BATS_OUT (see #353)
done <"$BATS_OUT" >&3
if [[ -n "$line" ]]; then
printf '# %s\n' "$line"
fi
status=1
else
printf 'ok %d %s%s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" \
printf 'ok %d %s%s\n' "$BATS_SUITE_TEST_NUMBER" "${BATS_TEST_DESCRIPTION}${BATS_TEST_TIME}" \
"$skipped" >&3
status=0
fi
@ -309,113 +120,60 @@ bats_exit_trap() {
exit "$status"
}
bats_perform_tests() {
printf '1..%d\n' "$#"
test_number=1
status=0
for test_name in "$@"; do
if ! "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" \
"$test_number"; then
status=1
fi
let test_number+=1
done
exit "$status"
get_mills_since_epoch() {
local ms_since_epoch
ms_since_epoch=$(date +%s%N)
if [[ "$ms_since_epoch" == *N || "${#ms_since_epoch}" -lt 19 ]]; then
ms_since_epoch=$(( $(date +%s) * 1000 ))
else
ms_since_epoch=$(( ms_since_epoch / 1000000 ))
fi
printf "%d\n" "$ms_since_epoch"
}
bats_perform_test() {
BATS_TEST_NAME="$1"
if declare -F "$BATS_TEST_NAME" >/dev/null; then
BATS_TEST_NUMBER="$2"
if [[ -z "$BATS_TEST_NUMBER" ]]; then
printf '1..1\n'
BATS_TEST_NUMBER=1
fi
BATS_TEST_COMPLETED=""
BATS_TEARDOWN_COMPLETED=""
BATS_ERROR_STATUS=""
trap "bats_debug_trap \"\$BASH_SOURCE\"" debug
trap "bats_error_trap" err
trap "bats_teardown_trap" exit
"$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1
BATS_TEST_COMPLETED=1
trap "bats_exit_trap" exit
bats_teardown_trap
export BATS_TEST_NAME="$1"
export BATS_SUITE_TEST_NUMBER="$2"
export BATS_TEST_NUMBER="$3"
else
if ! declare -F "$BATS_TEST_NAME" &>/dev/null; then
printf "bats: unknown test name \`%s'\n" "$BATS_TEST_NAME" >&2
exit 1
fi
}
if [[ -z "$TMPDIR" ]]; then
BATS_TMPDIR="/tmp"
else
BATS_TMPDIR="${TMPDIR%/}"
fi
BATS_TMPNAME="$BATS_TMPDIR/bats.$$"
BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID"
BATS_OUT="${BATS_TMPNAME}.out"
bats_preprocess_source() {
BATS_TEST_SOURCE="${BATS_TMPNAME}.src"
. bats-preprocess <<< "$(< "$BATS_TEST_FILENAME")"$'\n' > "$BATS_TEST_SOURCE"
trap "bats_cleanup_preprocessed_source" err exit
trap "bats_cleanup_preprocessed_source; exit 1" int
bats_detect_duplicate_test_case_names
}
bats_cleanup_preprocessed_source() {
rm -f "$BATS_TEST_SOURCE"
}
# Some versions of Bash will reset BASH_LINENO to the first line of the
# function when the ERR trap fires. All versions of Bash appear to reset it
# on an unbound variable access error. bats_debug_trap will fire both before
# the offending line is executed, and when the error is triggered.
# Consequently, we use `BATS_CURRENT_STACK_TRACE` recorded by the
# first call to bats_debug_trap, _before_ the ERR trap or unbound variable
# access fires.
BATS_STACK_TRACE=()
BATS_CURRENT_STACK_TRACE=()
bats_detect_duplicate_test_case_names() {
local test_names=()
local test_dupes=()
local line
BATS_TEST_COMPLETED=
BATS_TEST_SKIPPED=
BATS_TEARDOWN_COMPLETED=
BATS_ERROR_STATUS=
trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG
trap 'bats_error_trap' ERR
# mark this call as trap call
trap 'bats_teardown_trap as-exit-trap' EXIT
while read -r line; do
if [[ ! "$line" =~ ^bats_test_function\ ]]; then
continue
fi
line="${line%$'\r'}"
line="${line#* }"
BATS_TEST_START_TIME=$(get_mills_since_epoch)
"$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1
if [[ " ${test_names[*]} " == *" $line "* ]]; then
test_dupes+=("$line")
continue
fi
test_names+=("$line")
done <"$BATS_TEST_SOURCE"
if [[ "${#test_dupes[@]}" -ne 0 ]]; then
printf 'bats warning: duplicate test name(s) in %s: %s\n' \
"$BATS_TEST_FILENAME" "${test_dupes[*]}" >&2
fi
BATS_TEST_COMPLETED=1
trap 'bats_exit_trap' EXIT
bats_teardown_trap "" # pass empty parameter to signify call outside trap
}
bats_evaluate_preprocessed_source() {
if [[ -z "$BATS_TEST_SOURCE" ]]; then
BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src"
fi
source "$BATS_TEST_SOURCE"
}
# shellcheck source=lib/bats-core/preprocessing.bash
source "$BATS_ROOT/lib/bats-core/preprocessing.bash"
exec 3<&1
if [[ "$#" -eq 0 ]]; then
bats_preprocess_source
bats_evaluate_preprocessed_source
if [[ -n "$BATS_COUNT_ONLY" ]]; then
printf '%d\n' "${#BATS_TEST_NAMES[@]}"
else
bats_perform_tests "${BATS_TEST_NAMES[@]}"
fi
else
bats_evaluate_preprocessed_source
bats_perform_test "$@"
fi
# Run the given test.
bats_evaluate_preprocessed_source
bats_perform_test "$@"

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
cat

@ -0,0 +1,253 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_PATH=.
while [[ "$#" -ne 0 ]]; do
case "$1" in
--base-path)
shift
# the relative path root to use for reporting filenames
# this is mainly intended for suite mode, where this will be the suite root folder
BASE_PATH="$1"
# use the containing directory when --base-path is a file
if [[ ! -d "$BASE_PATH" ]]; then
BASE_PATH="$(dirname "$BASE_PATH")"
fi
# get the absolute path
BASE_PATH="$(cd "$BASE_PATH"; pwd)"
# ensure the path ends with / to strip that later on
if [[ "${BASE_PATH}" != *"/" ]]; then
BASE_PATH="$BASE_PATH/"
fi
;;
esac
shift
done
init_suite() {
suite_test_exec_time=0
# since we have to print the suite header before its contents but we don't know the contents before the header,
# we have to buffer the contents
_suite_buffer=""
test_result_state="" # declare for the first flush, when no test has been encountered
}
_buffer_log=
init_file() {
file_count=0
file_failures=0
file_skipped=0
file_exec_time=0
test_exec_time=0
_buffer=""
_buffer_log=""
_system_out_log=""
test_result_state="" # mark that no test has run in this file so far
}
host() {
local hostname="${HOST:-}"
[[ -z "$hostname" ]] && hostname="${HOSTNAME:-}"
[[ -z "$hostname" ]] && hostname="$(uname -n)"
[[ -z "$hostname" ]] && hostname="$(hostname -f)"
echo "$hostname"
}
# convert $1 (time in milliseconds) to seconds
milliseconds_to_seconds() {
# we cannot rely on having bc for this calculation
full_seconds=$(($1 / 1000))
remaining_milliseconds=$(($1 % 1000))
if [[ $remaining_milliseconds -eq 0 ]]; then
printf "%d" "$full_seconds"
else
printf "%d.%03d" "$full_seconds" "$remaining_milliseconds"
fi
}
suite_header() {
printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<testsuites time=\"%s\">\n" "$(milliseconds_to_seconds "${suite_test_exec_time}")"
}
file_header() {
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S")
printf "<testsuite name=\"%s\" tests=\"%s\" failures=\"%s\" errors=\"0\" skipped=\"%s\" time=\"%s\" timestamp=\"%s\" hostname=\"%s\">\n" \
"$(xml_escape "${class}")" "${file_count}" "${file_failures}" "${file_skipped}" "$(milliseconds_to_seconds "${file_exec_time}")" "${timestamp}" "$(host)"
}
file_footer() {
printf "</testsuite>\n"
}
suite_footer() {
printf "</testsuites>\n"
}
print_test_case() {
if [[ "$test_result_state" == ok && -z "$_system_out_log" && -z "$_buffer_log" ]]; then
# pass and no output can be shortened
printf " <testcase classname=\"%s\" name=\"%s\" time=\"%s\" />\n" "$(xml_escape "${class}")" "$(xml_escape "${name}")" "$(milliseconds_to_seconds "${test_exec_time}")"
else
printf " <testcase classname=\"%s\" name=\"%s\" time=\"%s\">\n" "$(xml_escape "${class}")" "$(xml_escape "${name}")" "$(milliseconds_to_seconds "${test_exec_time}")"
if [[ -n "$_system_out_log" ]]; then
printf " <system-out>%s</system-out>\n" "$(xml_escape "${_system_out_log}")"
fi
if [[ -n "$_buffer_log" || "$test_result_state" == not_ok ]]; then
printf " <failure type=\"failure\">%s</failure>\n" "$(xml_escape "${_buffer_log}")"
fi
if [[ "$test_result_state" == skipped ]]; then
printf " <skipped>%s</skipped>\n" "$(xml_escape "$test_skip_message")"
fi
printf " </testcase>\n"
fi
}
xml_escape() {
output=${1//&/&amp;}
output=${output//</&lt;}
output=${output//>/&gt;}
output=${output//'"'/&quot;}
output=${output//\'/&#39;}
local CONTROL_CHAR=$'\033'
output="${output//$CONTROL_CHAR/&#27;}"
printf "%s" "$output"
}
suite_buffer() {
local output
output="$("$@"; printf "x")" # use x marker to avoid losing trailing newlines
_suite_buffer="${_suite_buffer}${output%x}"
}
suite_flush() {
echo -n "${_suite_buffer}"
_suite_buffer=""
}
buffer() {
local output
output="$("$@"; printf "x")" # use x marker to avoid losing trailing newlines
_buffer="${_buffer}${output%x}"
}
flush() {
echo -n "${_buffer}"
_buffer=""
}
log() {
if [[ -n "$_buffer_log" ]]; then
_buffer_log="${_buffer_log}
$1"
else
_buffer_log="$1"
fi
}
flush_log() {
if [[ -n "$test_result_state" ]]; then
buffer print_test_case
fi
_buffer_log=""
_system_out_log=""
}
log_system_out() {
if [[ -n "$_system_out_log" ]]; then
_system_out_log="${_system_out_log}
$1"
else
_system_out_log="$1"
fi
}
finish_file() {
if [[ "${class-JUNIT_FORMATTER_NO_FILE_ENCOUNTERED}" != JUNIT_FORMATTER_NO_FILE_ENCOUNTERED ]]; then
file_header
printf "%s\n" "${_buffer}"
file_footer
fi
}
finish_suite() {
flush_log
suite_header
suite_flush
finish_file # must come after suite flush to not print the last file before the others
suite_footer
}
# shellcheck source=lib/bats-core/formatter.bash
source "$BATS_ROOT/lib/bats-core/formatter.bash"
bats_tap_stream_plan() { # <number of tests>
:
}
init_suite
trap finish_suite EXIT
bats_tap_stream_begin() { # <test index> <test name>
flush_log
# set after flushing to avoid overriding name of test
name="$2"
}
bats_tap_stream_ok() { # [--duration <milliseconds] <test index> <test name>
if [[ "$1" == "--duration" ]]; then
test_exec_time="${BASH_REMATCH[1]}"
else
test_exec_time=0
fi
((file_count += 1))
test_result_state='ok'
file_exec_time="$((file_exec_time + test_exec_time))"
suite_test_exec_time=$((suite_test_exec_time + test_exec_time))
}
bats_tap_stream_skipped() { # <test index> <test name> <skip reason>
((file_count += 1))
((file_skipped += 1))
test_result_state='skipped'
test_exec_time=0
test_skip_message="$3"
}
bats_tap_stream_not_ok() { # [--duration <milliseconds>] <test index> <test name>
((file_count += 1))
((file_failures += 1))
if [[ "$1" == "--duration" ]]; then
test_exec_time="${BASH_REMATCH[1]}"
else
test_exec_time=0
fi
test_result_state=not_ok
file_exec_time="$((file_exec_time + test_exec_time))"
suite_test_exec_time=$((suite_test_exec_time + test_exec_time))
}
bats_tap_stream_comment() { # <comment text without leading '# '> <scope>
if [[ "$2" == begin ]]; then
# everything that happens between begin and [not] ok is FD3 output from the test
log_system_out "$1"
else
# everything else is considered error output
log "$1"
fi
}
bats_tap_stream_suite() { # <file name>
flush_log
suite_buffer finish_file
init_file
class="$(remove_prefix "$BASE_PATH" "$1")"
}
bats_tap_stream_unknown() { # <full line>
:
}
bats_parse_internal_extended_tap

@ -0,0 +1,235 @@
#!/usr/bin/env bash
set -e
while [[ "$#" -ne 0 ]]; do
case "$1" in
-T)
BATS_ENABLE_TIMING="-T"
;;
esac
shift
done
update_count_column_width() {
count_column_width=$((${#count} * 2 + 2))
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
# additional space for ' in %s sec'
count_column_width=$((count_column_width + ${#SECONDS} + 8))
fi
# also update dependent value
update_count_column_left
}
update_screen_width() {
screen_width="$(tput cols)"
# also update dependent value
update_count_column_left
}
update_count_column_left() {
count_column_left=$((screen_width - count_column_width))
}
trap update_screen_width WINCH
update_screen_width
begin() {
go_to_column 0
update_count_column_width
buffer_with_truncation $((count_column_left - 1)) ' %s' "$name"
clear_to_end_of_line
go_to_column $count_column_left
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
buffer "%${#count}s/${count} in %s sec" "$index" "$SECONDS"
else
buffer "%${#count}s/${count}" "$index"
fi
go_to_column 1
}
pass() {
go_to_column 0
buffer ' ✓ %s' "$name"
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
set_color 2
buffer ' [%s]' "$1"
fi
advance
}
skip() {
local reason="$1"
if [[ -n "$reason" ]]; then
reason=": $reason"
fi
go_to_column 0
buffer ' - %s (skipped%s)' "$name" "$reason"
advance
}
fail() {
go_to_column 0
set_color 1 bold
buffer ' ✗ %s' "$name"
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
set_color 2
buffer ' [%s]' "$1"
fi
advance
}
log() {
set_color 1
buffer ' %s\n' "$1"
clear_color
}
summary() {
buffer '\n%d test' "$count"
if [[ "$count" -ne 1 ]]; then
buffer 's'
fi
buffer ', %d failure' "$failures"
if [[ "$failures" -ne 1 ]]; then
buffer 's'
fi
if [[ "$skipped" -gt 0 ]]; then
buffer ', %d skipped' "$skipped"
fi
not_run=$((count - passed - failures - skipped))
if [[ "$not_run" -gt 0 ]]; then
buffer ', %d not run' "$not_run"
fi
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
buffer " in $SECONDS seconds"
fi
buffer '\n'
}
buffer_with_truncation() {
local width="$1"
shift
local string
# shellcheck disable=SC2059
printf -v 'string' -- "$@"
if [[ "${#string}" -gt "$width" ]]; then
buffer '%s...' "${string:0:$((width - 4))}"
else
buffer '%s' "$string"
fi
}
go_to_column() {
local column="$1"
buffer '\x1B[%dG' $((column + 1))
}
clear_to_end_of_line() {
buffer '\x1B[K'
}
advance() {
clear_to_end_of_line
buffer '\n'
clear_color
}
set_color() {
local color="$1"
local weight=22
if [[ "$2" == 'bold' ]]; then
weight=1
fi
buffer '\x1B[%d;%dm' "$((30 + color))" "$weight"
}
clear_color() {
buffer '\x1B[0m'
}
_buffer=
buffer() {
local content
# shellcheck disable=SC2059
printf -v content -- "$@"
_buffer+="$content"
}
flush() {
printf '%s' "$_buffer"
_buffer=
}
finish() {
flush
printf '\n'
}
trap finish EXIT
# shellcheck source=lib/bats-core/formatter.bash
source "$BATS_ROOT/lib/bats-core/formatter.bash"
bats_tap_stream_plan() {
count="$1"
index=0
passed=0
failures=0
skipped=0
name=
update_count_column_width
}
bats_tap_stream_begin() {
index="$1"
name="$2"
begin
flush
}
bats_tap_stream_ok() {
index="$1"
((++passed))
if [[ "$1" == "--duration" ]]; then
pass "$2"
else
pass
fi
}
bats_tap_stream_skipped() {
index="$1"
((++skipped))
skip "$3"
}
bats_tap_stream_not_ok() {
index="$1"
((++failures))
if [[ "$1" == "--duration" ]]; then
fail "$2"
else
fail
fi
}
bats_tap_stream_comment() {
log "$1"
}
bats_tap_stream_suite() {
: #test_file="$1"
}
bats_parse_internal_extended_tap
summary

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -e
# shellcheck source=lib/bats-core/formatter.bash
source "$BATS_ROOT/lib/bats-core/formatter.bash"
bats_tap_stream_plan() {
printf "1..%d\n" "$1"
}
bats_tap_stream_begin() { #<test index> <test name>
:
}
bats_tap_stream_ok() { # [--duration <milliseconds] <test index> <test name>
if [[ "$1" == "--duration" ]]; then
printf "ok %d %s # in %d ms\n" "$3" "$4" "$2"
else
printf "ok %d %s\n" "$1" "$2"
fi
}
bats_tap_stream_not_ok() { # [--duration <milliseconds>] <test index> <test name>
if [[ "$1" == "--duration" ]]; then
printf "not ok %d %s # in %d ms\n" "$3" "$4" "$2"
else
printf "not ok %d %s\n" "$1" "$2"
fi
}
bats_tap_stream_skipped() { # <test index> <test name> <reason>
if [[ -n "$3" ]]; then
printf "ok %d %s # skip %s\n" "$1" "$2" "$3"
else
printf "ok %d %s # skip\n" "$1" "$2"
fi
}
bats_tap_stream_comment() { # <comment text without leading '# '>
printf "# %s\n" "$1"
}
bats_tap_stream_suite() { # <file name>
:
}
bats_tap_stream_unknown() { # <full line>
printf "%s\n" "$1"
}
bats_parse_internal_extended_tap

@ -1,177 +0,0 @@
#!/usr/bin/env bash
set -e
# Just stream the TAP output (sans extended syntax) if tput is missing
if ! command -v tput >/dev/null; then
exec grep -v "^begin "
fi
header_pattern='[0-9]+\.\.[0-9]+'
IFS= read -r header
if [[ "$header" =~ $header_pattern ]]; then
count="${header:3}"
index=0
failures=0
skipped=0
name=""
count_column_width=$(( ${#count} * 2 + 2 ))
else
# If the first line isn't a TAP plan, print it and pass the rest through
printf "%s\n" "$header"
exec cat
fi
update_screen_width() {
screen_width="$(tput cols)"
count_column_left=$(( $screen_width - $count_column_width ))
}
trap update_screen_width WINCH
update_screen_width
begin() {
go_to_column 0
printf_with_truncation $(( $count_column_left - 1 )) " %s" "$name"
clear_to_end_of_line
go_to_column $count_column_left
printf "%${#count}s/${count}" "$index"
go_to_column 1
}
pass() {
go_to_column 0
printf " ✓ %s" "$name"
advance
}
skip() {
local reason="$1"
if [[ -n "$reason" ]]; then
reason=": $reason"
fi
go_to_column 0
printf " - %s (skipped%s)" "$name" "$reason"
advance
}
fail() {
go_to_column 0
set_color 1 bold
printf " ✗ %s" "$name"
advance
}
log() {
set_color 1
printf " %s\n" "$1"
clear_color
}
summary() {
printf "\n%d test" "$count"
if [[ "$count" -ne 1 ]]; then
printf 's'
fi
printf ", %d failure" "$failures"
if [[ "$failures" -ne 1 ]]; then
printf 's'
fi
if [[ "$skipped" -gt 0 ]]; then
printf ", %d skipped" "$skipped"
fi
printf "\n"
}
printf_with_truncation() {
local width="$1"
shift
local string
printf -v 'string' -- "$@"
if [[ "${#string}" -gt "$width" ]]; then
printf "%s..." "${string:0:$(( $width - 4 ))}"
else
printf "%s" "$string"
fi
}
go_to_column() {
local column="$1"
printf "\x1B[%dG" $(( $column + 1 ))
}
clear_to_end_of_line() {
printf "\x1B[K"
}
advance() {
clear_to_end_of_line
printf '\n'
clear_color
}
set_color() {
local color="$1"
local weight=22
if [[ "$2" == 'bold' ]]; then
weight=1
fi
printf "\x1B[%d;%dm" $(( 30 + $color )) "$weight"
}
clear_color() {
printf "\x1B[0m"
}
_buffer=""
buffer() {
_buffer="${_buffer}$("$@")"
}
flush() {
printf "%s" "$_buffer"
_buffer=""
}
finish() {
flush
printf "\n"
}
trap finish EXIT
while IFS= read -r line; do
case "$line" in
"begin "* )
let index+=1
name="${line#* $index }"
buffer begin
flush
;;
"ok "* )
skip_expr="ok $index (.*) # skip ?(([[:print:]]*))?"
if [[ "$line" =~ $skip_expr ]]; then
let skipped+=1
buffer skip "${BASH_REMATCH[2]}"
else
buffer pass
fi
;;
"not ok "* )
let failures+=1
buffer fail
;;
"# "* )
buffer log "${line:2}"
;;
esac
done
buffer summary

@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -e
while [[ "$#" -ne 0 ]]; do
case "$1" in
-T)
BATS_ENABLE_TIMING="-T"
;;
esac
shift
done
header_pattern='[0-9]+\.\.[0-9]+'
IFS= read -r header
if [[ "$header" =~ $header_pattern ]]; then
printf "TAP version 13\n"
printf "%s\n" "$header"
else
# If the first line isn't a TAP plan, print it and pass the rest through
printf '%s\n' "$header"
exec cat
fi
yaml_block_open=''
add_yaml_entry() {
if [[ -z "$yaml_block_open" ]]; then
printf " ...\n"
fi
printf " %s: %s\n" "$1" "$2"
yaml_block_open=1
}
close_previous_yaml_block() {
if [[ -n "$yaml_block_open" ]]; then
printf " ...\n"
yaml_block_open=''
fi
}
while IFS= read -r line; do
case "$line" in
'begin '*) ;;
'ok '*)
close_previous_yaml_block
number_of_printed_log_lines_for_this_test_so_far=0
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
timing_expr="(ok [0-9]+ .+) in ([0-9]+)ms$"
if [[ "$line" =~ $timing_expr ]]; then
printf "%s\n" "${BASH_REMATCH[1]}"
add_yaml_entry "duration_ms" "${BASH_REMATCH[2]}"
else
echo "Could not match output line to timing regex: $line" >&2
exit 1
fi
else
printf "%s\n" "${line}"
fi
;;
'not ok '*)
close_previous_yaml_block
number_of_printed_log_lines_for_this_test_so_far=0
timing_expr="not ok [0-9]+ (.)+ in ([0-9])+ms$"
if [[ -n "$BATS_ENABLE_TIMING" ]]; then
if [[ "$line" =~ $timing_expr ]]; then
printf "%s\n" "${BATS_REMATCH[1]}"
add_yaml_entry "duration_ms" "${BASH_REMATCH[2]}"
else
echo "Could not match failure line to timing regex: $line" >&2
exit 1
fi
else
printf "%s\n" "${line}"
fi
;;
'# '*)
if [[ $number_of_printed_log_lines_for_this_test_so_far -eq 0 ]]; then
add_yaml_entry "message" "|" # use a multiline string for this entry
fi
((++number_of_printed_log_lines_for_this_test_so_far))
printf " %s\n" "$(echo "\"${line}\"" | cut -b 3-)"
;;
'suite '*) ;;
esac
done
# close the final block if there was one
close_previous_yaml_block

@ -1,9 +1,9 @@
#!/usr/bin/env bash
set -e
encode_name() {
bats_encode_test_name() {
local name="$1"
local result="test_"
local result='test_'
local hex_code
if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then
@ -15,14 +15,14 @@ encode_name() {
local length="${#name}"
local char i
for ((i=0; i<length; i++)); do
for ((i = 0; i < length; i++)); do
char="${name:$i:1}"
if [[ "$char" = " " ]]; then
result+="_"
if [[ "$char" == ' ' ]]; then
result+='_'
elif [[ "$char" =~ [[:alnum:]] ]]; then
result+="$char"
else
printf -v 'hex_code' -- "-%02x" \'"$char"
printf -v 'hex_code' -- '-%02x' \'"$char"
result+="$hex_code"
fi
done
@ -31,24 +31,26 @@ encode_name() {
printf -v "$2" '%s' "$result"
}
test_file="$1"
tests=()
index=0
{
while IFS= read -r line; do
line="${line//$'\r'/}"
if [[ "$line" =~ $BATS_TEST_PATTERN ]] || [[ "$line" =~ $BATS_TEST_PATTERN_COMMENT ]]; then
name="${BASH_REMATCH[1]#[\'\"]}"
name="${name%[\'\"]}"
body="${BASH_REMATCH[2]}"
bats_encode_test_name "$name" 'encoded_name'
printf '%s() { bats_test_begin "%s"; %s\n' "${encoded_name:?}" "$name" "$body" || :
while IFS= read -r line; do
line="${line//$'\r'}"
let index+=1
if [[ "$line" =~ $BATS_TEST_PATTERN ]]; then
name="${BASH_REMATCH[1]#[\'\"]}"
name="${name%[\'\"]}"
body="${BASH_REMATCH[2]}"
encode_name "$name" 'encoded_name'
tests["${#tests[@]}"]="$encoded_name"
printf '%s() { bats_test_begin "%s" %d; %s\n' "$encoded_name" "$name" \
"$index" "$body"
else
printf "%s\n" "$line"
fi
done
if [[ -z "$BATS_TEST_FILTER" || "$name" =~ $BATS_TEST_FILTER ]]; then
tests+=("$encoded_name")
fi
else
printf '%s\n' "$line"
fi
done
} <<<"$(<"$test_file")"$'\n'
for test_name in "${tests[@]}"; do
printf 'bats_test_function %s\n' "$test_name"

@ -1,10 +1,23 @@
RONN := ronn
# Makefile
#
# bats-core manpages
#
RONN := ronn -W
PAGES := bats.1 bats.7
ORG := bats-core
MANUAL := 'Bash Automated Testing System'
ISOFMT := $(shell date -I)
RM := rm -f
.PHONY: all clean
all: $(PAGES)
bats.1: bats.1.ronn
$(RONN) -r $<
$(RONN) --date=$(ISOFMT) --manual=$(MANUAL) --organization=$(ORG) --roff $<
bats.7: bats.7.ronn
$(RONN) -r $<
$(RONN) --date=$(ISOFMT) --manual=$(MANUAL) --organization=$(ORG) --roff $<
clean:
$(RM) $(PAGES)

@ -1,16 +1,16 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "BATS" "1" "August 2014" "" ""
.TH "BATS" "1" "November 2020" "bats-core" "Bash Automated Testing System"
.
.SH "NAME"
\fBbats\fR \- Bash Automated Testing System
.
.SH "SYNOPSIS"
bats [\-c] [\-p | \-t] \fItest\fR [\fItest\fR \.\.\.]
Usage: bats [OPTIONS] \fItests\fR bats [\-h | \-v]
.
.P
\fItest\fR is the path to a Bats test file, or the path to a directory containing Bats test files\.
\fItests\fR is the path to a Bats test file, or the path to a directory containing Bats test files (ending with "\.bats")
.
.SH "DESCRIPTION"
Bats is a TAP\-compliant testing framework for Bash\. It provides a simple way to verify that the UNIX programs you write behave as expected\.
@ -32,29 +32,52 @@ You can invoke the \fBbats\fR interpreter with multiple test file arguments, or
.
.SH "OPTIONS"
.
.TP
\fB\-c\fR, \fB\-\-count\fR
Count the number of test cases without running any tests
.IP "\(bu" 4
\fB\-c\fR, \fB\-\-count\fR: Count the number of test cases without running any tests
.
.TP
\fB\-h\fR, \fB\-\-help\fR
Display help message
.IP "\(bu" 4
\fB\-f\fR, \fB\-\-filter <regex>\fR: Filter test cases by names matching the regular expression
.
.TP
\fB\-p\fR, \fB\-\-pretty\fR
Show results in pretty format (default for terminals)
.IP "\(bu" 4
\fB\-F\fR, \fB\-\-formatter <type>\fR: Switch between formatters: pretty (default), tap (default w/o term), tap13, junit
.
.TP
\fB\-r\fR, \fB\-\-recursive\fR
Include tests in subdirectories
.IP "\(bu" 4
\fB\-h\fR, \fB\-\-help\fR: Display this help message
.
.TP
\fB\-t\fR, \fB\-\-tap\fR
Show results in TAP format
.IP "\(bu" 4
\fB\-j\fR, \fB\-\-jobs <jobs>\fR: Number of parallel jobs (requires GNU parallel)
.
.TP
\fB\-v\fR, \fB\-\-version\fR
Display the version number
.IP "\(bu" 4
\fB\-\-no\-tempdir\-cleanup\fR: Preserve test output temporary directory
.
.IP "\(bu" 4
\fB\-\-no\-parallelize\-across\-files\fR Serialize test file execution instead of running them in parallel (requires \-\-jobs >1)
.
.IP "\(bu" 4
\fB\-\-no\-parallelize\-within\-files\fR Serialize test execution within files instead of running them in parallel (requires \-\-jobs >1)
.
.IP "\(bu" 4
\fB\-\-report\-formatter <type>\fR: Switch between reporters (same options as \-\-formatter)
.
.IP "\(bu" 4
\fB\-o\fR, \fB\-\-output <dir>\fR: Directory to write report files
.
.IP "\(bu" 4
\fB\-p\fR, \fB\-\-pretty\fR: Shorthand for "\-\-formatter pretty"
.
.IP "\(bu" 4
\fB\-r\fR, \fB\-\-recursive\fR: Include tests in subdirectories
.
.IP "\(bu" 4
\fB\-t\fR, \fB\-\-tap\fR: Shorthand for "\-\-formatter tap"
.
.IP "\(bu" 4
\fB\-T\fR, \fB\-\-timing\fR: Add timing information to tests
.
.IP "\(bu" 4
\fB\-v\fR, \fB\-\-version\fR: Display the version number
.
.IP "" 0
.
.SH "OUTPUT"
When you run Bats from a terminal, you\'ll see output as each test is performed, with a check\-mark next to the test\'s name if it passes or an "X" if it fails\.
@ -99,9 +122,10 @@ Bats wiki: \fIhttps://github\.com/bats\-core/bats\-core/wiki/\fR
\fBbash\fR(1), \fBbats\fR(7)
.
.SH "COPYRIGHT"
(c) 2017 Bianca Tamayo (bats-core organization)
(c) 2014 Sam Stephenson
(c) 2017\-2018 bats\-core organization
.
.br
(c) 2011\-2016 Sam Stephenson
.
.P
Bats is released under the terms of an MIT\-style license\.

@ -5,10 +5,11 @@ bats(1) -- Bash Automated Testing System
SYNOPSIS
--------
bats [-c] [-p | -t] <test> [<test> ...]
Usage: bats [OPTIONS] <tests>
bats [-h | -v]
<test> is the path to a Bats test file, or the path to a directory
containing Bats test files.
<tests> is the path to a Bats test file, or the path to a directory
containing Bats test files (ending with ".bats")
DESCRIPTION
@ -48,14 +49,32 @@ OPTIONS
* `-c`, `--count`:
Count the number of test cases without running any tests
* `-f`, `--filter <regex>`:
Filter test cases by names matching the regular expression
* `-F`, `--formatter <type>`:
Switch between formatters: pretty (default), tap (default w/o term), tap13, junit
* `-h`, `--help`:
Display help message
Display this help message
* `-j`, `--jobs <jobs>`:
Number of parallel jobs (requires GNU parallel)
* `--no-tempdir-cleanup`:
Preserve test output temporary directory
* `--no-parallelize-across-files`
Serialize test file execution instead of running them in parallel (requires --jobs >1)
* `--no-parallelize-within-files`
Serialize test execution within files instead of running them in parallel (requires --jobs >1)
* `--report-formatter <type>`:
Switch between reporters (same options as --formatter)
* `-o`, `--output <dir>`:
Directory to write report files
* `-p`, `--pretty`:
Show results in pretty format (default for terminals)
Shorthand for "--formatter pretty"
* `-r`, `--recursive`:
Include tests in subdirectories
* `-t`, `--tap`:
Show results in TAP format
Shorthand for "--formatter tap"
* `-T`, `--timing`:
Add timing information to tests
* `-v`, `--version`:
Display the version number
@ -103,8 +122,8 @@ Bats wiki: _https://github.com/bats\-core/bats\-core/wiki/_
COPYRIGHT
---------
(c) 2017 Bianca Tamayo (bats-core organization)
(c) 2014 Sam Stephenson
(c) 2017-2018 bats-core organization<br/>
(c) 2011-2016 Sam Stephenson
Bats is released under the terms of an MIT-style license.

@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "BATS" "7" "November 2013" "" ""
.TH "BATS" "7" "November 2020" "bats-core" "Bash Automated Testing System"
.
.SH "NAME"
\fBbats\fR \- Bats test file format
@ -170,8 +170,14 @@ There are several global variables you can use to introspect on Bats tests:
\fB$BATS_TEST_NUMBER\fR is the (1\-based) index of the current test case in the test file\.
.
.IP "\(bu" 4
\fB$BATS_SUITE_TEST_NUMBER\fR is the (1\-based) index of the current test case in the test suite (over all files)\.
.
.IP "\(bu" 4
\fB$BATS_TMPDIR\fR is the location to a directory that may be used to store temporary files\.
.
.IP "\(bu" 4
\fB$BATS_FILE_EXTENSION\fR (default: \fBbats\fR) specifies the extension of test files that should be found when running a suite (via \fBbats [\-r] suite_folder/\fR)
.
.IP "" 0
.
.SH "SEE ALSO"

@ -146,8 +146,13 @@ test case.
case.
* `$BATS_TEST_NUMBER` is the (1-based) index of the current test case
in the test file.
* `$BATS_SUITE_TEST_NUMBER` is the (1-based) index of the current test
case in the test suite (over all files).
* `$BATS_TMPDIR` is the location to a directory that may be used to
store temporary files.
* `$BATS_FILE_EXTENSION` (default: `bats`) specifies the extension of
test files that should be found when running a suite (via
`bats [-r] suite_folder/`)
SEE ALSO

@ -1,6 +1,6 @@
{
"name": "bats",
"version": "1.1.0",
"version": "1.3.0",
"description": "Bash Automated Testing System",
"homepage": "https://github.com/bats-core/bats-core#readme",
"license": "MIT",
@ -10,6 +10,7 @@
"files": [
"bin",
"libexec",
"lib",
"man"
],
"directories": {

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
targets=()
while IFS= read -r -d $'\0'; do
targets+=("$REPLY")
done < <(
find \
bin/bats \
libexec/bats-core \
lib/bats-core \
shellcheck.sh \
-type f \
-print0
)
LC_ALL=C.UTF-8 shellcheck "${targets[@]}"
exit $?

@ -3,19 +3,23 @@
load test_helper
fixtures bats
teardown() {
# cleanup the test local tmpdir to avoid cleaning up all tests' at once
test_helper::cleanup_tmpdir "$BATS_TEST_NAME"
}
@test "no arguments prints message and usage instructions" {
run bats
[ $status -eq 1 ]
[ "${lines[0]}" == 'Error: Must specify at least one <test>' ]
[ "${lines[2]%% *}" == 'Usage:' ]
[ "${lines[1]%% *}" == 'Usage:' ]
}
@test "invalid option prints message and usage instructions" {
run bats --invalid-option
[ $status -eq 1 ]
emit_debug_output
[ "${lines[0]}" == "Error: Bad command line option '-invalid-option'" ]
[ "${lines[2]%% *}" == 'Usage:' ]
[ "${lines[0]}" == "Error: Bad command line option '--invalid-option'" ]
[ "${lines[1]%% *}" == 'Usage:' ]
}
@test "-v and --version print version number" {
@ -51,6 +55,7 @@ fixtures bats
@test "summary passing tests" {
run filter_control_sequences bats -p "$FIXTURE_ROOT/passing.bats"
echo "$output"
[ $status -eq 0 ]
[ "${lines[1]}" = "1 test, 0 failures" ]
}
@ -62,7 +67,7 @@ fixtures bats
}
@test "tap passing and skipping tests" {
run filter_control_sequences bats --tap "$FIXTURE_ROOT/passing_and_skipping.bats"
run filter_control_sequences bats --formatter tap "$FIXTURE_ROOT/passing_and_skipping.bats"
[ $status -eq 0 ]
[ "${lines[0]}" = "1..3" ]
[ "${lines[1]}" = "ok 1 a passing test" ]
@ -83,7 +88,7 @@ fixtures bats
}
@test "tap passing, failing and skipping tests" {
run filter_control_sequences bats --tap "$FIXTURE_ROOT/passing_failing_and_skipping.bats"
run filter_control_sequences bats --formatter tap "$FIXTURE_ROOT/passing_failing_and_skipping.bats"
[ $status -eq 0 ]
[ "${lines[0]}" = "1..3" ]
[ "${lines[1]}" = "ok 1 a passing test" ]
@ -138,7 +143,7 @@ fixtures bats
}
@test "setup is run once before each test" {
make_bats_test_suite_tmpdir
make_bats_test_suite_tmpdir "$BATS_TEST_NAME"
run bats "$FIXTURE_ROOT/setup.bats"
[ $status -eq 0 ]
run cat "$BATS_TEST_SUITE_TMPDIR/setup.log"
@ -146,7 +151,7 @@ fixtures bats
}
@test "teardown is run once after each test, even if it fails" {
make_bats_test_suite_tmpdir
make_bats_test_suite_tmpdir "$BATS_TEST_NAME"
run bats "$FIXTURE_ROOT/teardown.bats"
[ $status -eq 1 ]
run cat "$BATS_TEST_SUITE_TMPDIR/teardown.log"
@ -164,6 +169,7 @@ fixtures bats
@test "passing test with teardown failure" {
PASS=1 run bats "$FIXTURE_ROOT/failing_teardown.bats"
[ $status -eq 1 ]
echo "$output"
[ "${lines[1]}" = 'not ok 1 truth' ]
[ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 2)" ]
[ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ]
@ -184,7 +190,7 @@ fixtures bats
}
@test "failing test file outside of BATS_CWD" {
make_bats_test_suite_tmpdir
make_bats_test_suite_tmpdir "$BATS_TEST_NAME"
cd "$BATS_TEST_SUITE_TMPDIR"
run bats "$FIXTURE_ROOT/failing.bats"
[ $status -eq 1 ]
@ -196,6 +202,11 @@ fixtures bats
[ $status -eq 0 ]
}
@test "load sources relative scripts with filename extension" {
HELPER_NAME="test_helper.bash" run bats "$FIXTURE_ROOT/load.bats"
[ $status -eq 0 ]
}
@test "load aborts if the specified script does not exist" {
HELPER_NAME="nonexistent" run bats "$FIXTURE_ROOT/load.bats"
[ $status -eq 1 ]
@ -211,6 +222,55 @@ fixtures bats
[ $status -eq 1 ]
}
@test "load relative script with ambiguous name" {
HELPER_NAME="ambiguous" run bats "$FIXTURE_ROOT/load.bats"
[ $status -eq 0 ]
}
@test "load supports scripts on the PATH" {
path_dir="$BATS_TMPNAME/path"
mkdir -p "$path_dir"
cp "${FIXTURE_ROOT}/test_helper.bash" "${path_dir}/on_path"
PATH="${path_dir}:$PATH" HELPER_NAME="on_path" run bats "$FIXTURE_ROOT/load.bats"
[ $status -eq 0 ]
}
@test "load supports plain symbols" {
local -r helper="${BATS_TMPDIR}/load_helper_plain"
{
echo "plain_variable='value of plain variable'"
echo "plain_array=(test me hard)"
} > "${helper}"
load "${helper}"
[ "${plain_variable}" = 'value of plain variable' ]
[ "${plain_array[2]}" = 'hard' ]
rm "${helper}"
}
@test "load doesn't support _declare_d symbols" {
local -r helper="${BATS_TMPDIR}/load_helper_declared"
{
echo "declare declared_variable='value of declared variable'"
echo "declare -r a_constant='constant value'"
echo "declare -i an_integer=0x7e4"
echo "declare -a an_array=(test me hard)"
echo "declare -x exported_variable='value of exported variable'"
} > "${helper}"
load "${helper}"
! [ "${declared_variable:-}" = 'value of declared variable' ]
! [ "${a_constant:-}" = 'constant value' ]
! (( "${an_integer:-2019}" == 2020 ))
! [ "${an_array[2]:-}" = 'hard' ]
! [ "${exported_variable:-}" = 'value of exported variable' ]
rm "${helper}"
}
@test "output is discarded for passing tests and printed for failing tests" {
run bats "$FIXTURE_ROOT/output.bats"
[ $status -eq 1 ]
@ -264,16 +324,51 @@ fixtures bats
}
@test "extended syntax" {
run bats-exec-test -x "$FIXTURE_ROOT/failing_and_passing.bats"
emulate_bats_env
run bats-exec-suite -x "$FIXTURE_ROOT/failing_and_passing.bats"
echo "$output"
[ $status -eq 1 ]
[ "${lines[1]}" = "suite $FIXTURE_ROOT/failing_and_passing.bats" ]
[ "${lines[2]}" = 'begin 1 a failing test' ]
[ "${lines[3]}" = 'not ok 1 a failing test' ]
[ "${lines[6]}" = 'begin 2 a passing test' ]
[ "${lines[7]}" = 'ok 2 a passing test' ]
}
@test "timing syntax" {
run bats -T "$FIXTURE_ROOT/failing_and_passing.bats"
echo "$output"
[ $status -eq 1 ]
[ "${lines[1]}" = 'begin 1 a failing test' ]
[ "${lines[2]}" = 'not ok 1 a failing test' ]
[ "${lines[5]}" = 'begin 2 a passing test' ]
[ "${lines[6]}" = 'ok 2 a passing test' ]
regex='not ok 1 a failing test in [0-9]+ms'
[[ "${lines[1]}" =~ $regex ]]
regex='ok 2 a passing test in [0-9]+ms'
[[ "${lines[4]}" =~ $regex ]]
}
@test "extended timing syntax" {
emulate_bats_env
run bats-exec-suite -x -T "$FIXTURE_ROOT/failing_and_passing.bats"
echo "$output"
[ $status -eq 1 ]
regex="not ok 1 a failing test in [0-9]+ms"
[ "${lines[2]}" = 'begin 1 a failing test' ]
[[ "${lines[3]}" =~ $regex ]]
[ "${lines[6]}" = 'begin 2 a passing test' ]
regex="ok 2 a passing test in [0-9]+ms"
[[ "${lines[7]}" =~ $regex ]]
}
@test "time is greater than 0ms for long test" {
emulate_bats_env
run bats-exec-suite -x -T "$FIXTURE_ROOT/run_long_command.bats"
echo "$output"
[ $status -eq 0 ]
regex="ok 1 run long command in [1-9][0-9]*ms"
[[ "${lines[3]}" =~ $regex ]]
}
@test "pretty and tap formats" {
run bats --tap "$FIXTURE_ROOT/passing.bats"
run bats --formatter tap "$FIXTURE_ROOT/passing.bats"
tap_output="$output"
[ $status -eq 0 ]
@ -285,8 +380,8 @@ fixtures bats
}
@test "pretty formatter bails on invalid tap" {
run bats --tap "$FIXTURE_ROOT/invalid_tap.bats"
[ $status -eq 1 ]
run bats-format-pretty < <(printf "This isn't TAP!\nGood day to you\n")
[ $status -eq 0 ]
[ "${lines[0]}" = "This isn't TAP!" ]
[ "${lines[1]}" = "Good day to you" ]
}
@ -379,19 +474,19 @@ END_OF_ERR_MSG
[ "${lines[11]}" = 'ok 11 ' ] # empty name from single quote
}
@test "duplicate tests cause a warning on stderr" {
run bats "$FIXTURE_ROOT/duplicate-tests.bats"
@test "duplicate tests error and generate a warning on stderr" {
run bats --tap "$FIXTURE_ROOT/duplicate-tests.bats"
[ $status -eq 1 ]
local expected='bats warning: duplicate test name(s) in '
expected+="$FIXTURE_ROOT/duplicate-tests.bats: test_gizmo_test"
local expected='Error: Duplicate test name(s) in file '
expected+="\"${FIXTURE_ROOT}/duplicate-tests.bats\": test_gizmo_test"
printf 'expected: "%s"\n' "$expected" >&2
printf 'actual: "%s"\n' "${lines[0]}" >&2
[ "${lines[0]}" = "$expected" ]
printf 'num lines: %d\n' "${#lines[*]}" >&2
[ "${#lines[*]}" = "7" ]
[ "${#lines[*]}" = "1" ]
}
@test "sourcing a nonexistent file in setup produces error output" {
@ -467,3 +562,176 @@ END_OF_ERR_MSG
[ "${lines[5]}" = '# bar' ]
[ "${lines[6]}" = '# baz' ]
}
@test "run tests which consume stdin (see #197)" {
run bats "$FIXTURE_ROOT/read_from_stdin.bats"
[ "$status" -eq 0 ]
[[ "${lines[0]}" == "1..3" ]]
[[ "${lines[1]}" == "ok 1 test 1" ]]
[[ "${lines[2]}" == "ok 2 test 2 with TAB in name" ]]
[[ "${lines[3]}" == "ok 3 test 3" ]]
}
@test "report correct line on unset variables" {
LANG=C run bats "$FIXTURE_ROOT/unbound_variable.bats"
[ "$status" -eq 1 ]
[ "${#lines[@]}" -eq 9 ]
[ "${lines[1]}" = 'not ok 1 access unbound variable' ]
[ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/unbound_variable.bats, line 8)" ]
[ "${lines[3]}" = "# \`foo=\$unset_variable' failed" ]
[[ "${lines[4]}" =~ ".src: line 8:" ]]
[ "${lines[5]}" = 'not ok 2 access second unbound variable' ]
[ "${lines[6]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/unbound_variable.bats, line 13)" ]
[ "${lines[7]}" = "# \`foo=\$second_unset_variable' failed" ]
[[ "${lines[8]}" =~ ".src: line 13:" ]]
}
@test "report correct line on external function calls" {
run bats "$FIXTURE_ROOT/external_function_calls.bats"
[ "$status" -eq 1 ]
expectedNumberOfTests=12
linesOfOutputPerTest=3
[ "${#lines[@]}" -gt $((expectedNumberOfTests * linesOfOutputPerTest + 1)) ]
outputOffset=1
currentErrorLine=9
linesPerTest=5
for t in $(seq $expectedNumberOfTests); do
[[ "${lines[$outputOffset]}" =~ "not ok $t " ]]
# Skip backtrace into external function if set
if [[ "${lines[$((outputOffset + 1))]}" =~ "# (from function " ]]; then
outputOffset=$((outputOffset + 1))
parenChar=" "
else
parenChar="("
fi
[ "${lines[$((outputOffset + 1))]}" = "# ${parenChar}in test file $RELATIVE_FIXTURE_ROOT/external_function_calls.bats, line $currentErrorLine)" ]
[[ "${lines[$((outputOffset + 2))]}" =~ " failed" ]]
outputOffset=$((outputOffset + 3))
currentErrorLine=$((currentErrorLine + linesPerTest))
done
}
@test "test count validator catches mismatch and returns non zero" {
source "$BATS_ROOT/lib/bats-core/validator.bash"
export -f bats_test_count_validator
run bash -c "echo $'1..1\n' | bats_test_count_validator"
[[ $status -ne 0 ]]
run bash -c "echo $'1..1\nok 1\nok 2' | bats_test_count_validator"
[[ $status -ne 0 ]]
run bash -c "echo $'1..1\nok 1' | bats_test_count_validator"
[[ $status -eq 0 ]]
}
@test "running the same file twice runs its tests twice without errors" {
run bats "$FIXTURE_ROOT/passing.bats" "$FIXTURE_ROOT/passing.bats"
echo "$output"
[[ $status -eq 0 ]]
[[ "${lines[0]}" == "1..2" ]] # got 2x1 tests
}
@test "Don't use unbound variables inside bats (issue #340)" {
run bats "$FIXTURE_ROOT/set_-eu_in_setup_and_teardown.bats"
echo "$output"
[[ "${lines[0]}" == "1..4" ]]
[[ "${lines[1]}" == "ok 1 skipped test # skip" ]]
[[ "${lines[2]}" == "ok 2 skipped test with reason # skip reason" ]]
[[ "${lines[3]}" == "ok 3 passing test" ]]
[[ "${lines[4]}" == "not ok 4 failing test" ]]
[[ "${lines[5]}" == "# (in test file $RELATIVE_FIXTURE_ROOT/set_-eu_in_setup_and_teardown.bats, line 22)" ]]
[[ "${lines[6]}" == "# \`false' failed" ]]
[[ "${#lines[@]}" -eq 7 ]]
}
@test "filenames with tab can be used" {
[[ "$OSTYPE" == "linux"* ]] || skip "FS cannot deal with tabs in filenames"
cp "$FIXTURE_ROOT/tab in filename.bats" "$FIXTURE_ROOT/tab"$'\t'"in filename.bats"
bats "$FIXTURE_ROOT/tab"$'\t'"in filename.bats"
}
@test "each file is evaluated n+1 times" {
make_bats_test_suite_tmpdir
export TEMPFILE="$BATS_TEST_SUITE_TMPDIR/$BATS_TEST_NAME.log"
run bats "$FIXTURE_ROOT/evaluation_count/"
cat "$TEMPFILE"
run grep "file1" "$TEMPFILE"
[[ ${#lines[@]} -eq 2 ]]
run grep "file2" "$TEMPFILE"
[[ ${#lines[@]} -eq 3 ]]
}
@test "Don't hang on CTRL-C (issue #353)" {
# guarantee that background processes get their own process group -> pid=pgid
set -m
run bats "$FIXTURE_ROOT/run_long_command.bats" & # don't block execution, or we cannot send signals
echo "$output"
SUBPROCESS_PID=$!
sleep 1 # wait for the background process to start on slow systems
# emulate CTRL-C by sending SIGINT to the whole process group
kill -SIGINT -- -$SUBPROCESS_PID
sleep 1 # wait for the signal to be acted upon
# when the process is gone, we cannot deliver a signal anymore, getting non-zero from kill
run kill -0 -- -$SUBPROCESS_PID
[[ $status -ne 0 ]] \
|| (kill -9 -- -$SUBPROCESS_PID; false)
# ^ kill the process for good when SIGINT failed,
# to avoid waiting endlessly for stuck children to finish
}
@test "test comment style" {
run bats "$FIXTURE_ROOT/comment_style.bats"
[ $status -eq 0 ]
[ "${lines[0]}" = '1..6' ]
[ "${lines[1]}" = 'ok 1 should_be_found' ]
[ "${lines[2]}" = 'ok 2 should_be_found_with_trailing_whitespace' ]
[ "${lines[3]}" = 'ok 3 should_be_found_with_parens' ]
[ "${lines[4]}" = 'ok 4 should_be_found_with_parens_and_whitespace' ]
[ "${lines[5]}" = 'ok 5 should_be_found_with_function_and_parens' ]
[ "${lines[6]}" = 'ok 6 should_be_found_with_function_parens_and_whitespace' ]
}
@test "test works even if PATH is reset" {
run bats "$FIXTURE_ROOT/update_path_env.bats"
[ "$status" -eq 1 ]
[ "${lines[4]}" = "# /usr/local/bin:/usr/bin:/bin" ]
}
@test "Test nounset does not trip up bats' internals (see #385)" {
# don't export nounset within this file or we might trip up the testsuite itself,
# getting bad diagnostics
run bash -c "set -o nounset; export SHELLOPTS; bats --tap '$FIXTURE_ROOT/passing.bats'"
echo "$output"
[ "${lines[0]}" = "1..1" ]
[ "${lines[1]}" = "ok 1 a passing test" ]
[ ${#lines[@]} = 2 ]
}
@test "run tmpdir is cleaned up by default" {
TEST_TMPDIR="${BATS_RUN_TMPDIR}/$BATS_TEST_NAME"
bats --tempdir "$TEST_TMPDIR" "$FIXTURE_ROOT/passing.bats"
[ ! -d "$TEST_TMPDIR" ]
}
@test "run tmpdir is not cleanup up with --no-cleanup-tempdir" {
TEST_TMPDIR="${BATS_RUN_TMPDIR}/$BATS_TEST_NAME"
bats --tempdir "$TEST_TMPDIR" --no-tempdir-cleanup "$FIXTURE_ROOT/passing.bats"
[ -d "$TEST_TMPDIR" ]
# should also find preprocessed files!
[ $(find "$TEST_TMPDIR" -name '*.src' | wc -l) -eq 1 ]
}

@ -0,0 +1,179 @@
load 'test_helper'
fixtures file_setup_teardown
setup_file() {
export SETUP_FILE_EXPORT_TEST=true
}
setup() {
# give each test their own tmpdir to allow for parallelization without interference
make_bats_test_suite_tmpdir "$BATS_TEST_NAME"
}
teardown() {
test_helper::cleanup_tmpdir "$BATS_TEST_NAME"
}
@test "setup_file is run once per file" {
export LOG="$BATS_TEST_SUITE_TMPDIR/setup_file_once.log"
bats "$FIXTURE_ROOT/setup_file.bats"
}
@test "teardown_file is run once per file" {
export LOG="$BATS_TEST_SUITE_TMPDIR/teardown_file_once.log"
run bats "$FIXTURE_ROOT/teardown_file.bats"
[[ $status -eq 0 ]]
# output the log for faster debugging
cat "$LOG"
# expect to find an entry for the tested file
grep 'teardown_file.bats' "$LOG"
# it should be the only entry
run wc -l < "$LOG"
[[ $output -eq 1 ]]
}
@test "setup_file is called correctly in multi file suite" {
export LOG="$BATS_TEST_SUITE_TMPDIR/setup_file_multi_file_suite.log"
run bats "$FIXTURE_ROOT/setup_file.bats" "$FIXTURE_ROOT/no_setup_file.bats" "$FIXTURE_ROOT/setup_file2.bats"
[[ $status -eq 0 ]]
run wc -l < "$LOG"
# each setup_file[2].bats is in the log exactly once!
[[ $output -eq 2 ]]
grep setup_file.bats "$LOG"
grep setup_file2.bats "$LOG"
}
@test "teardown_file is called correctly in multi file suite" {
export LOG="$BATS_TEST_SUITE_TMPDIR/teardown_file_multi_file_suite.log"
run bats "$FIXTURE_ROOT/teardown_file.bats" "$FIXTURE_ROOT/no_teardown_file.bats" "$FIXTURE_ROOT/teardown_file2.bats"
[[ $status -eq 0 ]]
run wc -l < "$LOG"
# each teardown_file[2].bats is in the log exactly once!
[[ $output -eq 2 ]]
grep teardown_file.bats "$LOG"
grep teardown_file2.bats "$LOG"
}
@test "setup_file failure aborts tests for this file" {
# this might need to mark them as skipped as the test count is already determined at this point
run bats "$FIXTURE_ROOT/setup_file_failed.bats"
echo "$output"
[[ "${lines[0]}" == "1..2" ]]
[[ "${lines[1]}" == "not ok 1 setup_file failed" ]]
[[ "${lines[2]}" == "# (from function \`setup_file' in test file $RELATIVE_FIXTURE_ROOT/setup_file_failed.bats, line 2)" ]]
[[ "${lines[3]}" == "# \`false' failed" ]]
[[ "${lines[4]}" == "# bats warning: Executed 1 instead of expected 2 tests" ]] # this warning is expected
# to appease the count validator, we would have to reduce the expected number of tests (retroactively?) or
# output even those tests that should be skipped due to a failed setup_file.
# Since we are already in a failure mode, the additional error does not hurt and is less verbose than
# printing all the failed/skipped tests due to the setup failure.
}
@test "teardown_file failure fails at least one test from the file" {
run bats "$FIXTURE_ROOT/teardown_file_failed.bats"
[[ $status -ne 0 ]]
echo "$output"
[[ "${lines[0]}" == "1..1" ]]
[[ "${lines[1]}" == "ok 1 test" ]]
[[ "${lines[2]}" == "not ok 2 teardown_file failed" ]]
[[ "${lines[3]}" == "# (from function \`teardown_file' in test file $RELATIVE_FIXTURE_ROOT/teardown_file_failed.bats, line 3)" ]]
[[ "${lines[4]}" == "# \`false' failed" ]]
[[ "${lines[5]}" == "# bats warning: Executed 2 instead of expected 1 tests" ]] # for now this warning is expected
# for a failed teardown_file not to change the number of tests being reported, we would have to alter at least one provious test result report
# this would require arbitrary amounts of buffering so we simply add our own line with a fake test number
# tripping the count validator won't change the overall result, as we already are in a failure mode
}
@test "teardown_file runs even if any test in the file failed" {
export LOG="$BATS_TEST_SUITE_TMPDIR/teardown_file_failed.log"
run bats "$FIXTURE_ROOT/teardown_file_after_failing_test.bats"
[[ $status -ne 0 ]]
grep teardown_file_after_failing_test.bats "$LOG"
echo "$output"
[[ $output == "1..1
not ok 1 failing test
# (in test file $RELATIVE_FIXTURE_ROOT/teardown_file_after_failing_test.bats, line 6)
# \`false' failed" ]]
}
@test "teardown_file should run even after user abort via CTRL-C" {
export LOG="$BATS_TEST_SUITE_TMPDIR/teardown_file_abort.log"
STARTTIME=$SECONDS
# guarantee that background processes get their own process group -> pid=pgid
set -m
SECONDS=0
# run testsubprocess in background to not avoid blocking this test
bats "$FIXTURE_ROOT/teardown_file_after_long_test.bats"&
SUBPROCESS_PID=$!
# wait until we enter the test
sleep 2
# fake sending SIGINT (CTRL-C) to the process group of the background subprocess
kill -SIGINT -- -$SUBPROCESS_PID
wait # for the test to finish either way (SIGINT or normal execution)
echo "Waited: $SECONDS seconds"
[[ $SECONDS -lt 10 ]] # make sure we really cut it short with SIGINT
# check that teardown_file ran and created the log file
[[ -f "$LOG" ]]
grep teardown_file_after_long_test.bats "$LOG"
# but the test must not have run to the end!
! grep "test finished successfully" "$LOG"
}
@test "setup_file runs even if all tests in the file are skipped" {
export LOG="$BATS_TEST_SUITE_TMPDIR/setup_file_skipped.log"
run bats "$FIXTURE_ROOT/setup_file_even_if_all_tests_are_skipped.bats"
[[ -f "$LOG" ]]
grep setup_file_even_if_all_tests_are_skipped.bats "$LOG"
}
@test "teardown_file runs even if all tests in the file are skipped" {
export LOG="$BATS_TEST_SUITE_TMPDIR/teardown_file_skipped.log"
run bats "$FIXTURE_ROOT/teardown_file_even_if_all_tests_are_skipped.bats"
[[ $status -eq 0 ]]
[[ -f "$LOG" ]]
grep teardown_file_even_if_all_tests_are_skipped.bats "$LOG"
}
@test "setup_file must not leak context between tests in the same suite" {
# example: BATS_ROOT was unset in one test but used in others, therefore, the suite failed
# Simulate leaking env var from first to second test by: export SETUP_FILE_VAR="LEAK!"
run bats "$FIXTURE_ROOT/setup_file_does_not_leak_env.bats" "$FIXTURE_ROOT/setup_file_does_not_leak_env2.bats"
[[ $status -eq 0 ]] || (echo $output; return 1)
}
@test "teardown_file must not leak context between tests in the same suite" {
# example: BATS_ROOT was unset in one test but used in others, therefore, the suite failed
run bats "$FIXTURE_ROOT/teardown_file_does_not_leak.bats" "$FIXTURE_ROOT/teardown_file_does_not_leak2.bats"
echo "$output"
[[ $status -eq 0 ]]
[[ $output == "1..2
ok 1 test
ok 2 must not see variable from first run" ]]
}
@test "halfway setup_file errors are caught and reported" {
run bats "$FIXTURE_ROOT/setup_file_halfway_error.bats"
[[ $status -ne 0 ]]
echo "$output"
[[ "$output" == "1..1
not ok 1 setup_file failed
# (from function \`setup_file' in test file $RELATIVE_FIXTURE_ROOT/setup_file_halfway_error.bats, line 3)
# \`false' failed" ]]
}
@test "halfway teardown_file errors are caught and reported" {
run bats "$FIXTURE_ROOT/teardown_file_halfway_error.bats"
echo "$output"
[[ $status -ne 0 ]]
[[ "${lines[0]}" == "1..1" ]]
[[ "${lines[1]}" == "ok 1 empty" ]]
[[ "${lines[2]}" == "not ok 2 teardown_file failed" ]]
[[ "${lines[3]}" == "# (from function \`teardown_file' in test file $RELATIVE_FIXTURE_ROOT/teardown_file_halfway_error.bats, line 3)" ]]
[[ "${lines[4]}" == "# \`false' failed" ]]
[[ "${lines[5]}" == "# bats warning: Executed 2 instead of expected 1 tests" ]] # for now this warning is expected
}
@test "variables exported in setup_file are visible in tests" {
[[ $SETUP_FILE_EXPORT_TEST == "true" ]]
}

@ -0,0 +1,5 @@
# Helper to detect regressions in load's lookup ordering.
# An exact name match should be prioritized over name.bash.
echo "Should not have loaded this file!" >&2
exit 1

@ -0,0 +1,6 @@
# Helper to detect regressions in load's lookup ordering.
# An exact name match should be prioritized over name.bash.
echo "This is the expected file to load."
help_me() { :; }

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Fractional timeout supported in bash 4+
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
timeout=1
else
timeout=0.01
fi
# Just reading from stdin
while read -r -t $timeout foo; do
if [ "$foo" == "EXIT" ]; then
echo "Found"
exit 0
fi
done
echo "Not found"
exit 1

@ -0,0 +1,32 @@
function should_be_found { # @test
true
}
function should_be_found_with_trailing_whitespace { # @test
true
}
should_be_found_with_parens() { #@test
true
}
should_be_found_with_parens_and_whitespace () { #@test
true
}
function should_be_found_with_function_and_parens() { #@test
true
}
function should_be_found_with_function_parens_and_whitespace () { #@test
true
}
should_not_be_found() {
false
#@test
}
should_not_be_found() {
false
} #@test

@ -0,0 +1,5 @@
echo "file1" >> "$TEMPFILE"
@test "test1" {
:
}

@ -0,0 +1,9 @@
echo "file2" >> "$TEMPFILE"
@test "test 1" {
:
}
@test "test 2" {
:
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save